diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt index 163f413740..713fb07a84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt @@ -11,14 +11,14 @@ import io.reactivex.rxjava3.subjects.Subject import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository /** * Activity which houses the gift flow. */ class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent { - override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) } + override val stripeRepository: StripeRepository by lazy { StripeRepository(this) } override val googlePayResultPublisher: Subject = PublishSubject.create() diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt index f96747f307..562ef1c6bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt @@ -1,14 +1,22 @@ package org.thoughtcrime.securesms.badges.gifts.flow +import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.models.Badge +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError +import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.PlatformCurrencyUtil +import org.thoughtcrime.securesms.util.ProfileUtil import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.internal.ServiceResponse +import java.io.IOException import java.util.Currency import java.util.Locale @@ -17,6 +25,10 @@ import java.util.Locale */ class GiftFlowRepository { + companion object { + private val TAG = Log.tag(GiftFlowRepository::class.java) + } + fun getGiftBadge(): Single> { return Single .fromCallable { @@ -44,4 +56,37 @@ class GiftFlowRepository { .mapValues { (currency, price) -> FiatMoney(price, currency) } } } + + /** + * Verifies that the given recipient is a supported target for a gift. + */ + 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, "Cannot send a gift to self.", true) + throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts + } + + if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientDatabase.RegisteredState.REGISTERED) { + Log.w(TAG, "Invalid badge recipient $badgeRecipient. Verification failed.", true) + throw DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid + } + + try { + val profile = ProfileUtil.retrieveProfileSync(ApplicationDependencies.getApplication(), recipient, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) + if (!profile.profile.capabilities.isGiftBadges) { + Log.w(TAG, "Badge recipient does not support gifting. Verification failed.", true) + throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts + } else { + Log.d(TAG, "Badge recipient supports gifting. Verification successful.", true) + } + } catch (e: IOException) { + Log.w(TAG, "Failed to retrieve profile for recipient.", e, true) + throw DonationError.GiftRecipientVerificationError.FailedToFetchProfile(e) + } + }.subscribeOn(Schedulers.io()) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt index 5c029c8c4d..eb10a3928e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt @@ -10,11 +10,13 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent +import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle import org.thoughtcrime.securesms.components.settings.models.SplashImage +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -30,7 +32,13 @@ class GiftFlowStartFragment : DSLSettingsFragment( private val viewModel: GiftFlowViewModel by viewModels( ownerProducer = { requireActivity() }, - factoryProducer = { GiftFlowViewModel.Factory(GiftFlowRepository(), requireListener().donationPaymentRepository) } + factoryProducer = { + GiftFlowViewModel.Factory( + GiftFlowRepository(), + requireListener().stripeRepository, + OneTimeDonationRepository(ApplicationDependencies.getDonationsService()) + ) + } ) private val lifecycleDisposable = LifecycleDisposable() 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 c40440f6fe..aeaa11a4ad 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 @@ -23,7 +23,8 @@ import org.signal.donations.StripeIntentAccessor import org.thoughtcrime.securesms.badges.gifts.Gifts import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey @@ -38,8 +39,9 @@ import java.util.Currency * Maintains state as a user works their way through the gift flow. */ class GiftFlowViewModel( - val repository: GiftFlowRepository, - val donationPaymentRepository: DonationPaymentRepository + private val giftFlowRepository: GiftFlowRepository, + private val stripeRepository: StripeRepository, + private val oneTimeDonationRepository: OneTimeDonationRepository ) : ViewModel() { private var giftToPurchase: Gift? = null @@ -87,7 +89,7 @@ class GiftFlowViewModel( } } - disposables += repository.getGiftPricing().subscribe { giftPrices -> + disposables += giftFlowRepository.getGiftPricing().subscribe { giftPrices -> store.update { it.copy( giftPrices = giftPrices, @@ -96,7 +98,7 @@ class GiftFlowViewModel( } } - disposables += repository.getGiftBadge().subscribeBy( + disposables += giftFlowRepository.getGiftBadge().subscribeBy( onSuccess = { (giftLevel, giftBadge) -> store.update { it.copy( @@ -139,12 +141,12 @@ class GiftFlowViewModel( this.giftToPurchase = Gift(giftLevel, giftPrice) store.update { it.copy(stage = GiftFlowState.Stage.RECIPIENT_VERIFICATION) } - disposables += donationPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(giftRecipient) + disposables += giftFlowRepository.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) + stripeRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE) }, onError = this::onPaymentFlowError ) @@ -160,7 +162,7 @@ class GiftFlowViewModel( val recipient = store.state.recipient?.id - donationPaymentRepository.onActivityResult( + stripeRepository.onActivityResult( requestCode, resultCode, data, Gifts.GOOGLE_PAY_REQUEST_CODE, object : GooglePayApi.PaymentRequestCallback { override fun onSuccess(paymentData: PaymentData) { @@ -169,13 +171,13 @@ class GiftFlowViewModel( store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) } - val continuePayment: Single = donationPaymentRepository.continuePayment(gift.price, recipient, gift.level) + val continuePayment: Single = stripeRepository.continuePayment(gift.price, recipient, gift.level) val intentAndSource: Single> = Single.zip(continuePayment, Single.just(GooglePayPaymentSource(paymentData)), ::Pair) disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) -> - donationPaymentRepository.confirmPayment(paymentSource, paymentIntent, recipient) + stripeRepository.confirmPayment(paymentSource, paymentIntent, recipient) .flatMapCompletable { Completable.complete() } // We do not currently handle 3DS for gifts. - .andThen(donationPaymentRepository.waitForOneTimeRedemption(gift.price, paymentIntent, recipient, store.state.additionalMessage?.toString(), gift.level)) + .andThen(oneTimeDonationRepository.waitForOneTimeRedemption(gift.price, paymentIntent.intentId, recipient, store.state.additionalMessage?.toString(), gift.level)) }.subscribeBy( onError = this@GiftFlowViewModel::onPaymentFlowError, onComplete = { @@ -249,13 +251,15 @@ class GiftFlowViewModel( class Factory( private val repository: GiftFlowRepository, - private val donationPaymentRepository: DonationPaymentRepository + private val stripeRepository: StripeRepository, + private val oneTimeDonationRepository: OneTimeDonationRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast( GiftFlowViewModel( repository, - donationPaymentRepository + stripeRepository, + oneTimeDonationRepository ) ) as T } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerFragment.kt index 9c0dca14f0..377d680adc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerFragment.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.util.BottomSheetUtil @@ -21,7 +21,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() { private val viewModel: BecomeASustainerViewModel by viewModels( factoryProducer = { - BecomeASustainerViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService())) + BecomeASustainerViewModel.Factory(MonthlyDonationRepository(ApplicationDependencies.getDonationsService())) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerViewModel.kt index 2c1a7ad2b0..34b6f8252f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/none/BecomeASustainerViewModel.kt @@ -7,10 +7,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.util.livedata.Store -class BecomeASustainerViewModel(subscriptionsRepository: SubscriptionsRepository) : ViewModel() { +class BecomeASustainerViewModel(subscriptionsRepository: MonthlyDonationRepository) : ViewModel() { private val store = Store(BecomeASustainerState()) @@ -37,7 +37,7 @@ class BecomeASustainerViewModel(subscriptionsRepository: SubscriptionsRepository private val TAG = Log.tag(BecomeASustainerViewModel::class.java) } - class Factory(private val subscriptionsRepository: SubscriptionsRepository) : ViewModelProvider.Factory { + class Factory(private val subscriptionsRepository: MonthlyDonationRepository) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast(BecomeASustainerViewModel(subscriptionsRepository))!! } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt index fb2f0dd8f6..34a34e88a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient @@ -31,7 +31,7 @@ class BadgesOverviewFragment : DSLSettingsFragment( private val lifecycleDisposable = LifecycleDisposable() private val viewModel: BadgesOverviewViewModel by viewModels( factoryProducer = { - BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), SubscriptionsRepository(ApplicationDependencies.getDonationsService())) + BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), MonthlyDonationRepository(ApplicationDependencies.getDonationsService())) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt index e92c747072..08794193f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt @@ -12,7 +12,7 @@ import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.subjects.PublishSubject import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.badges.BadgeRepository -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.InternetConnectionObserver @@ -23,7 +23,7 @@ private val TAG = Log.tag(BadgesOverviewViewModel::class.java) class BadgesOverviewViewModel( private val badgeRepository: BadgeRepository, - private val subscriptionsRepository: SubscriptionsRepository + private val subscriptionsRepository: MonthlyDonationRepository ) : ViewModel() { private val store = Store(BadgesOverviewState()) private val eventSubject = PublishSubject.create() @@ -89,7 +89,7 @@ class BadgesOverviewViewModel( class Factory( private val badgeRepository: BadgeRepository, - private val subscriptionsRepository: SubscriptionsRepository + private val subscriptionsRepository: MonthlyDonationRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository))) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index 1f846abfed..0701362b1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SettingsValues @@ -34,7 +34,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { private var wasConfigurationUpdated = false - override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) } + override val stripeRepository: StripeRepository by lazy { StripeRepository(this) } override val googlePayResultPublisher: Subject = PublishSubject.create() override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt index b425161041..a251cf79f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt @@ -6,7 +6,7 @@ import io.reactivex.rxjava3.subjects.Subject import kotlinx.parcelize.Parcelize interface DonationPaymentComponent { - val donationPaymentRepository: DonationPaymentRepository + val stripeRepository: StripeRepository val googlePayResultPublisher: Subject @Parcelize 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 deleted file mode 100644 index b7658d92cd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ /dev/null @@ -1,480 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription - -import android.app.Activity -import android.content.Intent -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.core.util.concurrent.SignalExecutors -import org.signal.core.util.logging.Log -import org.signal.core.util.money.FiatMoney -import org.signal.donations.GooglePayApi -import org.signal.donations.StripeApi -import org.signal.donations.StripeIntentAccessor -import org.signal.donations.json.StripeIntentStatus -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.RecipientDatabase -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.DonationReceiptRecord -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.jobmanager.JobTracker -import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob -import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.storage.StorageSyncHelper -import org.thoughtcrime.securesms.subscription.LevelUpdate -import org.thoughtcrime.securesms.subscription.LevelUpdateOperation -import org.thoughtcrime.securesms.subscription.Subscriber -import org.thoughtcrime.securesms.util.Environment -import org.thoughtcrime.securesms.util.ProfileUtil -import org.whispersystems.signalservice.api.profiles.SignalServiceProfile -import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey -import org.whispersystems.signalservice.api.subscriptions.SubscriberId -import org.whispersystems.signalservice.api.subscriptions.SubscriptionClientSecret -import org.whispersystems.signalservice.internal.EmptyResponse -import org.whispersystems.signalservice.internal.ServiceResponse -import java.io.IOException -import java.util.Locale -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -/** - * Manages bindings with payment APIs - * - * Steps for setting up payments for a subscription: - * 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method. - * 1. Generate and send a SubscriberId, which is a 32 byte ID representing this user, to Signal Service, which creates a Stripe Customer - * 1. Create a SetupIntent via the Stripe API - * 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay - * 1. Confirm the SetupIntent via the Stripe API - * 1. Set the default PaymentMethod for the customer, using the PaymentMethod id, via the Signal service - * - * For Boosts and Gifts: - * 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method. - * 1. Create a PaymentIntent via the Stripe API - * 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay - * 1. Confirm the PaymentIntent via the Stripe API - */ -class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper { - - 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()) - - fun isGooglePayAvailable(): Completable { - return googlePayApi.queryIsReadyToPay() - } - - fun scheduleSyncForAccountRecordChange() { - SignalExecutors.BOUNDED.execute { - scheduleSyncForAccountRecordChangeSync() - } - } - - private fun scheduleSyncForAccountRecordChangeSync() { - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - } - - fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) { - Log.d(TAG, "Requesting a token from google pay...") - googlePayApi.requestPayment(price, label, requestCode) - } - - fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?, - expectedRequestCode: Int, - paymentsRequestCallback: GooglePayApi.PaymentRequestCallback - ) { - Log.d(TAG, "Processing possible google pay result...") - googlePayApi.onActivityResult(requestCode, resultCode, data, expectedRequestCode, paymentsRequestCallback) - } - - /** - * Verifies that the given recipient is a supported target for a gift. - */ - 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, "Cannot send a gift to self.", true) - throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts - } - - if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientDatabase.RegisteredState.REGISTERED) { - Log.w(TAG, "Invalid badge recipient $badgeRecipient. Verification failed.", true) - throw DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid - } - - try { - val profile = ProfileUtil.retrieveProfileSync(ApplicationDependencies.getApplication(), recipient, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) - if (!profile.profile.capabilities.isGiftBadges) { - Log.w(TAG, "Badge recipient does not support gifting. Verification failed.", true) - throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts - } else { - Log.d(TAG, "Badge recipient supports gifting. Verification successful.", true) - } - } catch (e: IOException) { - Log.w(TAG, "Failed to retrieve profile for recipient.", e, true) - throw DonationError.GiftRecipientVerificationError.FailedToFetchProfile(e) - } - }.subscribeOn(Schedulers.io()) - } - - /** - * @param price The amount to charce the local user - * @param badgeRecipient Who will be getting the badge - */ - fun continuePayment( - price: FiatMoney, - badgeRecipient: RecipientId, - badgeLevel: Long, - ): Single { - Log.d(TAG, "Creating payment intent for $price...", true) - - return stripeApi.createPaymentIntent(price, badgeLevel) - .onErrorResumeNext { - if (it is DonationError) { - Single.error(it) - } else { - val recipient = Recipient.resolved(badgeRecipient) - val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT - Single.error(DonationError.getPaymentSetupError(errorSource, it)) - } - } - .flatMap { result -> - val recipient = Recipient.resolved(badgeRecipient) - val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT - - Log.d(TAG, "Created payment intent for $price.", true) - when (result) { - is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Single.error(DonationError.oneTimeDonationAmountTooSmall(errorSource)) - is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Single.error(DonationError.oneTimeDonationAmountTooLarge(errorSource)) - is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Single.error(DonationError.invalidCurrencyForOneTimeDonation(errorSource)) - is StripeApi.CreatePaymentIntentResult.Success -> Single.just(result.paymentIntent) - } - }.subscribeOn(Schedulers.io()) - } - - fun createAndConfirmSetupIntent(paymentSource: StripeApi.PaymentSource): Single { - Log.d(TAG, "Continuing subscription setup...", true) - return stripeApi.createSetupIntent() - .flatMap { result -> - Log.d(TAG, "Retrieved SetupIntent, confirming...", true) - stripeApi.confirmSetupIntent(paymentSource, result.setupIntent) - } - } - - fun cancelActiveSubscription(): Completable { - Log.d(TAG, "Canceling active subscription...", true) - val localSubscriber = SignalStore.donationsValues().requireSubscriber() - return Single - .fromCallable { - ApplicationDependencies.getDonationsService() - .cancelSubscription(localSubscriber.subscriberId) - } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult) - .ignoreElement() - .doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) } - } - - fun ensureSubscriberId(): Completable { - Log.d(TAG, "Ensuring SubscriberId exists on Signal service...", true) - val subscriberId = SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate() - return Single - .fromCallable { - ApplicationDependencies - .getDonationsService() - .putSubscription(subscriberId) - } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult).ignoreElement() - .doOnComplete { - Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true) - - SignalStore - .donationsValues() - .setSubscriber(Subscriber(subscriberId, SignalStore.donationsValues().getSubscriptionCurrency().currencyCode)) - - scheduleSyncForAccountRecordChangeSync() - } - } - - fun confirmPayment( - paymentSource: StripeApi.PaymentSource, - paymentIntent: StripeIntentAccessor, - badgeRecipient: RecipientId - ): Single { - val isBoost = badgeRecipient == Recipient.self().id - val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT - - Log.d(TAG, "Confirming payment intent...", true) - return stripeApi.confirmPaymentIntent(paymentSource, paymentIntent) - .onErrorResumeNext { - Single.error(DonationError.getPaymentSetupError(donationErrorSource, it)) - } - } - - fun waitForOneTimeRedemption( - price: FiatMoney, - paymentIntent: StripeIntentAccessor, - badgeRecipient: RecipientId, - additionalMessage: String?, - badgeLevel: Long, - ): Completable { - val isBoost = badgeRecipient == Recipient.self().id - val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT - - val waitOnRedemption = Completable.create { - val donationReceiptRecord = if (isBoost) { - DonationReceiptRecord.createForBoost(price) - } else { - DonationReceiptRecord.createForGift(price) - } - - val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() } - - Log.d(TAG, "Confirmed payment intent. Recording $donationTypeLabel receipt and submitting badge reimbursement job chain.", true) - SignalDatabase.donationReceipts.addReceipt(donationReceiptRecord) - - val countDownLatch = CountDownLatch(1) - var finalJobState: JobTracker.JobState? = null - val chain = if (isBoost) { - BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntent) - } else { - BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntent, badgeRecipient, additionalMessage, badgeLevel) - } - - chain.enqueue { _, jobState -> - if (jobState.isComplete) { - finalJobState = jobState - countDownLatch.countDown() - } - } - - try { - if (countDownLatch.await(10, TimeUnit.SECONDS)) { - when (finalJobState) { - JobTracker.JobState.SUCCESS -> { - Log.d(TAG, "$donationTypeLabel request response job chain succeeded.", true) - it.onComplete() - } - JobTracker.JobState.FAILURE -> { - Log.d(TAG, "$donationTypeLabel request response job chain failed permanently.", true) - it.onError(DonationError.genericBadgeRedemptionFailure(donationErrorSource)) - } - else -> { - Log.d(TAG, "$donationTypeLabel request response job chain ignored due to in-progress jobs.", true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) - } - } - } else { - Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) - } - } catch (e: InterruptedException) { - Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) - } - } - - return waitOnRedemption - } - - fun setSubscriptionLevel(subscriptionLevel: String): Completable { - return getOrCreateLevelUpdateOperation(subscriptionLevel) - .flatMapCompletable { levelUpdateOperation -> - val subscriber = SignalStore.donationsValues().requireSubscriber() - - Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true) - Single - .fromCallable { - ApplicationDependencies.getDonationsService().updateSubscriptionLevel( - subscriber.subscriberId, - subscriptionLevel, - subscriber.currencyCode, - levelUpdateOperation.idempotencyKey.serialize(), - SubscriptionReceiptRequestResponseJob.MUTEX - ) - } - .flatMapCompletable { - if (it.status == 200 || it.status == 204) { - Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true) - SignalStore.donationsValues().updateLocalStateForLocalSubscribe() - scheduleSyncForAccountRecordChange() - LevelUpdate.updateProcessingState(false) - Completable.complete() - } else { - if (it.applicationError.isPresent) { - Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel with response code ${it.status}", it.applicationError.get(), true) - SignalStore.donationsValues().clearLevelOperations() - } else { - Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel", it.executionError.orElse(null), true) - } - - LevelUpdate.updateProcessingState(false) - it.flattenResult().ignoreElement() - } - }.andThen { - Log.d(TAG, "Enqueuing request response job chain.", true) - val countDownLatch = CountDownLatch(1) - var finalJobState: JobTracker.JobState? = null - - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue { _, jobState -> - if (jobState.isComplete) { - finalJobState = jobState - countDownLatch.countDown() - } - } - - try { - if (countDownLatch.await(10, TimeUnit.SECONDS)) { - when (finalJobState) { - JobTracker.JobState.SUCCESS -> { - Log.d(TAG, "Subscription request response job chain succeeded.", true) - it.onComplete() - } - JobTracker.JobState.FAILURE -> { - Log.d(TAG, "Subscription request response job chain failed permanently.", true) - it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)) - } - else -> { - Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) - } - } - } else { - Log.d(TAG, "Subscription request response job timed out.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) - } - } catch (e: InterruptedException) { - Log.w(TAG, "Subscription request response interrupted.", e, true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) - } - } - }.doOnError { - LevelUpdate.updateProcessingState(false) - }.subscribeOn(Schedulers.io()) - } - - private fun getOrCreateLevelUpdateOperation(subscriptionLevel: String): Single = Single.fromCallable { - Log.d(TAG, "Retrieving level update operation for $subscriptionLevel") - val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel) - if (levelUpdateOperation == null) { - val newOperation = LevelUpdateOperation( - idempotencyKey = IdempotencyKey.generate(), - level = subscriptionLevel - ) - - SignalStore.donationsValues().setLevelOperation(newOperation) - LevelUpdate.updateProcessingState(true) - Log.d(TAG, "Created a new operation for $subscriptionLevel") - newOperation - } else { - LevelUpdate.updateProcessingState(true) - Log.d(TAG, "Reusing operation for $subscriptionLevel") - levelUpdateOperation - } - } - - override fun fetchPaymentIntent(price: FiatMoney, level: Long): Single { - Log.d(TAG, "Fetching payment intent from Signal service for $price... (Locale.US minimum precision: ${price.minimumUnitPrecisionString})") - return Single - .fromCallable { - ApplicationDependencies - .getDonationsService() - .createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, level) - } - .flatMap(ServiceResponse::flattenResult) - .map { - StripeIntentAccessor( - objectType = StripeIntentAccessor.ObjectType.PAYMENT_INTENT, - intentId = it.id, - intentClientSecret = it.clientSecret - ) - }.doOnSuccess { - Log.d(TAG, "Got payment intent from Signal service!") - } - } - - override fun fetchSetupIntent(): Single { - Log.d(TAG, "Fetching setup intent from Signal service...") - return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() } - .flatMap { - Single.fromCallable { - ApplicationDependencies - .getDonationsService() - .createSubscriptionPaymentMethod(it.subscriberId) - } - } - .flatMap(ServiceResponse::flattenResult) - .map { - StripeIntentAccessor( - objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT, - intentId = it.id, - intentClientSecret = it.clientSecret - ) - } - .doOnSuccess { - Log.d(TAG, "Got setup intent from Signal service!") - } - } - - // We need to get the status and payment id from the intent. - - fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor): Single { - return Single.fromCallable { - when (stripeIntentAccessor.objectType) { - StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(StripeIntentStatus.SUCCEEDED, null) - StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> stripeApi.getPaymentIntent(stripeIntentAccessor).let { - StatusAndPaymentMethodId(it.status, it.paymentMethod) - } - StripeIntentAccessor.ObjectType.SETUP_INTENT -> stripeApi.getSetupIntent(stripeIntentAccessor).let { - StatusAndPaymentMethodId(it.status, it.paymentMethod) - } - } - } - } - - fun setDefaultPaymentMethod(paymentMethodId: String): Completable { - return Single.fromCallable { - Log.d(TAG, "Getting the subscriber...") - SignalStore.donationsValues().requireSubscriber() - }.flatMap { - Log.d(TAG, "Setting default payment method via Signal service...") - Single.fromCallable { - ApplicationDependencies - .getDonationsService() - .setDefaultPaymentMethodId(it.subscriberId, paymentMethodId) - } - }.flatMap(ServiceResponse::flattenResult).ignoreElement().doOnComplete { - Log.d(TAG, "Set default payment method via Signal service!") - } - } - - fun createCreditCardPaymentSource(donationErrorSource: DonationErrorSource, cardData: StripeApi.CardData): Single { - Log.d(TAG, "Creating credit card payment source via Stripe api...") - return stripeApi.createPaymentSourceFromCardData(cardData).map { - when (it) { - is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason) - is StripeApi.CreatePaymentSourceFromCardDataResult.Success -> it.paymentSource - } - } - } - - data class StatusAndPaymentMethodId( - val status: StripeIntentStatus, - val paymentMethod: String? - ) - - companion object { - private val TAG = Log.tag(DonationPaymentRepository::class.java) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt new file mode 100644 index 0000000000..1c8479544c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt @@ -0,0 +1,229 @@ +package org.thoughtcrime.securesms.components.settings.app.subscription + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.logging.Log +import org.signal.core.util.money.FiatMoney +import org.thoughtcrime.securesms.badges.Badges +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 +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.JobTracker +import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob +import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.subscription.LevelUpdate +import org.thoughtcrime.securesms.subscription.LevelUpdateOperation +import org.thoughtcrime.securesms.subscription.Subscriber +import org.thoughtcrime.securesms.subscription.Subscription +import org.thoughtcrime.securesms.util.PlatformCurrencyUtil +import org.whispersystems.signalservice.api.services.DonationsService +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription +import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey +import org.whispersystems.signalservice.api.subscriptions.SubscriberId +import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels +import org.whispersystems.signalservice.internal.EmptyResponse +import org.whispersystems.signalservice.internal.ServiceResponse +import java.util.Currency +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * Repository which can query for the user's active subscription as well as a list of available subscriptions, + * in the currency indicated. + */ +class MonthlyDonationRepository(private val donationsService: DonationsService) { + + private val TAG = Log.tag(MonthlyDonationRepository::class.java) + + fun getActiveSubscription(): Single { + val localSubscription = SignalStore.donationsValues().getSubscriber() + return if (localSubscription != null) { + Single.fromCallable { donationsService.getSubscription(localSubscription.subscriberId) } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse::flattenResult) + } else { + Single.just(ActiveSubscription.EMPTY) + } + } + + fun getSubscriptions(): Single> = Single + .fromCallable { donationsService.getSubscriptionLevels(Locale.getDefault()) } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse::flattenResult) + .map { subscriptionLevels -> + subscriptionLevels.levels.map { (code, level) -> + Subscription( + id = code, + name = level.name, + badge = Badges.fromServiceBadge(level.badge), + prices = level.currencies.filter { + PlatformCurrencyUtil + .getAvailableCurrencyCodes() + .contains(it.key) + }.map { (currencyCode, price) -> + FiatMoney(price, Currency.getInstance(currencyCode)) + }.toSet(), + level = code.toInt() + ) + }.sortedBy { + it.level + } + } + + fun syncAccountRecord(): Completable { + return Completable.fromAction { + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + }.subscribeOn(Schedulers.io()) + } + + fun ensureSubscriberId(): Completable { + Log.d(TAG, "Ensuring SubscriberId exists on Signal service...", true) + val subscriberId = SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate() + return Single + .fromCallable { + donationsService.putSubscription(subscriberId) + } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse::flattenResult).ignoreElement() + .doOnComplete { + Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true) + + SignalStore + .donationsValues() + .setSubscriber(Subscriber(subscriberId, SignalStore.donationsValues().getSubscriptionCurrency().currencyCode)) + + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + + fun cancelActiveSubscription(): Completable { + Log.d(TAG, "Canceling active subscription...", true) + val localSubscriber = SignalStore.donationsValues().requireSubscriber() + return Single + .fromCallable { + donationsService.cancelSubscription(localSubscriber.subscriberId) + } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse::flattenResult) + .ignoreElement() + .doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) } + } + + fun cancelActiveSubscriptionIfNecessary(): Completable { + return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable { + if (it) { + Log.d(TAG, "Cancelling active subscription...", true) + cancelActiveSubscription().doOnComplete { + SignalStore.donationsValues().updateLocalStateForManualCancellation() + MultiDeviceSubscriptionSyncRequestJob.enqueue() + } + } else { + Completable.complete() + } + } + } + + fun setSubscriptionLevel(subscriptionLevel: String): Completable { + return getOrCreateLevelUpdateOperation(subscriptionLevel) + .flatMapCompletable { levelUpdateOperation -> + val subscriber = SignalStore.donationsValues().requireSubscriber() + + Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true) + Single + .fromCallable { + ApplicationDependencies.getDonationsService().updateSubscriptionLevel( + subscriber.subscriberId, + subscriptionLevel, + subscriber.currencyCode, + levelUpdateOperation.idempotencyKey.serialize(), + SubscriptionReceiptRequestResponseJob.MUTEX + ) + } + .flatMapCompletable { + if (it.status == 200 || it.status == 204) { + Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true) + SignalStore.donationsValues().updateLocalStateForLocalSubscribe() + syncAccountRecord().subscribe() + LevelUpdate.updateProcessingState(false) + Completable.complete() + } else { + if (it.applicationError.isPresent) { + Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel with response code ${it.status}", it.applicationError.get(), true) + SignalStore.donationsValues().clearLevelOperations() + } else { + Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel", it.executionError.orElse(null), true) + } + + LevelUpdate.updateProcessingState(false) + it.flattenResult().ignoreElement() + } + }.andThen { + Log.d(TAG, "Enqueuing request response job chain.", true) + val countDownLatch = CountDownLatch(1) + var finalJobState: JobTracker.JobState? = null + + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue { _, jobState -> + if (jobState.isComplete) { + finalJobState = jobState + countDownLatch.countDown() + } + } + + try { + if (countDownLatch.await(10, TimeUnit.SECONDS)) { + when (finalJobState) { + JobTracker.JobState.SUCCESS -> { + Log.d(TAG, "Subscription request response job chain succeeded.", true) + it.onComplete() + } + JobTracker.JobState.FAILURE -> { + Log.d(TAG, "Subscription request response job chain failed permanently.", true) + it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)) + } + else -> { + Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) + } + } + } else { + Log.d(TAG, "Subscription request response job timed out.", true) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) + } + } catch (e: InterruptedException) { + Log.w(TAG, "Subscription request response interrupted.", e, true) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) + } + } + }.doOnError { + LevelUpdate.updateProcessingState(false) + }.subscribeOn(Schedulers.io()) + } + + private fun getOrCreateLevelUpdateOperation(subscriptionLevel: String): Single = Single.fromCallable { + Log.d(TAG, "Retrieving level update operation for $subscriptionLevel") + val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel) + if (levelUpdateOperation == null) { + val newOperation = LevelUpdateOperation( + idempotencyKey = IdempotencyKey.generate(), + level = subscriptionLevel + ) + + SignalStore.donationsValues().setLevelOperation(newOperation) + LevelUpdate.updateProcessingState(true) + Log.d(TAG, "Created a new operation for $subscriptionLevel") + newOperation + } else { + LevelUpdate.updateProcessingState(true) + Log.d(TAG, "Reusing operation for $subscriptionLevel") + levelUpdateOperation + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt new file mode 100644 index 0000000000..e68b3c687f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt @@ -0,0 +1,124 @@ +package org.thoughtcrime.securesms.components.settings.app.subscription + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.logging.Log +import org.signal.core.util.money.FiatMoney +import org.thoughtcrime.securesms.badges.Badges +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.errors.DonationError +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.DonationReceiptRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.JobTracker +import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.PlatformCurrencyUtil +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile +import org.whispersystems.signalservice.api.services.DonationsService +import org.whispersystems.signalservice.internal.ServiceResponse +import java.math.BigDecimal +import java.util.Currency +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class OneTimeDonationRepository(private val donationsService: DonationsService) { + + companion object { + private val TAG = Log.tag(OneTimeDonationRepository::class.java) + } + + fun getBoosts(): Single>> { + return Single.fromCallable { donationsService.boostAmounts } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse>>::flattenResult) + .map { result -> + result + .filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) } + .mapKeys { (code, _) -> Currency.getInstance(code) } + .mapValues { (currency, prices) -> prices.map { Boost(FiatMoney(it, currency)) } } + } + } + + fun getBoostBadge(): Single { + return Single + .fromCallable { + ApplicationDependencies.getDonationsService() + .getBoostBadge(Locale.getDefault()) + } + .subscribeOn(Schedulers.io()) + .flatMap(ServiceResponse::flattenResult) + .map(Badges::fromServiceBadge) + } + + fun waitForOneTimeRedemption( + price: FiatMoney, + paymentIntentId: String, + badgeRecipient: RecipientId, + additionalMessage: String?, + badgeLevel: Long, + ): Completable { + val isBoost = badgeRecipient == Recipient.self().id + val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT + + val waitOnRedemption = Completable.create { + val donationReceiptRecord = if (isBoost) { + DonationReceiptRecord.createForBoost(price) + } else { + DonationReceiptRecord.createForGift(price) + } + + val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() } + + Log.d(TAG, "Confirmed payment intent. Recording $donationTypeLabel receipt and submitting badge reimbursement job chain.", true) + SignalDatabase.donationReceipts.addReceipt(donationReceiptRecord) + + val countDownLatch = CountDownLatch(1) + var finalJobState: JobTracker.JobState? = null + val chain = if (isBoost) { + BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId) + } else { + BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel) + } + + chain.enqueue { _, jobState -> + if (jobState.isComplete) { + finalJobState = jobState + countDownLatch.countDown() + } + } + + try { + if (countDownLatch.await(10, TimeUnit.SECONDS)) { + when (finalJobState) { + JobTracker.JobState.SUCCESS -> { + Log.d(TAG, "$donationTypeLabel request response job chain succeeded.", true) + it.onComplete() + } + JobTracker.JobState.FAILURE -> { + Log.d(TAG, "$donationTypeLabel request response job chain failed permanently.", true) + it.onError(DonationError.genericBadgeRedemptionFailure(donationErrorSource)) + } + else -> { + Log.d(TAG, "$donationTypeLabel request response job chain ignored due to in-progress jobs.", true) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) + } + } + } else { + Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) + } + } catch (e: InterruptedException) { + Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) + } + } + + return waitOnRedemption + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt new file mode 100644 index 0000000000..4d15c91549 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt @@ -0,0 +1,237 @@ +package org.thoughtcrime.securesms.components.settings.app.subscription + +import android.app.Activity +import android.content.Intent +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.signal.core.util.money.FiatMoney +import org.signal.donations.GooglePayApi +import org.signal.donations.StripeApi +import org.signal.donations.StripeIntentAccessor +import org.signal.donations.json.StripeIntentStatus +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 +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.Environment +import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret +import org.whispersystems.signalservice.internal.EmptyResponse +import org.whispersystems.signalservice.internal.ServiceResponse + +/** + * Manages bindings with payment APIs + * + * Steps for setting up payments for a subscription: + * 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method. + * 1. Generate and send a SubscriberId, which is a 32 byte ID representing this user, to Signal Service, which creates a Stripe Customer + * 1. Create a SetupIntent via the Stripe API + * 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay + * 1. Confirm the SetupIntent via the Stripe API + * 1. Set the default PaymentMethod for the customer, using the PaymentMethod id, via the Signal service + * + * For Boosts and Gifts: + * 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method. + * 1. Create a PaymentIntent via the Stripe API + * 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay + * 1. Confirm the PaymentIntent via the Stripe API + */ +class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper { + + 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()) + + fun isGooglePayAvailable(): Completable { + return googlePayApi.queryIsReadyToPay() + } + + fun scheduleSyncForAccountRecordChange() { + SignalExecutors.BOUNDED.execute { + scheduleSyncForAccountRecordChangeSync() + } + } + + private fun scheduleSyncForAccountRecordChangeSync() { + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + } + + fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) { + Log.d(TAG, "Requesting a token from google pay...") + googlePayApi.requestPayment(price, label, requestCode) + } + + fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent?, + expectedRequestCode: Int, + paymentsRequestCallback: GooglePayApi.PaymentRequestCallback + ) { + Log.d(TAG, "Processing possible google pay result...") + googlePayApi.onActivityResult(requestCode, resultCode, data, expectedRequestCode, paymentsRequestCallback) + } + + /** + * @param price The amount to charce the local user + * @param badgeRecipient Who will be getting the badge + */ + fun continuePayment( + price: FiatMoney, + badgeRecipient: RecipientId, + badgeLevel: Long, + ): Single { + Log.d(TAG, "Creating payment intent for $price...", true) + + return stripeApi.createPaymentIntent(price, badgeLevel) + .onErrorResumeNext { + handleCreatePaymentIntentError(it, badgeRecipient) + } + .flatMap { result -> + val recipient = Recipient.resolved(badgeRecipient) + val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT + + Log.d(TAG, "Created payment intent for $price.", true) + when (result) { + is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Single.error(DonationError.oneTimeDonationAmountTooSmall(errorSource)) + is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Single.error(DonationError.oneTimeDonationAmountTooLarge(errorSource)) + is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Single.error(DonationError.invalidCurrencyForOneTimeDonation(errorSource)) + is StripeApi.CreatePaymentIntentResult.Success -> Single.just(result.paymentIntent) + } + }.subscribeOn(Schedulers.io()) + } + + fun createAndConfirmSetupIntent(paymentSource: StripeApi.PaymentSource): Single { + Log.d(TAG, "Continuing subscription setup...", true) + return stripeApi.createSetupIntent() + .flatMap { result -> + Log.d(TAG, "Retrieved SetupIntent, confirming...", true) + stripeApi.confirmSetupIntent(paymentSource, result.setupIntent) + } + } + + fun confirmPayment( + paymentSource: StripeApi.PaymentSource, + paymentIntent: StripeIntentAccessor, + badgeRecipient: RecipientId + ): Single { + val isBoost = badgeRecipient == Recipient.self().id + val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT + + Log.d(TAG, "Confirming payment intent...", true) + return stripeApi.confirmPaymentIntent(paymentSource, paymentIntent) + .onErrorResumeNext { + Single.error(DonationError.getPaymentSetupError(donationErrorSource, it)) + } + } + + override fun fetchPaymentIntent(price: FiatMoney, level: Long): Single { + Log.d(TAG, "Fetching payment intent from Signal service for $price... (Locale.US minimum precision: ${price.minimumUnitPrecisionString})") + return Single + .fromCallable { + ApplicationDependencies + .getDonationsService() + .createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, level) + } + .flatMap(ServiceResponse::flattenResult) + .map { + StripeIntentAccessor( + objectType = StripeIntentAccessor.ObjectType.PAYMENT_INTENT, + intentId = it.id, + intentClientSecret = it.clientSecret + ) + }.doOnSuccess { + Log.d(TAG, "Got payment intent from Signal service!") + } + } + + override fun fetchSetupIntent(): Single { + Log.d(TAG, "Fetching setup intent from Signal service...") + return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() } + .flatMap { + Single.fromCallable { + ApplicationDependencies + .getDonationsService() + .createStripeSubscriptionPaymentMethod(it.subscriberId) + } + } + .flatMap(ServiceResponse::flattenResult) + .map { + StripeIntentAccessor( + objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT, + intentId = it.id, + intentClientSecret = it.clientSecret + ) + } + .doOnSuccess { + Log.d(TAG, "Got setup intent from Signal service!") + } + } + + // We need to get the status and payment id from the intent. + + fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor): Single { + return Single.fromCallable { + when (stripeIntentAccessor.objectType) { + StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(StripeIntentStatus.SUCCEEDED, null) + StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> stripeApi.getPaymentIntent(stripeIntentAccessor).let { + StatusAndPaymentMethodId(it.status, it.paymentMethod) + } + StripeIntentAccessor.ObjectType.SETUP_INTENT -> stripeApi.getSetupIntent(stripeIntentAccessor).let { + StatusAndPaymentMethodId(it.status, it.paymentMethod) + } + } + } + } + + fun setDefaultPaymentMethod(paymentMethodId: String): Completable { + return Single.fromCallable { + Log.d(TAG, "Getting the subscriber...") + SignalStore.donationsValues().requireSubscriber() + }.flatMap { + Log.d(TAG, "Setting default payment method via Signal service...") + Single.fromCallable { + ApplicationDependencies + .getDonationsService() + .setDefaultStripePaymentMethod(it.subscriberId, paymentMethodId) + } + }.flatMap(ServiceResponse::flattenResult).ignoreElement().doOnComplete { + Log.d(TAG, "Set default payment method via Signal service!") + } + } + + fun createCreditCardPaymentSource(donationErrorSource: DonationErrorSource, cardData: StripeApi.CardData): Single { + Log.d(TAG, "Creating credit card payment source via Stripe api...") + return stripeApi.createPaymentSourceFromCardData(cardData).map { + when (it) { + is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason) + is StripeApi.CreatePaymentSourceFromCardDataResult.Success -> it.paymentSource + } + } + } + + data class StatusAndPaymentMethodId( + val status: StripeIntentStatus, + val paymentMethod: String? + ) + + companion object { + private val TAG = Log.tag(StripeRepository::class.java) + + fun handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId): Single { + return if (throwable is DonationError) { + Single.error(throwable) + } else { + val recipient = Recipient.resolved(badgeRecipient) + val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT + Single.error(DonationError.getPaymentSetupError(errorSource, throwable)) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/SubscriptionsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/SubscriptionsRepository.kt deleted file mode 100644 index ab3bff75e6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/SubscriptionsRepository.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription - -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.core.util.money.FiatMoney -import org.thoughtcrime.securesms.badges.Badges -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.storage.StorageSyncHelper -import org.thoughtcrime.securesms.subscription.Subscription -import org.thoughtcrime.securesms.util.PlatformCurrencyUtil -import org.whispersystems.signalservice.api.services.DonationsService -import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription -import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels -import org.whispersystems.signalservice.internal.ServiceResponse -import java.util.Currency -import java.util.Locale - -/** - * Repository which can query for the user's active subscription as well as a list of available subscriptions, - * in the currency indicated. - */ -class SubscriptionsRepository(private val donationsService: DonationsService) { - - fun getActiveSubscription(): Single { - val localSubscription = SignalStore.donationsValues().getSubscriber() - return if (localSubscription != null) { - Single.fromCallable { donationsService.getSubscription(localSubscription.subscriberId) } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult) - } else { - Single.just(ActiveSubscription.EMPTY) - } - } - - fun getSubscriptions(): Single> = Single - .fromCallable { donationsService.getSubscriptionLevels(Locale.getDefault()) } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult) - .map { subscriptionLevels -> - subscriptionLevels.levels.map { (code, level) -> - Subscription( - id = code, - name = level.name, - badge = Badges.fromServiceBadge(level.badge), - prices = level.currencies.filter { - PlatformCurrencyUtil - .getAvailableCurrencyCodes() - .contains(it.key) - }.map { (currencyCode, price) -> - FiatMoney(price, Currency.getInstance(currencyCode)) - }.toSet(), - level = code.toInt() - ) - }.sortedBy { - it.level - } - } - - fun syncAccountRecord(): Completable { - return Completable.fromAction { - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - }.subscribeOn(Schedulers.io()) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostRepository.kt deleted file mode 100644 index c7e2bfa447..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostRepository.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.boost - -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.core.util.money.FiatMoney -import org.thoughtcrime.securesms.badges.Badges -import org.thoughtcrime.securesms.badges.models.Badge -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.util.PlatformCurrencyUtil -import org.whispersystems.signalservice.api.profiles.SignalServiceProfile -import org.whispersystems.signalservice.api.services.DonationsService -import org.whispersystems.signalservice.internal.ServiceResponse -import java.math.BigDecimal -import java.util.Currency -import java.util.Locale - -class BoostRepository(private val donationsService: DonationsService) { - - fun getBoosts(): Single>> { - return Single.fromCallable { donationsService.boostAmounts } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse>>::flattenResult) - .map { result -> - result - .filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) } - .mapKeys { (code, _) -> Currency.getInstance(code) } - .mapValues { (currency, prices) -> prices.map { Boost(FiatMoney(it, currency)) } } - } - } - - fun getBoostBadge(): Single { - return Single - .fromCallable { - ApplicationDependencies.getDonationsService() - .getBoostBadge(Locale.getDefault()) - } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult) - .map(Badges::fromServiceBadge) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt index 519f2f684d..75a0b0fece 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt @@ -40,7 +40,7 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() { summary = DSLSettingsText.from(currency.currencyCode), onClick = { viewModel.setSelectedCurrency(currency.currencyCode) - donationPaymentComponent.donationPaymentRepository.scheduleSyncForAccountRecordChange() + donationPaymentComponent.stripeRepository.scheduleSyncForAccountRecordChange() dismissAllowingStateLoss() } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt index 3c0699f47c..98a7a91854 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt @@ -6,7 +6,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.Subject import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository /** * Activity wrapper for donate to signal screen. An activity is needed because Google Pay uses the @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP */ class DonateToSignalActivity : FragmentWrapperActivity(), DonationPaymentComponent { - override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) } + override val stripeRepository: StripeRepository by lazy { StripeRepository(this) } override val googlePayResultPublisher: Subject = PublishSubject.create() override fun getFragment(): Fragment { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt index 7f730e65c6..43e5785dd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt @@ -41,8 +41,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.ca import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet -import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripeAction -import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripeActionResult import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError @@ -104,7 +102,7 @@ class DonateToSignalFragment : DSLSettingsFragment( R.id.donate_to_signal, factoryProducer = { donationPaymentComponent = requireListener() - StripePaymentInProgressViewModel.Factory(donationPaymentComponent.donationPaymentRepository) + StripePaymentInProgressViewModel.Factory(donationPaymentComponent.stripeRepository) } ) @@ -144,8 +142,8 @@ class DonateToSignalFragment : DSLSettingsFragment( } setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle -> - val result: StripeActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!! - handleStripeActionResult(result) + val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!! + handleDonationProcessorActionResult(result) } setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle -> @@ -208,7 +206,7 @@ class DonateToSignalFragment : DSLSettingsFragment( is DonateToSignalAction.CancelSubscription -> { findNavController().safeNavigate( DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment( - StripeAction.CANCEL_SUBSCRIPTION, + DonationProcessorAction.CANCEL_SUBSCRIPTION, action.gatewayRequest ) ) @@ -216,7 +214,7 @@ class DonateToSignalFragment : DSLSettingsFragment( is DonateToSignalAction.UpdateSubscription -> { findNavController().safeNavigate( DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment( - StripeAction.UPDATE_SUBSCRIPTION, + DonationProcessorAction.UPDATE_SUBSCRIPTION, action.gatewayRequest ) ) @@ -432,28 +430,28 @@ class DonateToSignalFragment : DSLSettingsFragment( private fun handleCreditCardResult(creditCardResult: CreditCardResult) { Log.d(TAG, "Received credit card information from fragment.") stripePaymentViewModel.provideCardData(creditCardResult.creditCardData) - findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(StripeAction.PROCESS_NEW_DONATION, creditCardResult.gatewayRequest)) + findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, creditCardResult.gatewayRequest)) } - private fun handleStripeActionResult(result: StripeActionResult) { + private fun handleDonationProcessorActionResult(result: DonationProcessorActionResult) { when (result.status) { - StripeActionResult.Status.SUCCESS -> handleSuccessfulStripeActionResult(result) - StripeActionResult.Status.FAILURE -> handleFailedStripeActionResult(result) + DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result) + DonationProcessorActionResult.Status.FAILURE -> handleFailedDonationProcessorActionResult(result) } viewModel.refreshActiveSubscription() } - private fun handleSuccessfulStripeActionResult(result: StripeActionResult) { - if (result.action == StripeAction.CANCEL_SUBSCRIPTION) { + private fun handleSuccessfulDonationProcessorActionResult(result: DonationProcessorActionResult) { + if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) { Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show() } else { findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(result.request.badge)) } } - private fun handleFailedStripeActionResult(result: StripeActionResult) { - if (result.action == StripeAction.CANCEL_SUBSCRIPTION) { + private fun handleFailedDonationProcessorActionResult(result: DonationProcessorActionResult) { + if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.DonationsErrors__failed_to_cancel_subscription) .setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection) @@ -468,7 +466,7 @@ class DonateToSignalFragment : DSLSettingsFragment( private fun launchGooglePay(gatewayResponse: GatewayResponse) { viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request) - donationPaymentComponent.donationPaymentRepository.requestTokenFromGooglePay( + donationPaymentComponent.stripeRepository.requestTokenFromGooglePay( price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)), label = gatewayResponse.request.label, requestCode = gatewayResponse.request.donateToSignalType.requestCode.toInt() @@ -487,7 +485,7 @@ class DonateToSignalFragment : DSLSettingsFragment( disposables += donationPaymentComponent.googlePayResultPublisher.subscribeBy( onNext = { paymentResult -> viewModel.consumeGatewayRequestForGooglePay()?.let { - donationPaymentComponent.donationPaymentRepository.onActivityResult( + donationPaymentComponent.stripeRepository.onActivityResult( paymentResult.requestCode, paymentResult.resultCode, paymentResult.data, @@ -549,7 +547,7 @@ class DonateToSignalFragment : DSLSettingsFragment( override fun onSuccess(paymentData: PaymentData) { Log.d(TAG, "Successfully retrieved payment data from Google Pay", true) stripePaymentViewModel.providePaymentData(paymentData) - findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(StripeAction.PROCESS_NEW_DONATION, request)) + findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, request)) } override fun onError(googlePayException: GooglePayApi.GooglePayException) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt index 297638f745..6621ec5262 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt @@ -12,9 +12,9 @@ import io.reactivex.rxjava3.subjects.PublishSubject import org.signal.core.util.StringUtil import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost -import org.thoughtcrime.securesms.components.settings.app.subscription.boost.BoostRepository import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest import org.thoughtcrime.securesms.components.settings.app.subscription.manage.SubscriptionRedemptionJobWatcher import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -42,8 +42,8 @@ import java.util.Currency */ class DonateToSignalViewModel( startType: DonateToSignalType, - private val subscriptionsRepository: SubscriptionsRepository, - private val boostRepository: BoostRepository + private val subscriptionsRepository: MonthlyDonationRepository, + private val oneTimeDonationRepository: OneTimeDonationRepository ) : ViewModel() { companion object { @@ -63,7 +63,7 @@ class DonateToSignalViewModel( val actions: Observable = _actions.observeOn(AndroidSchedulers.mainThread()) init { - initializeOneTimeDonationState(boostRepository) + initializeOneTimeDonationState(oneTimeDonationRepository) initializeMonthlyDonationState(subscriptionsRepository) networkDisposable += InternetConnectionObserver @@ -87,7 +87,7 @@ class DonateToSignalViewModel( fun retryOneTimeDonationState() { if (!oneTimeDonationDisposables.isDisposed && store.state.oneTimeDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) { store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) } - initializeOneTimeDonationState(boostRepository) + initializeOneTimeDonationState(oneTimeDonationRepository) } } @@ -197,8 +197,8 @@ class DonateToSignalViewModel( } } - private fun initializeOneTimeDonationState(boostRepository: BoostRepository) { - oneTimeDonationDisposables += boostRepository.getBoostBadge().subscribeBy( + private fun initializeOneTimeDonationState(oneTimeDonationRepository: OneTimeDonationRepository) { + oneTimeDonationDisposables += oneTimeDonationRepository.getBoostBadge().subscribeBy( onSuccess = { badge -> store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) } }, @@ -207,7 +207,7 @@ class DonateToSignalViewModel( } ) - val boosts: Observable>> = boostRepository.getBoosts().toObservable() + val boosts: Observable>> = oneTimeDonationRepository.getBoosts().toObservable() val oneTimeCurrency: Observable = SignalStore.donationsValues().observableOneTimeCurrency oneTimeDonationDisposables += Observable.combineLatest(boosts, oneTimeCurrency) { boostMap, currency -> @@ -243,7 +243,7 @@ class DonateToSignalViewModel( ) } - private fun initializeMonthlyDonationState(subscriptionsRepository: SubscriptionsRepository) { + private fun initializeMonthlyDonationState(subscriptionsRepository: MonthlyDonationRepository) { monitorLevelUpdateProcessing() val allSubscriptions = subscriptionsRepository.getSubscriptions() @@ -362,11 +362,11 @@ class DonateToSignalViewModel( class Factory( private val startType: DonateToSignalType, - private val subscriptionsRepository: SubscriptionsRepository = SubscriptionsRepository(ApplicationDependencies.getDonationsService()), - private val boostRepository: BoostRepository = BoostRepository(ApplicationDependencies.getDonationsService()) + private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()), + private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService()) ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, boostRepository)) as T + return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, oneTimeDonationRepository)) as T } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeAction.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorAction.kt similarity index 76% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeAction.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorAction.kt index 3cc65e673f..31ae5403bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeAction.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorAction.kt @@ -1,10 +1,10 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe +package org.thoughtcrime.securesms.components.settings.app.subscription.donate import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -enum class StripeAction : Parcelable { +enum class DonationProcessorAction : Parcelable { PROCESS_NEW_DONATION, UPDATE_SUBSCRIPTION, CANCEL_SUBSCRIPTION diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeActionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt similarity index 80% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeActionResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt index 1e91bc3e26..0e07693778 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeActionResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt @@ -1,12 +1,12 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe +package org.thoughtcrime.securesms.components.settings.app.subscription.donate import android.os.Parcelable import kotlinx.parcelize.Parcelize import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest @Parcelize -class StripeActionResult( - val action: StripeAction, +class DonationProcessorActionResult( + val action: DonationProcessorAction, val request: GatewayRequest, val status: Status ) : Parcelable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeStage.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorStage.kt similarity index 84% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeStage.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorStage.kt index 167748add0..c33dbeca59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripeStage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorStage.kt @@ -1,6 +1,6 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe +package org.thoughtcrime.securesms.components.settings.app.subscription.donate -enum class StripeStage { +enum class DonationProcessorStage { INIT, PAYMENT_PIPELINE, CANCELLING, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt index cda080c66f..f6f0391952 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt @@ -34,7 +34,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() { private val args: GatewaySelectorBottomSheetArgs by navArgs() private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = { - GatewaySelectorViewModel.Factory(args, requireListener().donationPaymentRepository) + GatewaySelectorViewModel.Factory(args, requireListener().stripeRepository) }) override fun bindAdapter(adapter: DSLSettingsAdapter) { @@ -64,7 +64,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() { DonateToSignalType.ONE_TIME -> presentOneTimeText() } - space(68.dp) + space(66.dp) if (state.isGooglePayAvailable) { customPref( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt index fcf8b37324..073fc46916 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.util.rx.RxStore class GatewaySelectorViewModel( args: GatewaySelectorBottomSheetArgs, - private val repository: DonationPaymentRepository + private val repository: StripeRepository ) : ViewModel() { private val store = RxStore(GatewaySelectorState(args.request.badge)) @@ -40,7 +40,7 @@ class GatewaySelectorViewModel( class Factory( private val args: GatewaySelectorBottomSheetArgs, - private val repository: DonationPaymentRepository + private val repository: StripeRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast(GatewaySelectorViewModel(args, repository)) as T diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/Stripe3DSDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt similarity index 99% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/Stripe3DSDialogFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt index 0448e754c4..5012d31da0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/Stripe3DSDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.donate +package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe import android.annotation.SuppressLint import android.content.DialogInterface diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt index c554822f7a..957180f3ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt @@ -22,7 +22,9 @@ import org.signal.donations.StripeIntentAccessor import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent -import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Stripe3DSDialogFragment +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage import org.thoughtcrime.securesms.databinding.StripePaymentInProgressFragmentBinding import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.fragments.requireListener @@ -43,7 +45,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_i private val viewModel: StripePaymentInProgressViewModel by navGraphViewModels( R.id.donate_to_signal, factoryProducer = { - StripePaymentInProgressViewModel.Factory(requireListener().donationPaymentRepository) + StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository) } ) @@ -58,13 +60,13 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_i if (savedInstanceState == null) { viewModel.onBeginNewAction() when (args.action) { - StripeAction.PROCESS_NEW_DONATION -> { + DonationProcessorAction.PROCESS_NEW_DONATION -> { viewModel.processNewDonation(args.request, this::handleSecure3dsAction) } - StripeAction.UPDATE_SUBSCRIPTION -> { + DonationProcessorAction.UPDATE_SUBSCRIPTION -> { viewModel.updateSubscription(args.request) } - StripeAction.CANCEL_SUBSCRIPTION -> { + DonationProcessorAction.CANCEL_SUBSCRIPTION -> { viewModel.cancelSubscription() } } @@ -76,39 +78,39 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_i } } - private fun presentUiState(stage: StripeStage) { + private fun presentUiState(stage: DonationProcessorStage) { when (stage) { - StripeStage.INIT -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment) - StripeStage.PAYMENT_PIPELINE -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment) - StripeStage.FAILED -> { + DonationProcessorStage.INIT -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment) + DonationProcessorStage.PAYMENT_PIPELINE -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment) + DonationProcessorStage.FAILED -> { viewModel.onEndAction() findNavController().popBackStack() setFragmentResult( REQUEST_KEY, bundleOf( - REQUEST_KEY to StripeActionResult( + REQUEST_KEY to DonationProcessorActionResult( action = args.action, request = args.request, - status = StripeActionResult.Status.FAILURE + status = DonationProcessorActionResult.Status.FAILURE ) ) ) } - StripeStage.COMPLETE -> { + DonationProcessorStage.COMPLETE -> { viewModel.onEndAction() findNavController().popBackStack() setFragmentResult( REQUEST_KEY, bundleOf( - REQUEST_KEY to StripeActionResult( + REQUEST_KEY to DonationProcessorActionResult( action = args.action, request = args.request, - status = StripeActionResult.Status.SUCCESS + status = DonationProcessorActionResult.Status.SUCCESS ) ) ) } - StripeStage.CANCELLING -> binding.progressCardStatus.setText(R.string.StripePaymentInProgressFragment__cancelling) + DonationProcessorStage.CANCELLING -> binding.progressCardStatus.setText(R.string.StripePaymentInProgressFragment__cancelling) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt index d6fe4aaed8..8053d10076 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt @@ -14,8 +14,11 @@ import org.signal.core.util.logging.Log import org.signal.donations.GooglePayPaymentSource import org.signal.donations.StripeApi import org.signal.donations.StripeIntentAccessor -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage 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 @@ -28,15 +31,17 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels import org.whispersystems.signalservice.api.util.Preconditions class StripePaymentInProgressViewModel( - private val donationPaymentRepository: DonationPaymentRepository + private val stripeRepository: StripeRepository, + private val monthlyDonationRepository: MonthlyDonationRepository, + private val oneTimeDonationRepository: OneTimeDonationRepository ) : ViewModel() { companion object { private val TAG = Log.tag(StripePaymentInProgressViewModel::class.java) } - private val store = RxStore(StripeStage.INIT) - val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + private val store = RxStore(DonationProcessorStage.INIT) + val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) private val disposables = CompositeDisposable() private var paymentData: PaymentData? = null @@ -59,7 +64,7 @@ class StripePaymentInProgressViewModel( Preconditions.checkState(store.state.isTerminal) Log.d(TAG, "Ending current state. Clearing state and setting stage to INIT", true) - store.update { StripeStage.INIT } + store.update { DonationProcessorStage.INIT } disposables.clear() } @@ -87,7 +92,7 @@ class StripePaymentInProgressViewModel( paymentData == null && cardData == null -> error("No payment provider available.") paymentData != null && cardData != null -> error("Too many providers available") paymentData != null -> Single.just(GooglePayPaymentSource(paymentData)) - cardData != null -> donationPaymentRepository.createCreditCardPaymentSource(errorSource, cardData) + cardData != null -> stripeRepository.createCreditCardPaymentSource(errorSource, cardData) else -> error("This should never happen.") }.doAfterTerminate { clearPaymentInformation() } } @@ -114,28 +119,28 @@ class StripePaymentInProgressViewModel( } private fun proceedMonthly(request: GatewayRequest, paymentSourceProvider: Single, nextActionHandler: (StripeApi.Secure3DSAction) -> Single) { - val ensureSubscriberId: Completable = donationPaymentRepository.ensureSubscriberId() - val createAndConfirmSetupIntent: Single = paymentSourceProvider.flatMap { donationPaymentRepository.createAndConfirmSetupIntent(it) } - val setLevel: Completable = donationPaymentRepository.setSubscriptionLevel(request.level.toString()) + val ensureSubscriberId: Completable = monthlyDonationRepository.ensureSubscriberId() + val createAndConfirmSetupIntent: Single = paymentSourceProvider.flatMap { stripeRepository.createAndConfirmSetupIntent(it) } + val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString()) Log.d(TAG, "Starting subscription payment pipeline...", true) - store.update { StripeStage.PAYMENT_PIPELINE } + store.update { DonationProcessorStage.PAYMENT_PIPELINE } val setup: Completable = ensureSubscriberId - .andThen(cancelActiveSubscriptionIfNecessary()) + .andThen(monthlyDonationRepository.cancelActiveSubscriptionIfNecessary()) .andThen(createAndConfirmSetupIntent) .flatMap { secure3DSAction -> nextActionHandler(secure3DSAction) - .flatMap { secure3DSResult -> donationPaymentRepository.getStatusAndPaymentMethodId(secure3DSResult) } + .flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult) } .map { (_, paymentMethod) -> paymentMethod ?: secure3DSAction.paymentMethodId!! } } - .flatMapCompletable { donationPaymentRepository.setDefaultPaymentMethod(it) } + .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it) } .onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it)) } disposables += setup.andThen(setLevel).subscribeBy( onError = { throwable -> Log.w(TAG, "Failure in subscription payment pipeline...", throwable, true) - store.update { StripeStage.FAILED } + store.update { DonationProcessorStage.FAILED } val donationError: DonationError = if (throwable is DonationError) { throwable @@ -146,25 +151,11 @@ class StripePaymentInProgressViewModel( }, onComplete = { Log.d(TAG, "Finished subscription payment pipeline...", true) - store.update { StripeStage.COMPLETE } + store.update { DonationProcessorStage.COMPLETE } } ) } - private fun cancelActiveSubscriptionIfNecessary(): Completable { - return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable { - if (it) { - Log.d(TAG, "Cancelling active subscription...", true) - donationPaymentRepository.cancelActiveSubscription().doOnComplete { - SignalStore.donationsValues().updateLocalStateForManualCancellation() - MultiDeviceSubscriptionSyncRequestJob.enqueue() - } - } else { - Completable.complete() - } - } - } - private fun proceedOneTime( request: GatewayRequest, paymentSourceProvider: Single, @@ -176,18 +167,18 @@ class StripePaymentInProgressViewModel( val recipient = Recipient.self().id val level = SubscriptionLevels.BOOST_LEVEL.toLong() - val continuePayment: Single = donationPaymentRepository.continuePayment(amount, recipient, level) + val continuePayment: Single = stripeRepository.continuePayment(amount, recipient, level) val intentAndSource: Single> = Single.zip(continuePayment, paymentSourceProvider, ::Pair) disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) -> - donationPaymentRepository.confirmPayment(paymentSource, paymentIntent, recipient) + stripeRepository.confirmPayment(paymentSource, paymentIntent, recipient) .flatMap { nextActionHandler(it) } - .flatMap { donationPaymentRepository.getStatusAndPaymentMethodId(it) } - .flatMapCompletable { donationPaymentRepository.waitForOneTimeRedemption(amount, paymentIntent, recipient, null, level) } + .flatMap { stripeRepository.getStatusAndPaymentMethodId(it) } + .flatMapCompletable { oneTimeDonationRepository.waitForOneTimeRedemption(amount, paymentIntent.intentId, recipient, null, level) } }.subscribeBy( onError = { throwable -> Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true) - store.update { StripeStage.FAILED } + store.update { DonationProcessorStage.FAILED } val donationError: DonationError = if (throwable is DonationError) { throwable @@ -198,7 +189,7 @@ class StripePaymentInProgressViewModel( }, onComplete = { Log.w(TAG, "Completed one-time payment pipeline...", true) - store.update { StripeStage.COMPLETE } + store.update { DonationProcessorStage.COMPLETE } } ) } @@ -206,18 +197,18 @@ class StripePaymentInProgressViewModel( fun cancelSubscription() { Log.d(TAG, "Beginning cancellation...", true) - store.update { StripeStage.CANCELLING } - disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy( + store.update { DonationProcessorStage.CANCELLING } + disposables += monthlyDonationRepository.cancelActiveSubscription().subscribeBy( onComplete = { Log.d(TAG, "Cancellation succeeded", true) SignalStore.donationsValues().updateLocalStateForManualCancellation() MultiDeviceSubscriptionSyncRequestJob.enqueue() - donationPaymentRepository.scheduleSyncForAccountRecordChange() - store.update { StripeStage.COMPLETE } + stripeRepository.scheduleSyncForAccountRecordChange() + store.update { DonationProcessorStage.COMPLETE } }, onError = { throwable -> Log.w(TAG, "Cancellation failed", throwable, true) - store.update { StripeStage.FAILED } + store.update { DonationProcessorStage.FAILED } } ) } @@ -225,12 +216,12 @@ class StripePaymentInProgressViewModel( fun updateSubscription(request: GatewayRequest) { Log.d(TAG, "Beginning subscription update...", true) - store.update { StripeStage.PAYMENT_PIPELINE } - disposables += cancelActiveSubscriptionIfNecessary().andThen(donationPaymentRepository.setSubscriptionLevel(request.level.toString())) + store.update { DonationProcessorStage.PAYMENT_PIPELINE } + disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString())) .subscribeBy( onComplete = { Log.w(TAG, "Completed subscription update", true) - store.update { StripeStage.COMPLETE } + store.update { DonationProcessorStage.COMPLETE } }, onError = { throwable -> Log.w(TAG, "Failed to update subscription", throwable, true) @@ -241,16 +232,18 @@ class StripePaymentInProgressViewModel( } DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError) - store.update { StripeStage.FAILED } + store.update { DonationProcessorStage.FAILED } } ) } class Factory( - private val donationPaymentRepository: DonationPaymentRepository + private val stripeRepository: StripeRepository, + private val monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()), + private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService()) ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.cast(StripePaymentInProgressViewModel(donationPaymentRepository)) as T + return modelClass.cast(StripePaymentInProgressViewModel(stripeRepository, monthlyDonationRepository, oneTimeDonationRepository)) as T } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index 83e67a5494..a6977bae29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure import org.thoughtcrime.securesms.components.settings.configure @@ -58,7 +58,7 @@ class ManageDonationsFragment : private val viewModel: ManageDonationsViewModel by viewModels( factoryProducer = { - ManageDonationsViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService())) + ManageDonationsViewModel.Factory(MonthlyDonationRepository(ApplicationDependencies.getDonationsService())) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt index a132c0d226..be10a88e54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt @@ -11,7 +11,7 @@ import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.recipients.Recipient @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.util.livedata.Store import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription class ManageDonationsViewModel( - private val subscriptionsRepository: SubscriptionsRepository + private val subscriptionsRepository: MonthlyDonationRepository ) : ViewModel() { private val store = Store(ManageDonationsState()) @@ -122,7 +122,7 @@ class ManageDonationsViewModel( } class Factory( - private val subscriptionsRepository: SubscriptionsRepository + private val subscriptionsRepository: MonthlyDonationRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast(ManageDonationsViewModel(subscriptionsRepository))!! diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt index 5d16a48c1d..7538d78863 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.HidingLinearLayout import org.thoughtcrime.securesms.components.reminder.ReminderView import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicTheme @@ -113,6 +113,6 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare return fragment.reminderView } - override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) } + override val stripeRepository: StripeRepository by lazy { StripeRepository(this) } override val googlePayResultPublisher: Subject = PublishSubject.create() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java index e54c2f7533..b365b245b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java @@ -55,7 +55,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob { private final String paymentIntentId; private final long badgeLevel; - private static BoostReceiptRequestResponseJob createJob(StripeIntentAccessor paymentIntent, DonationErrorSource donationErrorSource, long badgeLevel) { + private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel) { return new BoostReceiptRequestResponseJob( new Parameters .Builder() @@ -65,14 +65,14 @@ public class BoostReceiptRequestResponseJob extends BaseJob { .setMaxAttempts(Parameters.UNLIMITED) .build(), null, - paymentIntent.getIntentId(), + paymentIntentId, donationErrorSource, badgeLevel ); } - public static JobManager.Chain createJobChainForBoost(@NonNull StripeIntentAccessor paymentIntent) { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL)); + public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId) { + BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL)); DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(); RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost(); MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); @@ -84,12 +84,12 @@ public class BoostReceiptRequestResponseJob extends BaseJob { .then(multiDeviceProfileContentUpdateJob); } - public static JobManager.Chain createJobChainForGift(@NonNull StripeIntentAccessor paymentIntent, + public static JobManager.Chain createJobChainForGift(@NonNull String paymentIntentId, @NonNull RecipientId recipientId, @Nullable String additionalMessage, long badgeLevel) { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent, DonationErrorSource.GIFT, badgeLevel); + BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel); GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage); diff --git a/app/src/main/res/layout/dsl_button_primary.xml b/app/src/main/res/layout/dsl_button_primary.xml index 2275a4585f..b10d735e27 100644 --- a/app/src/main/res/layout/dsl_button_primary.xml +++ b/app/src/main/res/layout/dsl_button_primary.xml @@ -8,9 +8,11 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/dsl_settings_gutter" android:layout_marginEnd="@dimen/dsl_settings_gutter" + android:insetTop="2dp" + android:insetBottom="2dp" app:iconGravity="textStart" + app:iconSize="32dp" app:iconTint="@null" tools:icon="@drawable/credit_card" - app:iconSize="32dp" tools:text="Primary button" tools:viewBindingIgnore="true" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/donate_to_signal.xml b/app/src/main/res/navigation/donate_to_signal.xml index bd73347dc8..563701baaf 100644 --- a/app/src/main/res/navigation/donate_to_signal.xml +++ b/app/src/main/res/navigation/donate_to_signal.xml @@ -78,7 +78,7 @@ diff --git a/donations/lib/src/main/res/layout/donate_with_googlepay_button.xml b/donations/lib/src/main/res/layout/donate_with_googlepay_button.xml index 62eb72e1d3..888e1acb3f 100755 --- a/donations/lib/src/main/res/layout/donate_with_googlepay_button.xml +++ b/donations/lib/src/main/res/layout/donate_with_googlepay_button.xml @@ -1,31 +1,36 @@ + android:clickable="true" + android:contentDescription="@string/donate_with_googlepay_button_content_description" + android:focusable="true" + android:padding="2sp"> + + android:orientation="vertical" + android:weightSum="2"> + + android:scaleType="fitCenter" + android:src="@drawable/donate_with_googlepay_button_content" /> + + android:scaleType="fitXY" + android:src="@drawable/googlepay_button_overlay" /> \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java index 9d9d0300fa..1bb049d04f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -9,8 +9,8 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; +import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret; import org.whispersystems.signalservice.api.subscriptions.SubscriberId; -import org.whispersystems.signalservice.api.subscriptions.SubscriptionClientSecret; import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.internal.EmptyResponse; @@ -43,7 +43,8 @@ public class DonationsService { String signalAgent, GroupsV2Operations groupsV2Operations, boolean automaticNetworkRetry - ) { + ) + { this(new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations(), automaticNetworkRetry)); } @@ -71,12 +72,12 @@ public class DonationsService { /** * Submits price information to the server to generate a payment intent via the payment gateway. * - * @param amount Price, in the minimum currency unit (e.g. cents or yen) - * @param currencyCode The currency code for the amount - * @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway. + * @param amount Price, in the minimum currency unit (e.g. cents or yen) + * @param currencyCode The currency code for the amount + * @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway. */ - public ServiceResponse createDonationIntentWithAmount(String amount, String currencyCode, long level) { - return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.createBoostPaymentMethod(currencyCode, Long.parseLong(amount), level), 200)); + public ServiceResponse createDonationIntentWithAmount(String amount, String currencyCode, long level) { + return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.createStripeOneTimePaymentIntent(currencyCode, Long.parseLong(amount), level), 200)); } /** @@ -165,9 +166,10 @@ public class DonationsService { String currencyCode, String idempotencyKey, Object mutex - ) { + ) + { return wrapInServiceResponse(() -> { - synchronized(mutex) { + synchronized (mutex) { pushServiceSocket.updateSubscriptionLevel(subscriberId.serialize(), level, currencyCode, idempotencyKey); } return new Pair<>(EmptyResponse.INSTANCE, 200); @@ -188,11 +190,11 @@ public class DonationsService { * Creates a subscriber record on the signal server and stripe. Can be called idempotently as-is. After receiving 200 from this endpoint, * clients should save subscriberId locally and to storage service for the account. If you get a 403 from this endpoint and you did not * use an account authenticated connection, then the subscriberId has been corrupted in some way. - * + *

* Clients MUST periodically hit this endpoint to update the access time on the subscription record. Recommend trying to call it approximately * every 3 days. Not accessing this endpoint for an extended period of time will result in the subscription being canceled. * - * @param subscriberId The subscriber ID for the user polling their subscription + * @param subscriberId The subscriber ID for the user polling their subscription */ public ServiceResponse putSubscription(SubscriberId subscriberId) { return wrapInServiceResponse(() -> { @@ -204,7 +206,7 @@ public class DonationsService { /** * Cancels any current subscription at the end of the current subscription period. * - * @param subscriberId The subscriber ID for the user cancelling their subscription + * @param subscriberId The subscriber ID for the user cancelling their subscription */ public ServiceResponse cancelSubscription(SubscriberId subscriberId) { return wrapInServiceResponse(() -> { @@ -213,7 +215,7 @@ public class DonationsService { }); } - public ServiceResponse setDefaultPaymentMethodId(SubscriberId subscriberId, String paymentMethodId) { + public ServiceResponse setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) { return wrapInServiceResponse(() -> { pushServiceSocket.setDefaultSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId); return new Pair<>(EmptyResponse.INSTANCE, 200); @@ -226,9 +228,9 @@ public class DonationsService { * @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs * but instead with the SetupIntent stripe APIs. */ - public ServiceResponse createSubscriptionPaymentMethod(SubscriberId subscriberId) { + public ServiceResponse createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) { return wrapInServiceResponse(() -> { - SubscriptionClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize()); + StripeClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize()); return new Pair<>(clientSecret, 200); }); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/SubscriptionClientSecret.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/StripeClientSecret.java similarity index 78% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/SubscriptionClientSecret.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/StripeClientSecret.java index f77218fc07..952ecf82c8 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/SubscriptionClientSecret.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/subscriptions/StripeClientSecret.java @@ -3,13 +3,13 @@ package org.whispersystems.signalservice.api.subscriptions; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public final class SubscriptionClientSecret { +public final class StripeClientSecret { private final String id; private final String clientSecret; @JsonCreator - public SubscriptionClientSecret(@JsonProperty("clientSecret") String clientSecret) { + public StripeClientSecret(@JsonProperty("clientSecret") String clientSecret) { this.id = clientSecret.replaceFirst("_secret.*", ""); this.clientSecret = clientSecret; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 4ed7b2ec37..b81b22e827 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -86,7 +86,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedExc import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.storage.StorageAuthResponse; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; -import org.whispersystems.signalservice.api.subscriptions.SubscriptionClientSecret; +import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret; import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Tls12SocketFactory; @@ -1017,10 +1017,10 @@ public class PushServiceSocket { makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload); } - public SubscriptionClientSecret createBoostPaymentMethod(String currencyCode, long amount, long level) throws IOException { - String payload = JsonUtil.toJson(new DonationIntentPayload(amount, currencyCode, level)); + public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException { + String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level)); String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload); - return JsonUtil.fromJsonResponse(result, SubscriptionClientSecret.class); + return JsonUtil.fromJsonResponse(result, StripeClientSecret.class); } public Map> getBoostAmounts() throws IOException { @@ -1082,9 +1082,9 @@ public class PushServiceSocket { makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null); } - public SubscriptionClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException { + public StripeClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException { String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", ""); - return JsonUtil.fromJson(response, SubscriptionClientSecret.class); + return JsonUtil.fromJson(response, StripeClientSecret.class); } public void setDefaultSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DonationIntentPayload.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java similarity index 72% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DonationIntentPayload.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java index de8df9450b..3638901a94 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DonationIntentPayload.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java @@ -2,7 +2,7 @@ package org.whispersystems.signalservice.internal.push; import com.fasterxml.jackson.annotation.JsonProperty; -class DonationIntentPayload { +class StripeOneTimePaymentIntentPayload { @JsonProperty private long amount; @@ -12,7 +12,7 @@ class DonationIntentPayload { @JsonProperty private long level; - public DonationIntentPayload(long amount, String currency, long level) { + public StripeOneTimePaymentIntentPayload(long amount, String currency, long level) { this.amount = amount; this.currency = currency; this.level = level;