mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-01 14:16:49 +00:00
Add google play billing token conversion endpoint and job.
This commit is contained in:
committed by
Greyson Parrelli
parent
d23ef647d8
commit
12e25b0f40
@@ -18,5 +18,6 @@ data class MessageBackupsFlowState(
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||
val startScreen: MessageBackupsStage,
|
||||
val stage: MessageBackupsStage = startScreen,
|
||||
val backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
val backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
|
||||
val failure: Throwable? = null
|
||||
)
|
||||
|
||||
@@ -8,10 +8,16 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
@@ -19,15 +25,21 @@ import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPurchaseTokenJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.math.BigDecimal
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
|
||||
@@ -45,6 +57,17 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
check(SignalStore.backup.backupTier != MessageBackupTier.PAID) { "This screen does not support cancellation or downgrades." }
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
ensureSubscriberIdForBackups()
|
||||
} catch (e: Exception) {
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
stage = MessageBackupsStage.FAILURE,
|
||||
failure = e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
|
||||
@@ -55,12 +78,30 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
AppDependencies.billingApi.getBillingPurchaseResults().collect {
|
||||
when (it) {
|
||||
AppDependencies.billingApi.getBillingPurchaseResults().collect { result ->
|
||||
when (result) {
|
||||
is BillingPurchaseResult.Success -> {
|
||||
// 1. Copy the purchaseToken into our inAppPaymentData
|
||||
// 2. Enqueue the redemption chain
|
||||
goToNextStage()
|
||||
internalStateFlow.update { it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT) }
|
||||
|
||||
try {
|
||||
handleSuccess(
|
||||
result,
|
||||
internalStateFlow.value.inAppPayment!!.id
|
||||
)
|
||||
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
stage = MessageBackupsStage.COMPLETED
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
stage = MessageBackupsStage.FAILURE,
|
||||
failure = e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> goToPreviousStage()
|
||||
@@ -81,9 +122,10 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it)
|
||||
MessageBackupsStage.CHECKOUT_SHEET -> validateGatewayAndUpdateState(it)
|
||||
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> error("This is driven by an async coroutine.")
|
||||
MessageBackupsStage.PROCESS_PAYMENT -> it.copy(stage = MessageBackupsStage.COMPLETED)
|
||||
MessageBackupsStage.PROCESS_FREE -> it.copy(stage = MessageBackupsStage.COMPLETED)
|
||||
MessageBackupsStage.PROCESS_PAYMENT -> error("This is driven by an async coroutine.")
|
||||
MessageBackupsStage.PROCESS_FREE -> error("This is driven by an async coroutine.")
|
||||
MessageBackupsStage.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
MessageBackupsStage.FAILURE -> error("Unsupported state transition from terminal state FAILURE")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,6 +145,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
MessageBackupsStage.PROCESS_PAYMENT -> MessageBackupsStage.PROCESS_PAYMENT
|
||||
MessageBackupsStage.PROCESS_FREE -> MessageBackupsStage.PROCESS_FREE
|
||||
MessageBackupsStage.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
MessageBackupsStage.FAILURE -> error("Unsupported state transition from terminal state FAILURE")
|
||||
}
|
||||
|
||||
it.copy(stage = previousScreen)
|
||||
@@ -146,7 +189,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_BACKUP,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
badge = null,
|
||||
@@ -170,4 +213,51 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
|
||||
return state.copy(stage = MessageBackupsStage.CREATING_IN_APP_PAYMENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures we have a SubscriberId created and available for use. This is considered safe because
|
||||
* the screen this is called in is assumed to only be accessible if the user does not currently have
|
||||
* a subscription.
|
||||
*/
|
||||
private suspend fun ensureSubscriberIdForBackups() {
|
||||
val product = AppDependencies.billingApi.queryProduct() ?: error("No product available.")
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(product.price.currency, InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP).blockingAwait()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a successful BillingPurchaseResult. Updates the in app payment, enqueues the appropriate job chain,
|
||||
* and handles any resulting error. Like donations, we will wait up to 10s for the completion of the job chain.
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = inAppPayment.data.redemption!!.copy(
|
||||
googlePlayBillingPurchaseToken = result.purchaseToken
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue()
|
||||
}
|
||||
|
||||
val terminalInAppPayment = withContext(Dispatchers.IO) {
|
||||
InAppPaymentsRepository.observeUpdates(inAppPaymentId).asFlow()
|
||||
.filter { it.state == InAppPaymentTable.State.END }
|
||||
.take(1)
|
||||
.timeout(10.seconds)
|
||||
.first()
|
||||
}
|
||||
|
||||
if (terminalInAppPayment.data.error != null) {
|
||||
throw InAppPaymentError(terminalInAppPayment.data.error)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ enum class MessageBackupsStage(
|
||||
CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION),
|
||||
PROCESS_PAYMENT(route = Route.TYPE_SELECTION),
|
||||
PROCESS_FREE(route = Route.TYPE_SELECTION),
|
||||
COMPLETED(route = Route.TYPE_SELECTION);
|
||||
COMPLETED(route = Route.TYPE_SELECTION),
|
||||
FAILURE(route = Route.TYPE_SELECTION);
|
||||
|
||||
/**
|
||||
* Compose navigation route to display while in a given stage.
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentProcessorError
|
||||
import java.security.SecureRandom
|
||||
import java.util.Currency
|
||||
import java.util.Optional
|
||||
@@ -95,7 +95,7 @@ object InAppPaymentsRepository {
|
||||
|
||||
val donationError: DonationError = when (error) {
|
||||
is DonationError -> error
|
||||
is DonationProcessorError -> error.toDonationError(donationErrorSource, paymentSourceType)
|
||||
is InAppPaymentProcessorError -> error.toDonationError(donationErrorSource, paymentSourceType)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(donationErrorSource)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentProcessorError
|
||||
|
||||
class StripePaymentInProgressViewModel(
|
||||
private val stripeRepository: StripeRepository,
|
||||
@@ -175,7 +175,7 @@ class StripePaymentInProgressViewModel(
|
||||
.onErrorResumeNext {
|
||||
when (it) {
|
||||
is DonationError -> Completable.error(it)
|
||||
is DonationProcessorError -> Completable.error(it.toDonationError(DonationErrorSource.MONTHLY, paymentSourceProvider.paymentSourceType))
|
||||
is InAppPaymentProcessorError -> Completable.error(it.toDonationError(DonationErrorSource.MONTHLY, paymentSourceProvider.paymentSourceType))
|
||||
else -> Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.MONTHLY, it, paymentSourceProvider.paymentSourceType))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentProcessorError
|
||||
|
||||
fun DonationProcessorError.toDonationError(
|
||||
fun InAppPaymentProcessorError.toDonationError(
|
||||
source: DonationErrorSource,
|
||||
method: PaymentSourceType
|
||||
): DonationError {
|
||||
@@ -44,5 +44,9 @@ fun DonationProcessorError.toDonationError(
|
||||
}
|
||||
}
|
||||
}
|
||||
ActiveSubscription.Processor.GOOGLE_PLAY_BILLING -> {
|
||||
check(method is PaymentSourceType.GooglePlayBilling)
|
||||
DonationError.PaymentSetupError.GenericError(source, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationReceiptCredentialError;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentReceiptCredentialError;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -325,8 +325,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
Log.w(TAG, "User payment failed.", applicationException, true);
|
||||
DonationError.routeBackgroundError(context, DonationError.genericPaymentFailure(donationErrorSource), terminalDonation.isLongRunningPaymentMethod);
|
||||
|
||||
if (applicationException instanceof DonationReceiptCredentialError) {
|
||||
setPendingOneTimeDonationChargeFailureError(((DonationReceiptCredentialError) applicationException).getChargeFailure());
|
||||
if (applicationException instanceof InAppPaymentReceiptCredentialError) {
|
||||
setPendingOneTimeDonationChargeFailureError(((InAppPaymentReceiptCredentialError) applicationException).getChargeFailure());
|
||||
} else {
|
||||
setPendingOneTimeDonationGenericRedemptionError(response.getStatus());
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationReceiptCredentialError
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentReceiptCredentialError
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -238,7 +238,7 @@ class InAppPaymentOneTimeContextJob private constructor(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(
|
||||
error = InAppPaymentsRepository.buildPaymentFailure(inAppPayment, (applicationError as? DonationReceiptCredentialError)?.chargeFailure)
|
||||
error = InAppPaymentsRepository.buildPaymentFailure(inAppPayment, (applicationError as? InAppPaymentReceiptCredentialError)?.chargeFailure)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import okio.IOException
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
|
||||
/**
|
||||
* Submits a purchase token to the server to link it with a subscriber id.
|
||||
*/
|
||||
class InAppPaymentPurchaseTokenJob private constructor(
|
||||
private val inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
parameters: Parameters
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(InAppPaymentPurchaseTokenJob::class)
|
||||
|
||||
const val KEY = "InAppPaymentPurchaseTokenJob"
|
||||
|
||||
private fun create(inAppPayment: InAppPaymentTable.InAppPayment): Job {
|
||||
return InAppPaymentPurchaseTokenJob(
|
||||
inAppPaymentId = inAppPayment.id,
|
||||
parameters = Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment))
|
||||
.setLifespan(InAppPaymentsRepository.resolveContextJobLifespan(inAppPayment).inWholeMilliseconds)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun createJobChain(inAppPayment: InAppPaymentTable.InAppPayment): Chain {
|
||||
return AppDependencies.jobManager
|
||||
.startChain(create(inAppPayment))
|
||||
.then(InAppPaymentRecurringContextJob.create(inAppPayment))
|
||||
.then(InAppPaymentRedemptionJob.create(inAppPayment))
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray = inAppPaymentId.serialize().toByteArray()
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun onFailure() {
|
||||
warning("A permanent failure occurred.")
|
||||
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||
if (inAppPayment != null && inAppPayment.data.error == null) {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.REDEMPTION
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRun() {
|
||||
synchronized(InAppPaymentsRepository.resolveMutex(inAppPaymentId)) {
|
||||
doRun()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doRun() {
|
||||
val inAppPayment = getAndValidateInAppPayment()
|
||||
|
||||
val response = AppDependencies.donationsService.linkGooglePlayBillingPurchaseTokenToSubscriberId(
|
||||
inAppPayment.subscriberId!!,
|
||||
inAppPayment.data.redemption!!.googlePlayBillingPurchaseToken!!,
|
||||
InAppPaymentSubscriberRecord.Type.BACKUP
|
||||
)
|
||||
|
||||
if (response.applicationError.isPresent) {
|
||||
handleApplicationError(response.applicationError.get(), response.status)
|
||||
} else if (response.result.isPresent) {
|
||||
info("Successfully linked purchase token to subscriber id.")
|
||||
} else {
|
||||
warning("Encountered a retryable exception.", response.executionError.get())
|
||||
throw InAppPaymentRetryException(response.executionError.get())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAndValidateInAppPayment(): InAppPaymentTable.InAppPayment {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||
if (inAppPayment == null) {
|
||||
warning("Not found")
|
||||
throw IOException("InAppPayment for given ID not found.")
|
||||
}
|
||||
|
||||
if (inAppPayment.state != InAppPaymentTable.State.PENDING) {
|
||||
warning("Unexpected state. Got ${inAppPayment.state} but expected PENDING")
|
||||
throw IOException("InAppPayment in unexpected state.")
|
||||
}
|
||||
|
||||
if (inAppPayment.type != InAppPaymentType.RECURRING_BACKUP) {
|
||||
warning("Unexpected type. Got ${inAppPayment.type} but expected a recurring backup.")
|
||||
throw IOException("InAppPayment is an unexpected type.")
|
||||
}
|
||||
|
||||
if (inAppPayment.subscriberId == null) {
|
||||
warning("Expected a subscriber id.")
|
||||
throw IOException("InAppPayment is missing its subscriber id")
|
||||
}
|
||||
|
||||
if (inAppPayment.data.redemption == null) {
|
||||
warning("Expected redemption state.")
|
||||
throw IOException("InAppPayment has no redemption state. Waiting for authorization?")
|
||||
}
|
||||
|
||||
if (inAppPayment.data.redemption.stage == InAppPaymentData.RedemptionState.Stage.REDEMPTION_STARTED || inAppPayment.data.redemption.stage == InAppPaymentData.RedemptionState.Stage.REDEEMED) {
|
||||
warning("Already began redemption.")
|
||||
throw IOException("InAppPayment has already started redemption.")
|
||||
}
|
||||
|
||||
if (inAppPayment.data.redemption.googlePlayBillingPurchaseToken == null) {
|
||||
warning("No purchase token for linking!")
|
||||
throw IOException("InAppPayment does not have a purchase token!")
|
||||
}
|
||||
|
||||
return inAppPayment
|
||||
}
|
||||
|
||||
private fun handleApplicationError(applicationError: Throwable, status: Int) {
|
||||
when (status) {
|
||||
402 -> {
|
||||
warning("The purchaseToken payment is incomplete or invalid.", applicationError)
|
||||
// TODO [message-backups] -- Is this a recoverable failure?
|
||||
throw IOException("TODO -- recoverable?")
|
||||
}
|
||||
|
||||
403 -> {
|
||||
warning("subscriberId authentication failure OR account authentication is present", applicationError)
|
||||
throw IOException("subscriberId authentication failure OR account authentication is present")
|
||||
}
|
||||
|
||||
404 -> {
|
||||
warning("No such subscriberId exists or subscriberId is malformed or the purchaseToken does not exist", applicationError)
|
||||
throw IOException("No such subscriberId exists or subscriberId is malformed or the purchaseToken does not exist")
|
||||
}
|
||||
|
||||
409 -> {
|
||||
warning("subscriberId is already linked to a processor that does not support Play Billing. Delete this subscriberId and use a new one.", applicationError)
|
||||
|
||||
try {
|
||||
info("Generating a new subscriber id.")
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, true).blockingAwait()
|
||||
} catch (e: Exception) {
|
||||
throw InAppPaymentRetryException(e)
|
||||
}
|
||||
|
||||
info("Writing the new subscriber id to the InAppPayment.")
|
||||
val latest = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
SignalDatabase.inAppPayments.update(
|
||||
latest.copy(subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId)
|
||||
)
|
||||
|
||||
info("Scheduling retry.")
|
||||
throw InAppPaymentRetryException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean = e is InAppPaymentRetryException
|
||||
|
||||
private fun info(message: String, throwable: Throwable? = null) {
|
||||
Log.i(TAG, "InAppPayment[$inAppPaymentId]: $message", throwable, true)
|
||||
}
|
||||
|
||||
private fun warning(message: String, throwable: Throwable? = null) {
|
||||
Log.w(TAG, "InAppPayment[$inAppPaymentId]: $message", throwable, true)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<InAppPaymentPurchaseTokenJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): InAppPaymentPurchaseTokenJob {
|
||||
return InAppPaymentPurchaseTokenJob(
|
||||
InAppPaymentTable.InAppPaymentId(serializedData!!.decodeToString().toLong()),
|
||||
parameters
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class InAppPaymentRecurringContextJob private constructor(
|
||||
|
||||
const val KEY = "InAppPurchaseRecurringContextJob"
|
||||
|
||||
private fun create(inAppPayment: InAppPaymentTable.InAppPayment): Job {
|
||||
fun create(inAppPayment: InAppPaymentTable.InAppPayment): Job {
|
||||
return InAppPaymentRecurringContextJob(
|
||||
inAppPaymentId = inAppPayment.id,
|
||||
parameters = Parameters.Builder()
|
||||
|
||||
@@ -155,6 +155,7 @@ public final class JobManagerFactories {
|
||||
put(InAppPaymentAuthCheckJob.KEY, new InAppPaymentAuthCheckJob.Factory());
|
||||
put(InAppPaymentGiftSendJob.KEY, new InAppPaymentGiftSendJob.Factory());
|
||||
put(InAppPaymentKeepAliveJob.KEY, new InAppPaymentKeepAliveJob.Factory());
|
||||
put(InAppPaymentPurchaseTokenJob.KEY, new InAppPaymentPurchaseTokenJob.Factory());
|
||||
put(InAppPaymentRecurringContextJob.KEY, new InAppPaymentRecurringContextJob.Factory());
|
||||
put(InAppPaymentOneTimeContextJob.KEY, new InAppPaymentOneTimeContextJob.Factory());
|
||||
put(InAppPaymentRedemptionJob.KEY, new InAppPaymentRedemptionJob.Factory());
|
||||
|
||||
Reference in New Issue
Block a user