Implement new APIs for Boost badging.
This commit is contained in:
parent
48a81da883
commit
186bd9db48
19 changed files with 457 additions and 137 deletions
|
@ -23,7 +23,7 @@ class BadgeRepository(context: Context) {
|
|||
|
||||
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges
|
||||
val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge)
|
||||
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
|
||||
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
|
|
|
@ -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 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 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> {
|
||||
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?
|
||||
) {
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode,
|
||||
resultCode,
|
||||
data,
|
||||
this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode, resultCode, data, this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.donations.StripeApi;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredential;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialRequestContext;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
import org.signal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.subscription.SubscriptionNotification;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Job responsible for submitting ReceiptCredentialRequest objects to the server until
|
||||
* we get a response.
|
||||
*/
|
||||
public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
|
||||
private static final String TAG = Log.tag(BoostReceiptRequestResponseJob.class);
|
||||
|
||||
public static final String KEY = "BoostReceiptCredentialsSubmissionJob";
|
||||
|
||||
private static final String DATA_REQUEST_BYTES = "data.request.bytes";
|
||||
private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id";
|
||||
|
||||
private ReceiptCredentialRequestContext requestContext;
|
||||
|
||||
private final String paymentIntentId;
|
||||
|
||||
static BoostReceiptRequestResponseJob createJob(StripeApi.PaymentIntent paymentIntent) {
|
||||
return new BoostReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("BoostReceiptRedemption")
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(30))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
null,
|
||||
paymentIntent.getId()
|
||||
);
|
||||
}
|
||||
|
||||
public static Pair<String, String> enqueueChain(StripeApi.PaymentIntent paymentIntent) {
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(requestReceiptJob)
|
||||
.then(redeemReceiptJob)
|
||||
.enqueue();
|
||||
|
||||
return new Pair<>(requestReceiptJob.getId(), redeemReceiptJob.getId());
|
||||
}
|
||||
|
||||
private BoostReceiptRequestResponseJob(@NonNull Parameters parameters,
|
||||
@Nullable ReceiptCredentialRequestContext requestContext,
|
||||
@NonNull String paymentIntentId)
|
||||
{
|
||||
super(parameters);
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
Data.Builder builder = new Data.Builder().putString(DATA_PAYMENT_INTENT_ID, paymentIntentId);
|
||||
|
||||
if (requestContext != null) {
|
||||
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
SubscriptionNotification.VerificationFailed.INSTANCE.show(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
if (requestContext == null) {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
byte[] randomBytes = new byte[ReceiptSerial.SIZE];
|
||||
|
||||
secureRandom.nextBytes(randomBytes);
|
||||
|
||||
ReceiptSerial receiptSerial = new ReceiptSerial(randomBytes);
|
||||
ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations();
|
||||
|
||||
requestContext = operations.createReceiptCredentialRequestContext(secureRandom, receiptSerial);
|
||||
}
|
||||
|
||||
ServiceResponse<ReceiptCredentialResponse> response = ApplicationDependencies.getDonationsService()
|
||||
.submitBoostReceiptCredentialRequest(paymentIntentId, requestContext.getRequest())
|
||||
.blockingGet();
|
||||
|
||||
if (response.getApplicationError().isPresent()) {
|
||||
if (response.getStatus() == 204) {
|
||||
Log.w(TAG, "User does not have receipts available to exchange. Exiting.", response.getApplicationError().get());
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a permanent failure: " + response.getStatus(), response.getApplicationError().get());
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
}
|
||||
} else if (response.getResult().isPresent()) {
|
||||
ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get());
|
||||
|
||||
if (!isCredentialValid(receiptCredential)) {
|
||||
throw new IOException("Could not validate receipt credential");
|
||||
}
|
||||
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential);
|
||||
setOutputData(new Data.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION,
|
||||
receiptCredentialPresentation.serialize())
|
||||
.build());
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orNull());
|
||||
throw new RetryableException();
|
||||
}
|
||||
}
|
||||
|
||||
private ReceiptCredentialPresentation getReceiptCredentialPresentation(@NonNull ReceiptCredential receiptCredential) throws RetryableException {
|
||||
ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations();
|
||||
|
||||
try {
|
||||
return operations.createReceiptCredentialPresentation(receiptCredential);
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e);
|
||||
requestContext = null;
|
||||
throw new RetryableException();
|
||||
}
|
||||
}
|
||||
|
||||
private ReceiptCredential getReceiptCredential(@NonNull ReceiptCredentialResponse response) throws RetryableException {
|
||||
ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations();
|
||||
|
||||
try {
|
||||
return operations.receiveReceiptCredential(requestContext, response);
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e);
|
||||
requestContext = null;
|
||||
throw new RetryableException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the generated Receipt Credential has the following characteristics
|
||||
* - level should match the current subscription level and be the same level you signed up for at the time the subscription was last updated
|
||||
* - expiration time should have the following characteristics:
|
||||
* - expiration_time mod 86400 == 0
|
||||
* - expiration_time is between now and 60 days from now
|
||||
*/
|
||||
private boolean isCredentialValid(@NonNull ReceiptCredential receiptCredential) {
|
||||
long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
|
||||
long monthFromNow = now + TimeUnit.DAYS.toSeconds(60);
|
||||
boolean isCorrectLevel = receiptCredential.getReceiptLevel() == 1;
|
||||
boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0;
|
||||
boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now;
|
||||
boolean isExpirationWithinAMonth = receiptCredential.getReceiptExpirationTime() < monthFromNow;
|
||||
|
||||
return isCorrectLevel && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinAMonth;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof RetryableException;
|
||||
}
|
||||
|
||||
@VisibleForTesting final static class RetryableException extends Exception {
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<BoostReceiptRequestResponseJob> {
|
||||
@Override
|
||||
public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
|
||||
try {
|
||||
if (data.hasString(DATA_REQUEST_BYTES)) {
|
||||
byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES);
|
||||
ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob);
|
||||
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId);
|
||||
} else {
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId);
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,13 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey;
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
@ -27,7 +29,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
public static final String KEY = "DonationReceiptRedemptionJob";
|
||||
public static final String INPUT_RECEIPT_CREDENTIAL_PRESENTATION = "data.receipt.credential.presentation";
|
||||
|
||||
public static DonationReceiptRedemptionJob createJob() {
|
||||
public static DonationReceiptRedemptionJob createJobForSubscription() {
|
||||
return new DonationReceiptRedemptionJob(
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
|
@ -39,6 +41,17 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
.build());
|
||||
}
|
||||
|
||||
public static DonationReceiptRedemptionJob createJobForBoost() {
|
||||
return new DonationReceiptRedemptionJob(
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("BoostReceiptRedemption")
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(30))
|
||||
.build());
|
||||
}
|
||||
|
||||
private DonationReceiptRedemptionJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@ public final class JobManagerFactories {
|
|||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
put(SubscriptionKeepAliveJob.KEY, new SubscriptionKeepAliveJob.Factory());
|
||||
put(SubscriptionReceiptRequestResponseJob.KEY, new SubscriptionReceiptRequestResponseJob.Factory());
|
||||
put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory());
|
||||
put(DonationReceiptRedemptionJob.KEY, new DonationReceiptRedemptionJob.Factory());
|
||||
|
||||
// Migrations
|
||||
|
|
|
@ -67,7 +67,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
public static Pair<String, String> enqueueSubscriptionContinuation() {
|
||||
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId());
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJob();
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription();
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(requestReceiptJob)
|
||||
|
|
|
@ -60,6 +60,11 @@ public final class FiatMoneyUtil {
|
|||
formatter.setMinimumFractionDigits(amount.getCurrency().getDefaultFractionDigits());
|
||||
}
|
||||
|
||||
if (options.trimZerosAfterDecimal) {
|
||||
formatter.setMinimumFractionDigits(0);
|
||||
formatter.setMaximumFractionDigits(amount.getCurrency().getDefaultFractionDigits());
|
||||
}
|
||||
|
||||
String formattedAmount = formatter.format(amount.getAmount());
|
||||
if (amount.getTimestamp() > 0 && options.displayTime) {
|
||||
return resources.getString(R.string.CurrencyAmountFormatter_s_at_s,
|
||||
|
@ -108,6 +113,7 @@ public final class FiatMoneyUtil {
|
|||
public static class FormatOptions {
|
||||
private boolean displayTime = true;
|
||||
private boolean withSymbol = true;
|
||||
private boolean trimZerosAfterDecimal = false;
|
||||
|
||||
private FormatOptions() {
|
||||
}
|
||||
|
@ -121,5 +127,10 @@ public final class FiatMoneyUtil {
|
|||
this.withSymbol = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull FormatOptions trimZerosAfterDecimal() {
|
||||
this.trimZerosAfterDecimal = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.signal.zkgroup.receipts.ReceiptCredentialResponse;
|
|||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
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.SubscriberId;
|
||||
|
@ -19,6 +20,9 @@ import org.whispersystems.signalservice.internal.push.DonationIntentResult;
|
|||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.rxjava3.annotations.NonNull;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
@ -73,8 +77,33 @@ public class DonationsService {
|
|||
* @param currencyCode The currency code for the amount
|
||||
* @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway.
|
||||
*/
|
||||
public Single<ServiceResponse<DonationIntentResult>> createDonationIntentWithAmount(String amount, String currencyCode) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createDonationIntentWithAmount(amount, currencyCode), 200));
|
||||
public Single<ServiceResponse<SubscriptionClientSecret>> createDonationIntentWithAmount(String amount, String currencyCode) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createBoostPaymentMethod(currencyCode, Long.parseLong(amount)), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a completed payment intent and a receipt credential request produces a receipt credential response.
|
||||
* Clients should always use the same ReceiptCredentialRequest with the same payment intent id. This request is repeatable so long as the two values are reused.
|
||||
*
|
||||
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
|
||||
* @param receiptCredentialRequest Client-generated request token
|
||||
*/
|
||||
public Single<ServiceResponse<ReceiptCredentialResponse>> submitBoostReceiptCredentialRequest(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The suggested amounts for Signal Boost
|
||||
*/
|
||||
public Single<ServiceResponse<Map<String, List<BigDecimal>>>> getBoostAmounts() {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostAmounts(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The badge configuration for signal boost. Expect for right now only a single level numbered 1.
|
||||
*/
|
||||
public Single<ServiceResponse<SignalServiceProfile.Badge>> getBoostBadge() {
|
||||
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostLevels().getLevels().get("1").getBadge(), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,8 @@ import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
|
|||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Encapsulates a parsed APi response regardless of where it came from (WebSocket or REST). Not only
|
||||
* includes the success result but also any application errors encountered (404s, parsing, etc.) or
|
||||
|
@ -68,6 +70,18 @@ public final class ServiceResponse<Result> {
|
|||
return executionError;
|
||||
}
|
||||
|
||||
public Single<Result> flattenResult() {
|
||||
if (result.isPresent()) {
|
||||
return Single.just(result.get());
|
||||
} else if (applicationError.isPresent()) {
|
||||
return Single.error(applicationError.get());
|
||||
} else if (executionError.isPresent()) {
|
||||
return Single.error(executionError.get());
|
||||
} else {
|
||||
return Single.error(new AssertionError("Should never get here."));
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> ServiceResponse<T> forResult(T result, WebsocketResponse response) {
|
||||
return new ServiceResponse<>(result, response);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
class BoostReceiptCredentialRequestJson {
|
||||
@JsonProperty("paymentIntentId")
|
||||
private final String paymentIntentId;
|
||||
|
||||
@JsonProperty("receiptCredentialRequest")
|
||||
private final String receiptCredentialRequest;
|
||||
|
||||
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize());
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ package org.whispersystems.signalservice.internal.push;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.MessageLite;
|
||||
|
||||
|
@ -133,6 +134,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -242,7 +244,6 @@ public class PushServiceSocket {
|
|||
private static final String SUBMIT_RATE_LIMIT_CHALLENGE = "/v1/challenge";
|
||||
private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push";
|
||||
|
||||
private static final String DONATION_INTENT = "/v1/donation/authorize-apple-pay";
|
||||
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
|
||||
|
||||
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
|
||||
|
@ -251,6 +252,10 @@ public class PushServiceSocket {
|
|||
private static final String CREATE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
|
||||
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
|
||||
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
|
||||
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
|
||||
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
|
||||
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
|
||||
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
|
||||
|
||||
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
|
||||
|
||||
|
@ -870,15 +875,42 @@ public class PushServiceSocket {
|
|||
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The PaymentIntent id
|
||||
*/
|
||||
public DonationIntentResult createDonationIntentWithAmount(String amount, String currencyCode) throws IOException {
|
||||
String payload = JsonUtil.toJson(new DonationIntentPayload(Long.parseLong(amount), currencyCode.toLowerCase(Locale.ROOT)));
|
||||
String result = makeServiceRequest(DONATION_INTENT, "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, DonationIntentResult.class);
|
||||
public SubscriptionClientSecret createBoostPaymentMethod(String currencyCode, long amount) throws IOException {
|
||||
String payload = JsonUtil.toJson(new DonationIntentPayload(amount, currencyCode));
|
||||
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, SubscriptionClientSecret.class);
|
||||
}
|
||||
|
||||
public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
|
||||
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {};
|
||||
return JsonUtil.fromJsonResponse(result, typeRef);
|
||||
}
|
||||
|
||||
public SubscriptionLevels getBoostLevels() throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(BOOST_BADGES, "GET", null);
|
||||
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
|
||||
}
|
||||
|
||||
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
|
||||
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest));
|
||||
String response = makeServiceRequestWithoutAuthentication(
|
||||
BOOST_RECEIPT_CREDENTIALS,
|
||||
"POST",
|
||||
payload,
|
||||
(code, body) -> {
|
||||
if (code == 204) throw new NonSuccessfulResponseCodeException(204);
|
||||
});
|
||||
|
||||
ReceiptCredentialResponseJson responseJson = JsonUtil.fromJson(response, ReceiptCredentialResponseJson.class);
|
||||
if (responseJson.getReceiptCredentialResponse() != null) {
|
||||
return responseJson.getReceiptCredentialResponse();
|
||||
} else {
|
||||
throw new MalformedResponseException("Unable to parse response");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public SubscriptionLevels getSubscriptionLevels() throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(SUBSCRIPTION_LEVELS, "GET", null);
|
||||
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
|
||||
|
|
|
@ -10,6 +10,7 @@ package org.whispersystems.signalservice.internal.util;
|
|||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
|
@ -52,6 +53,22 @@ public class JsonUtil {
|
|||
return objectMapper.readValue(json, clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String json, TypeReference<T> typeRef)
|
||||
throws IOException
|
||||
{
|
||||
return objectMapper.readValue(json, typeRef);
|
||||
}
|
||||
|
||||
public static <T> T fromJsonResponse(String json, TypeReference<T> typeRef)
|
||||
throws MalformedResponseException
|
||||
{
|
||||
try {
|
||||
return JsonUtil.fromJson(json, typeRef);
|
||||
} catch (IOException e) {
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T fromJsonResponse(String body, Class<T> clazz)
|
||||
throws MalformedResponseException {
|
||||
try {
|
||||
|
|
Loading…
Add table
Reference in a new issue