mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 10:20:25 +01:00
Implement new APIs for Boost badging.
This commit is contained in:
committed by
Greyson Parrelli
parent
48a81da883
commit
186bd9db48
@@ -39,7 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
|
||||
private val boostViewModel: BoostViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BoostViewModel.Factory(BoostRepository(), donationRepository, FETCH_BOOST_TOKEN_REQUEST_CODE)
|
||||
BoostViewModel.Factory(BoostRepository(ApplicationDependencies.getDonationsService()), donationRepository, FETCH_BOOST_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.StripeApi
|
||||
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.subscription.LevelUpdateOperation
|
||||
@@ -19,6 +19,9 @@ import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
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.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -67,7 +70,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(Exception("Amount is too small"))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(Exception("Amount is too large"))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(Exception("Currency is not supported"))
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), result.paymentIntent)
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(paymentData, result.paymentIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,14 +84,9 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
|
||||
fun cancelActiveSubscription(): Completable {
|
||||
val localSubscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
return ApplicationDependencies.getDonationsService().cancelSubscription(localSubscriber.subscriberId).flatMapCompletable {
|
||||
when {
|
||||
it.status == 200 -> Completable.complete()
|
||||
it.applicationError.isPresent -> Completable.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Completable.error(it.executionError.get())
|
||||
else -> Completable.error(AssertionError("Something bad happened"))
|
||||
}
|
||||
}
|
||||
return ApplicationDependencies.getDonationsService()
|
||||
.cancelSubscription(localSubscriber.subscriberId)
|
||||
.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement()
|
||||
}
|
||||
|
||||
fun ensureSubscriberId(): Completable {
|
||||
@@ -96,14 +94,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
return ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.putSubscription(subscriberId)
|
||||
.flatMapCompletable {
|
||||
when {
|
||||
it.status == 200 -> Completable.complete()
|
||||
it.applicationError.isPresent -> Completable.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Completable.error(it.executionError.get())
|
||||
else -> Completable.error(AssertionError("Something bad happened"))
|
||||
}
|
||||
}
|
||||
.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement()
|
||||
.doOnComplete {
|
||||
SignalStore
|
||||
.donationsValues()
|
||||
@@ -111,6 +102,36 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmPayment(paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
|
||||
return Completable.create {
|
||||
stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).blockingSubscribe()
|
||||
|
||||
val jobIds = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent)
|
||||
val countDownLatch = CountDownLatch(2)
|
||||
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
it.onComplete()
|
||||
} else {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(subscriptionLevel: String): Completable {
|
||||
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||
.flatMapCompletable { levelUpdateOperation ->
|
||||
@@ -121,40 +142,29 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
subscriptionLevel,
|
||||
subscriber.currencyCode,
|
||||
levelUpdateOperation.idempotencyKey.serialize()
|
||||
).flatMapCompletable { response ->
|
||||
when {
|
||||
response.status == 200 -> Completable.complete()
|
||||
response.applicationError.isPresent -> Completable.error(response.applicationError.get())
|
||||
response.executionError.isPresent -> Completable.error(response.executionError.get())
|
||||
else -> Completable.error(AssertionError("should never happen"))
|
||||
}
|
||||
}.andThen {
|
||||
).flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().andThen {
|
||||
SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation)
|
||||
it.onComplete()
|
||||
}.andThen {
|
||||
val jobIds = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
|
||||
val countDownLatch = CountDownLatch(2)
|
||||
|
||||
val firstJobListener = JobTracker.JobListener { _, jobState ->
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
val secondJobListener = JobTracker.JobListener { _, jobState ->
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.first(), firstJobListener)
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.second(), secondJobListener)
|
||||
|
||||
try {
|
||||
if (!countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
} else {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
it.onComplete()
|
||||
} else {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
@@ -182,29 +192,17 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
return ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode)
|
||||
.flatMap { response ->
|
||||
when {
|
||||
response.status == 200 -> Single.just(StripeApi.PaymentIntent(response.result.get().id, response.result.get().clientSecret))
|
||||
response.executionError.isPresent -> Single.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Single.error(response.applicationError.get())
|
||||
else -> Single.error(AssertionError("should never get here"))
|
||||
}
|
||||
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||
.map {
|
||||
StripeApi.PaymentIntent(it.id, it.clientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSetupIntent(): Single<StripeApi.SetupIntent> {
|
||||
return Single.fromCallable {
|
||||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
ApplicationDependencies.getDonationsService().createSubscriptionPaymentMethod(it.subscriberId)
|
||||
}.flatMap { response ->
|
||||
when {
|
||||
response.status == 200 -> Single.just(StripeApi.SetupIntent(response.result.get().id, response.result.get().clientSecret))
|
||||
response.executionError.isPresent -> Single.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Single.error(response.applicationError.get())
|
||||
else -> Single.error(AssertionError("should never get here"))
|
||||
}
|
||||
}
|
||||
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
|
||||
.flatMap { ApplicationDependencies.getDonationsService().createSubscriptionPaymentMethod(it.subscriberId) }
|
||||
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||
.map { StripeApi.SetupIntent(it.id, it.clientSecret) }
|
||||
}
|
||||
|
||||
override fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||
@@ -212,13 +210,6 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
ApplicationDependencies.getDonationsService().setDefaultPaymentMethodId(it.subscriberId, paymentMethodId)
|
||||
}.flatMapCompletable { response ->
|
||||
when {
|
||||
response.status == 200 -> Completable.complete()
|
||||
response.executionError.isPresent -> Completable.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Completable.error(response.applicationError.get())
|
||||
else -> Completable.error(AssertionError("Should never get here"))
|
||||
}
|
||||
}
|
||||
}.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
@@ -18,14 +19,8 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
|
||||
fun getActiveSubscription(): Single<ActiveSubscription> {
|
||||
val localSubscription = SignalStore.donationsValues().getSubscriber()
|
||||
return if (localSubscription != null) {
|
||||
donationsService.getSubscription(localSubscription.subscriberId).flatMap {
|
||||
when {
|
||||
it.status == 200 -> Single.just(it.result.get())
|
||||
it.applicationError.isPresent -> Single.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Single.error(it.executionError.get())
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
donationsService.getSubscription(localSubscription.subscriberId)
|
||||
.flatMap(ServiceResponse<ActiveSubscription>::flattenResult)
|
||||
} else {
|
||||
Single.just(ActiveSubscription(null))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import java.util.regex.Pattern
|
||||
* can unlock a corresponding badge for a time determined by the server.
|
||||
*/
|
||||
data class Boost(
|
||||
val badge: Badge,
|
||||
val price: FiatMoney
|
||||
) {
|
||||
|
||||
@@ -93,7 +92,7 @@ data class Boost(
|
||||
button.text = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
boost.price,
|
||||
FiatMoneyUtil.formatOptions()
|
||||
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||
)
|
||||
button.setOnClickListener {
|
||||
model.onBoostClick(boost)
|
||||
|
||||
@@ -1,47 +1,29 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
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
|
||||
|
||||
class BoostRepository {
|
||||
class BoostRepository(private val donationsService: DonationsService) {
|
||||
|
||||
fun getBoosts(currency: Currency): Single<Pair<List<Boost>, Boost?>> {
|
||||
val boosts = testBoosts(currency)
|
||||
|
||||
return Single.just(
|
||||
Pair(
|
||||
boosts,
|
||||
boosts[2]
|
||||
)
|
||||
)
|
||||
fun getBoosts(currency: Currency): Single<List<Boost>> {
|
||||
return donationsService.boostAmounts
|
||||
.flatMap(ServiceResponse<Map<String, List<BigDecimal>>>::flattenResult)
|
||||
.map { result ->
|
||||
val boosts = result[currency.currencyCode] ?: throw Exception("Unsupported currency! ${currency.currencyCode}")
|
||||
boosts.map { Boost(FiatMoney(it, currency)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoostBadge(): Single<Badge> = Single.fromCallable {
|
||||
// Get boost badge from server
|
||||
// throw NotImplementedError()
|
||||
testBadge
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val testBadge = Badge(
|
||||
id = "TEST",
|
||||
category = Badge.Category.Testing,
|
||||
name = "Test Badge",
|
||||
description = "Test Badge",
|
||||
imageUrl = Uri.EMPTY,
|
||||
imageDensity = "xxxhdpi",
|
||||
expirationTimestamp = 0L,
|
||||
visible = false,
|
||||
)
|
||||
|
||||
private fun testBoosts(currency: Currency) = listOf(
|
||||
3L, 5L, 10L, 20L, 50L, 100L
|
||||
).map {
|
||||
Boost(testBadge, FiatMoney(BigDecimal.valueOf(it), currency))
|
||||
}
|
||||
fun getBoostBadge(): Single<Badge> {
|
||||
return donationsService.boostBadge
|
||||
.flatMap(ServiceResponse<SignalServiceProfile.Badge>::flattenResult)
|
||||
.map(Badges::fromServiceBadge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ class BoostViewModel(
|
||||
val boosts = currencyObservable.flatMapSingle { boostRepository.getBoosts(it) }
|
||||
val boostBadge = boostRepository.getBoostBadge()
|
||||
|
||||
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) { (boosts, defaultBoost), badge -> BoostInfo(boosts, defaultBoost, badge) }.subscribe { info ->
|
||||
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) {
|
||||
boostList, badge ->
|
||||
BoostInfo(boostList, boostList[2], badge)
|
||||
}.subscribe { info ->
|
||||
store.update {
|
||||
it.copy(
|
||||
boosts = info.boosts,
|
||||
@@ -79,25 +82,22 @@ class BoostViewModel(
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode,
|
||||
resultCode,
|
||||
data,
|
||||
this.fetchTokenRequestCode,
|
||||
requestCode, resultCode, data, this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
if (boost != null) {
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
|
||||
|
||||
donationPaymentRepository.continuePayment(boost.price, paymentData).subscribeBy(
|
||||
onError = { throwable ->
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationError(throwable))
|
||||
},
|
||||
onComplete = {
|
||||
// TODO [alex] Now we need to do the whole query for a token, submit token rigamarole
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.boostBadge!!))
|
||||
}
|
||||
@@ -127,10 +127,8 @@ class BoostViewModel(
|
||||
|
||||
store.update { it.copy(stage = BoostState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
// TODO [alex] -- Do we want prevalidation? Stripe will catch us anyway.
|
||||
// TODO [alex] -- Custom boost badge details... how do we determine this?
|
||||
boostToPurchase = if (snapshot.isCustomAmountFocused) {
|
||||
Boost(snapshot.selectedBoost.badge, snapshot.customAmount)
|
||||
Boost(snapshot.customAmount)
|
||||
} else {
|
||||
snapshot.selectedBoost
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@ import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
class SetCurrencyViewModel(val isBoost: Boolean) : ViewModel() {
|
||||
class SetCurrencyViewModel(private val isBoost: Boolean) : ViewModel() {
|
||||
|
||||
private val store = Store(SetCurrencyState())
|
||||
|
||||
val state: LiveData<SetCurrencyState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
val defaultCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
val defaultCurrency = if (isBoost) {
|
||||
SignalStore.donationsValues().getBoostCurrency()
|
||||
} else {
|
||||
SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
}
|
||||
|
||||
store.update { state ->
|
||||
val platformCurrencies = Currency.getAvailableCurrencies()
|
||||
|
||||
@@ -107,7 +107,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
|
||||
if (controlState == ControlState.DISPLAY) {
|
||||
badgeRepository.setVisibilityForAllBadges(controlChecked).subscribe()
|
||||
} else {
|
||||
} else if (controlChecked) {
|
||||
badgeRepository.setFeaturedBadge(args.badge).subscribe()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user