diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt index 32b093d580..32b71bc3f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt @@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.errors.toDonationError import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobStatus -import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation import org.thoughtcrime.securesms.database.DatabaseObserver.InAppPaymentObserver import org.thoughtcrime.securesms.database.InAppPaymentTable @@ -522,21 +521,13 @@ object InAppPaymentsRepository { */ @WorkerThread fun hasPendingDonation(): Boolean { - return SignalDatabase.inAppPayments.hasPendingDonation() || DonationRedemptionJobWatcher.hasPendingRedemptionJob() + return SignalDatabase.inAppPayments.hasPendingDonation() } /** * Emits a stream of status updates for donations of the given type. Only One-time donations and recurring donations are currently supported. */ fun observeInAppPaymentRedemption(type: InAppPaymentType): Observable { - val jobStatusObservable: Observable = when (type) { - InAppPaymentType.UNKNOWN -> Observable.empty() - InAppPaymentType.ONE_TIME_GIFT -> Observable.empty() - InAppPaymentType.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption() - InAppPaymentType.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption() - InAppPaymentType.RECURRING_BACKUP -> Observable.empty() - } - val fromDatabase: Observable = Observable.create { emitter -> val observer = InAppPaymentObserver { val latestInAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(type) @@ -547,7 +538,7 @@ object InAppPaymentsRepository { AppDependencies.databaseObserver.registerInAppPaymentObserver(observer) emitter.setCancellable { AppDependencies.databaseObserver.unregisterObserver(observer) } }.switchMap { inAppPaymentOptional -> - val inAppPayment = inAppPaymentOptional.getOrNull() ?: return@switchMap jobStatusObservable + val inAppPayment = inAppPaymentOptional.getOrNull() ?: return@switchMap Observable.just(DonationRedemptionJobStatus.None) val value = when (inAppPayment.state) { InAppPaymentTable.State.CREATED -> error("This should have been filtered out.") @@ -576,15 +567,7 @@ object InAppPaymentsRepository { Observable.just(value) } - return fromDatabase - .switchMap { - if (it == DonationRedemptionJobStatus.None) { - jobStatusObservable - } else { - Observable.just(it) - } - } - .distinctUntilChanged() + return fromDatabase.distinctUntilChanged() } fun scheduleSyncForAccountRecordChange() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt deleted file mode 100644 index 9a59c4d3ad..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.subscription.manage - -import androidx.annotation.WorkerThread -import io.reactivex.rxjava3.core.Observable -import org.thoughtcrime.securesms.badges.Badges -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney -import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData -import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec -import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob -import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob -import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob -import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob -import org.thoughtcrime.securesms.keyvalue.SignalStore -import java.util.concurrent.TimeUnit - -/** - * Allows observer to poll for the status of the latest pending, running, or completed redemption job for subscriptions or one time payments. - * - * @deprecated This object is deprecated and will be removed once we are sure all jobs have drained. - */ -object DonationRedemptionJobWatcher { - - enum class RedemptionType { - SUBSCRIPTION, - ONE_TIME - } - - @WorkerThread - fun hasPendingRedemptionJob(): Boolean { - return getDonationRedemptionJobStatus(RedemptionType.SUBSCRIPTION).isInProgress() || getDonationRedemptionJobStatus(RedemptionType.ONE_TIME).isInProgress() - } - - fun watchSubscriptionRedemption(): Observable = watch(RedemptionType.SUBSCRIPTION) - - @JvmStatic - @WorkerThread - fun getSubscriptionRedemptionJobStatus(): DonationRedemptionJobStatus { - return getDonationRedemptionJobStatus(RedemptionType.SUBSCRIPTION) - } - - fun watchOneTimeRedemption(): Observable = watch(RedemptionType.ONE_TIME) - - private fun watch(redemptionType: RedemptionType): Observable { - return Observable - .interval(0, 5, TimeUnit.SECONDS) - .map { - getDonationRedemptionJobStatus(redemptionType) - } - .distinctUntilChanged() - } - - private fun getDonationRedemptionJobStatus(redemptionType: RedemptionType): DonationRedemptionJobStatus { - val queue = when (redemptionType) { - RedemptionType.SUBSCRIPTION -> DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE - RedemptionType.ONE_TIME -> DonationReceiptRedemptionJob.ONE_TIME_QUEUE - } - - val donationJobSpecs = AppDependencies - .jobManager - .find { it.queueKey?.startsWith(queue) == true } - .sortedBy { it.createTime } - - val externalLaunchJobSpec: JobSpec? = donationJobSpecs.firstOrNull { - it.factoryKey == ExternalLaunchDonationJob.KEY - } - - val receiptRequestJobKey = when (redemptionType) { - RedemptionType.SUBSCRIPTION -> SubscriptionReceiptRequestResponseJob.KEY - RedemptionType.ONE_TIME -> BoostReceiptRequestResponseJob.KEY - } - - val receiptJobSpec: JobSpec? = donationJobSpecs.firstOrNull { - it.factoryKey == receiptRequestJobKey - } - - val redemptionJobSpec: JobSpec? = donationJobSpecs.firstOrNull { - it.factoryKey == DonationReceiptRedemptionJob.KEY - } - - val jobSpec: JobSpec? = externalLaunchJobSpec ?: redemptionJobSpec ?: receiptJobSpec - - return if (redemptionType == RedemptionType.SUBSCRIPTION && jobSpec == null && SignalStore.inAppPayments.getSubscriptionRedemptionFailed()) { - DonationRedemptionJobStatus.FailedSubscription - } else { - jobSpec?.toDonationRedemptionStatus(redemptionType) ?: DonationRedemptionJobStatus.None - } - } - - private fun JobSpec.toDonationRedemptionStatus(redemptionType: RedemptionType): DonationRedemptionJobStatus { - return when (factoryKey) { - ExternalLaunchDonationJob.KEY -> { - val stripe3DSData = ExternalLaunchDonationJob.Factory.parseSerializedData(serializedData!!) - DonationRedemptionJobStatus.PendingExternalVerification( - pendingOneTimeDonation = pendingOneTimeDonation(redemptionType, stripe3DSData), - nonVerifiedMonthlyDonation = nonVerifiedMonthlyDonation(redemptionType, stripe3DSData) - ) - } - - SubscriptionReceiptRequestResponseJob.KEY, - BoostReceiptRequestResponseJob.KEY -> DonationRedemptionJobStatus.PendingReceiptRequest - - DonationReceiptRedemptionJob.KEY -> DonationRedemptionJobStatus.PendingReceiptRedemption - - else -> { - DonationRedemptionJobStatus.None - } - } - } - - private fun JobSpec.pendingOneTimeDonation(redemptionType: RedemptionType, stripe3DSData: Stripe3DSData): PendingOneTimeDonation? { - if (redemptionType != RedemptionType.ONE_TIME) { - return null - } - - return DonationSerializationHelper.createPendingOneTimeDonationProto( - badge = Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!), - paymentSourceType = stripe3DSData.paymentSourceType, - amount = stripe3DSData.inAppPayment.data.amount!!.toFiatMoney() - ).copy( - timestamp = createTime, - pendingVerification = true, - checkedVerification = runAttempt > 0 - ) - } - - private fun JobSpec.nonVerifiedMonthlyDonation(redemptionType: RedemptionType, stripe3DSData: Stripe3DSData): NonVerifiedMonthlyDonation? { - if (redemptionType != RedemptionType.SUBSCRIPTION) { - return null - } - - return NonVerifiedMonthlyDonation( - timestamp = createTime, - price = stripe3DSData.inAppPayment.data.amount!!.toFiatMoney(), - level = stripe3DSData.inAppPayment.data.level.toInt(), - checkedVerification = runAttempt > 0 - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java deleted file mode 100644 index abef5c8b2b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import org.signal.core.util.logging.Log; -import org.signal.donations.StripeDeclineCode; -import org.signal.donations.StripeFailureCode; -import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.VerificationFailedException; -import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredential; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; -import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; -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.model.databaseprotos.DonationErrorValue; -import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.recipients.RecipientId; -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.InAppPaymentReceiptCredentialError; - -import java.io.IOException; -import java.security.SecureRandom; -import java.util.concurrent.TimeUnit; - -import okio.ByteString; - -/** - * Job responsible for submitting ReceiptCredentialRequest objects to the server until - * we get a response. - * - * @deprecated Replaced with InAppPaymentOneTimeContextJob - */ -@Deprecated -public class BoostReceiptRequestResponseJob extends BaseJob { - - private static final String TAG = Log.tag(BoostReceiptRequestResponseJob.class); - - public static final String KEY = "BoostReceiptCredentialsSubmissionJob"; - - private static final String BOOST_QUEUE = "BoostReceiptRedemption"; - private static final String GIFT_QUEUE = "GiftReceiptRedemption"; - private static final String LONG_RUNNING_SUFFIX = "__LongRunning"; - - private static final String DATA_REQUEST_BYTES = "data.request.bytes"; - private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id"; - private static final String DATA_ERROR_SOURCE = "data.error.source"; - private static final String DATA_BADGE_LEVEL = "data.badge.level"; - private static final String DATA_DONATION_PROCESSOR = "data.donation.processor"; - private static final String DATA_UI_SESSION_KEY = "data.ui.session.key"; - private static final String DATA_TERMINAL_DONATION = "data.terminal.donation"; - - private ReceiptCredentialRequestContext requestContext; - private TerminalDonationQueue.TerminalDonation terminalDonation; - - - private final DonationErrorSource donationErrorSource; - private final String paymentIntentId; - private final long badgeLevel; - private final DonationProcessor donationProcessor; - private final long uiSessionKey; - - private static String resolveQueue(DonationErrorSource donationErrorSource, boolean isLongRunning) { - String baseQueue = donationErrorSource == DonationErrorSource.ONE_TIME ? BOOST_QUEUE : GIFT_QUEUE; - return isLongRunning ? baseQueue + LONG_RUNNING_SUFFIX : baseQueue; - } - - private static long resolveLifespan(boolean isLongRunning) { - return isLongRunning ? TimeUnit.DAYS.toMillis(30) : TimeUnit.DAYS.toMillis(1); - } - - private static BoostReceiptRequestResponseJob createJob(@NonNull String paymentIntentId, - @NonNull DonationErrorSource donationErrorSource, - long badgeLevel, - @NonNull DonationProcessor donationProcessor, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - return new BoostReceiptRequestResponseJob( - new Parameters - .Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(resolveQueue(donationErrorSource, terminalDonation.isLongRunningPaymentMethod)) - .setLifespan(resolveLifespan(terminalDonation.isLongRunningPaymentMethod)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - null, - paymentIntentId, - donationErrorSource, - badgeLevel, - donationProcessor, - uiSessionKey, - terminalDonation - ); - } - - public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId, - @NonNull DonationProcessor donationProcessor, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.ONE_TIME, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, terminalDonation); - DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey, terminalDonation.isLongRunningPaymentMethod); - RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost(); - MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); - - return AppDependencies.getJobManager() - .startChain(requestReceiptJob) - .then(redeemReceiptJob) - .then(refreshOwnProfileJob) - .then(multiDeviceProfileContentUpdateJob); - } - - public static JobManager.Chain createJobChainForGift(@NonNull String paymentIntentId, - @NonNull RecipientId recipientId, - @Nullable String additionalMessage, - long badgeLevel, - @NonNull DonationProcessor donationProcessor, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey, terminalDonation); - GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage); - - - return AppDependencies.getJobManager() - .startChain(requestReceiptJob) - .then(giftSendJob); - } - - private BoostReceiptRequestResponseJob(@NonNull Parameters parameters, - @Nullable ReceiptCredentialRequestContext requestContext, - @NonNull String paymentIntentId, - @NonNull DonationErrorSource donationErrorSource, - long badgeLevel, - @NonNull DonationProcessor donationProcessor, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - super(parameters); - this.requestContext = requestContext; - this.paymentIntentId = paymentIntentId; - this.donationErrorSource = donationErrorSource; - this.badgeLevel = badgeLevel; - this.donationProcessor = donationProcessor; - this.uiSessionKey = uiSessionKey; - this.terminalDonation = terminalDonation; - } - - @Override - public @Nullable byte[] serialize() { - JsonJobData.Builder builder = new JsonJobData.Builder().putString(DATA_PAYMENT_INTENT_ID, paymentIntentId) - .putString(DATA_ERROR_SOURCE, donationErrorSource.serialize()) - .putLong(DATA_BADGE_LEVEL, badgeLevel) - .putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode()) - .putLong(DATA_UI_SESSION_KEY, uiSessionKey) - .putBlobAsString(DATA_TERMINAL_DONATION, terminalDonation.encode()); - - if (requestContext != null) { - builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize()); - } - - return builder.serialize(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - if (terminalDonation.error != null) { - SignalStore.inAppPayments().appendToTerminalDonationQueue(terminalDonation); - } else { - Log.w(TAG, "Job is in terminal state without an error on TerminalDonation."); - } - } - - @Override - public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) { - if (terminalDonation.isLongRunningPaymentMethod) { - return TimeUnit.DAYS.toMillis(1); - } else { - return super.getNextRunAttemptBackoff(pastAttemptCount, exception); - } - } - - @Override - protected void onRun() throws Exception { - if (requestContext == null) { - Log.d(TAG, "Creating request context.."); - - SecureRandom secureRandom = new SecureRandom(); - byte[] randomBytes = new byte[ReceiptSerial.SIZE]; - - secureRandom.nextBytes(randomBytes); - - ReceiptSerial receiptSerial = new ReceiptSerial(randomBytes); - ClientZkReceiptOperations operations = AppDependencies.getClientZkReceiptOperations(); - - requestContext = operations.createReceiptCredentialRequestContext(secureRandom, receiptSerial); - } else { - Log.d(TAG, "Reusing request context from previous run", true); - } - - Log.d(TAG, "Submitting credential to server", true); - ServiceResponse response = AppDependencies.getDonationsService() - .submitBoostReceiptCredentialRequestSync(paymentIntentId, requestContext.getRequest(), donationProcessor); - - if (response.getApplicationError().isPresent()) { - handleApplicationError(context, response, donationErrorSource); - } else if (response.getResult().isPresent()) { - ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get()); - - if (!isCredentialValid(receiptCredential)) { - DonationError.routeBackgroundError(context, DonationError.badgeCredentialVerificationFailure(donationErrorSource)); - setPendingOneTimeDonationGenericRedemptionError(-1); - throw new IOException("Could not validate receipt credential"); - } - - Log.d(TAG, "Validated credential. Handing off to next job.", true); - ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential); - setOutputData(new JsonJobData.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION, - receiptCredentialPresentation.serialize()) - .putBlobAsString(DonationReceiptRedemptionJob.INPUT_TERMINAL_DONATION, terminalDonation.encode()) - .serialize()); - - if (donationErrorSource == DonationErrorSource.GIFT) { - SignalStore.inAppPayments().setPendingOneTimeDonation(null); - } - } else { - Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orElse(null), true); - throw new RetryableException(); - } - } - - /** - * Sets the pending one-time donation error according to the status code. - */ - private void setPendingOneTimeDonationGenericRedemptionError(int statusCode) { - DonationErrorValue donationErrorValue = new DonationErrorValue.Builder() - .type(statusCode == 402 - ? DonationErrorValue.Type.PAYMENT - : DonationErrorValue.Type.REDEMPTION) - .code(Integer.toString(statusCode)) - .build(); - - SignalStore.inAppPayments().setPendingOneTimeDonationError( - donationErrorValue - ); - - terminalDonation = terminalDonation.newBuilder() - .error(donationErrorValue) - .build(); - } - - /** - * Sets the pending one-time donation error according to the given charge failure. - */ - private void setPendingOneTimeDonationChargeFailureError(@NonNull ActiveSubscription.ChargeFailure chargeFailure) { - final DonationErrorValue.Type type; - final String code; - - if (donationProcessor == DonationProcessor.PAYPAL) { - code = chargeFailure.getCode(); - type = DonationErrorValue.Type.PROCESSOR_CODE; - } else { - StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason()); - StripeFailureCode failureCode = StripeFailureCode.Companion.getFromCode(chargeFailure.getCode()); - - if (failureCode.isKnown()) { - code = failureCode.toString(); - type = DonationErrorValue.Type.FAILURE_CODE; - } else if (declineCode.isKnown()) { - code = declineCode.toString(); - type = DonationErrorValue.Type.DECLINE_CODE; - } else { - code = chargeFailure.getCode(); - type = DonationErrorValue.Type.PROCESSOR_CODE; - } - } - - DonationErrorValue donationErrorValue = new DonationErrorValue.Builder() - .type(type) - .code(code) - .build(); - - SignalStore.inAppPayments().setPendingOneTimeDonationError( - donationErrorValue - ); - - terminalDonation = terminalDonation.newBuilder() - .error(donationErrorValue) - .build(); - } - - private void handleApplicationError(Context context, ServiceResponse response, @NonNull DonationErrorSource donationErrorSource) throws Exception { - Throwable applicationException = response.getApplicationError().get(); - switch (response.getStatus()) { - case 204: - Log.w(TAG, "User payment not be completed yet.", applicationException, true); - throw new RetryableException(); - case 400: - Log.w(TAG, "Receipt credential request failed to validate.", applicationException, true); - DonationError.routeBackgroundError(context, DonationError.genericBadgeRedemptionFailure(donationErrorSource)); - setPendingOneTimeDonationGenericRedemptionError(response.getStatus()); - throw new Exception(applicationException); - case 402: - Log.w(TAG, "User payment failed.", applicationException, true); - DonationError.routeBackgroundError(context, DonationError.genericPaymentFailure(donationErrorSource), terminalDonation.isLongRunningPaymentMethod); - - if (applicationException instanceof InAppPaymentReceiptCredentialError) { - setPendingOneTimeDonationChargeFailureError(((InAppPaymentReceiptCredentialError) applicationException).getChargeFailure()); - } else { - setPendingOneTimeDonationGenericRedemptionError(response.getStatus()); - } - - throw new Exception(applicationException); - case 409: - Log.w(TAG, "Receipt already redeemed with a different request credential.", response.getApplicationError().get(), true); - DonationError.routeBackgroundError(context, DonationError.genericBadgeRedemptionFailure(donationErrorSource)); - setPendingOneTimeDonationGenericRedemptionError(response.getStatus()); - throw new Exception(applicationException); - default: - Log.w(TAG, "Encountered a server failure: " + response.getStatus(), applicationException, true); - throw new RetryableException(); - } - } - - private ReceiptCredentialPresentation getReceiptCredentialPresentation(@NonNull ReceiptCredential receiptCredential) throws RetryableException { - ClientZkReceiptOperations operations = AppDependencies.getClientZkReceiptOperations(); - - try { - return operations.createReceiptCredentialPresentation(receiptCredential); - } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e, true); - requestContext = null; - throw new RetryableException(); - } - } - - private ReceiptCredential getReceiptCredential(@NonNull ReceiptCredentialResponse response) throws RetryableException { - ClientZkReceiptOperations operations = AppDependencies.getClientZkReceiptOperations(); - - try { - return operations.receiveReceiptCredential(requestContext, response); - } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e, true); - 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 90 days from now - */ - private boolean isCredentialValid(@NonNull ReceiptCredential receiptCredential) { - long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); - long maxExpirationTime = now + TimeUnit.DAYS.toSeconds(90); - boolean isCorrectLevel = receiptCredential.getReceiptLevel() == badgeLevel; - boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0; - boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now; - boolean isExpirationWithinMax = receiptCredential.getReceiptExpirationTime() <= maxExpirationTime; - - Log.d(TAG, "Credential validation: isCorrectLevel(" + isCorrectLevel + " actual: " + receiptCredential.getReceiptLevel() + ", expected: " + badgeLevel + - ") isExpiration86400(" + isExpiration86400 + - ") isExpirationInTheFuture(" + isExpirationInTheFuture + - ") isExpirationWithinMax(" + isExpirationWithinMax + ")", true); - - return isCorrectLevel && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinMax; - } - - @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 { - @Override - public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - - String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID); - DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.ONE_TIME.serialize())); - long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL)); - String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode()); - DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor); - long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L); - byte[] rawTerminalDonation = data.getStringAsBlob(DATA_TERMINAL_DONATION); - - TerminalDonationQueue.TerminalDonation terminalDonation = null; - if (rawTerminalDonation != null) { - try { - terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation); - } catch (IOException e) { - Log.e(TAG, "Failed to parse terminal donation. Generating a default."); - } - } - - if (terminalDonation == null) { - terminalDonation = new TerminalDonationQueue.TerminalDonation( - -1, - false, - null, - ByteString.EMPTY - ); - } - - 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, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, terminalDonation); - } else { - return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, terminalDonation); - } - } catch (InvalidInputException e) { - throw new IllegalStateException(e); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java deleted file mode 100644 index 4f1d01554b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ /dev/null @@ -1,358 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; -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.MessageTable; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue; -import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; -import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.MessageRecordUtil; -import org.whispersystems.signalservice.internal.EmptyResponse; -import org.whispersystems.signalservice.internal.ServiceResponse; - -import java.io.IOException; -import java.util.Collections; -import java.util.Objects; -import java.util.concurrent.locks.Lock; - -/** - * Job to redeem a verified donation receipt. It is up to the Job prior in the chain to specify a valid - * presentation object via setOutputData. This is expected to be the byte[] blob of a ReceiptCredentialPresentation object. - * - * @deprecated Replaced with InAppPaymentRedemptionJob - */ -@Deprecated -public class DonationReceiptRedemptionJob extends BaseJob { - private static final String TAG = Log.tag(DonationReceiptRedemptionJob.class); - private static final long NO_ID = -1L; - private static final int MAX_RETRIES = 1500; - - public static final String SUBSCRIPTION_QUEUE = "ReceiptRedemption"; - public static final String ONE_TIME_QUEUE = "BoostReceiptRedemption"; - public static final String KEY = "DonationReceiptRedemptionJob"; - - private static final String LONG_RUNNING_QUEUE_SUFFIX = "__LONG_RUNNING"; - - public static final String INPUT_RECEIPT_CREDENTIAL_PRESENTATION = "data.receipt.credential.presentation"; - public static final String INPUT_TERMINAL_DONATION = "data.terminal.donation"; - public static final String INPUT_KEEP_ALIVE_409 = "data.keep.alive.409"; - public static final String DATA_ERROR_SOURCE = "data.error.source"; - public static final String DATA_GIFT_MESSAGE_ID = "data.gift.message.id"; - public static final String DATA_PRIMARY = "data.primary"; - public static final String DATA_UI_SESSION_KEY = "data.ui.session.key"; - - private final long giftMessageId; - private final boolean makePrimary; - private final DonationErrorSource errorSource; - private final long uiSessionKey; - - private TerminalDonationQueue.TerminalDonation terminalDonation; - - public static DonationReceiptRedemptionJob createJobForSubscription(@NonNull DonationErrorSource errorSource, long uiSessionKey, boolean isLongRunningDonationPaymentType) { - return new DonationReceiptRedemptionJob( - NO_ID, - false, - errorSource, - uiSessionKey, - new Job.Parameters - .Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(SUBSCRIPTION_QUEUE + (isLongRunningDonationPaymentType ? LONG_RUNNING_QUEUE_SUFFIX : "")) - .setMaxAttempts(MAX_RETRIES) - .setLifespan(Parameters.IMMORTAL) - .build()); - } - - public static DonationReceiptRedemptionJob createJobForBoost(long uiSessionKey, boolean isLongRunningDonationPaymentType) { - return new DonationReceiptRedemptionJob( - NO_ID, - false, - DonationErrorSource.ONE_TIME, - uiSessionKey, - new Job.Parameters - .Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(ONE_TIME_QUEUE + (isLongRunningDonationPaymentType ? LONG_RUNNING_QUEUE_SUFFIX : "")) - .setMaxAttempts(MAX_RETRIES) - .setLifespan(Parameters.IMMORTAL) - .build()); - } - - public static JobManager.Chain createJobChainForKeepAlive() { - DonationReceiptRedemptionJob redemptionJob = createJobForSubscription(DonationErrorSource.KEEP_ALIVE, -1L, false); - RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob(); - MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); - - return AppDependencies.getJobManager() - .startChain(redemptionJob) - .then(refreshOwnProfileJob) - .then(multiDeviceProfileContentUpdateJob); - } - - private DonationReceiptRedemptionJob(long giftMessageId, boolean primary, @NonNull DonationErrorSource errorSource, long uiSessionKey, @NonNull Job.Parameters parameters) { - super(parameters); - this.giftMessageId = giftMessageId; - this.makePrimary = primary; - this.errorSource = errorSource; - this.uiSessionKey = uiSessionKey; - } - - @Override - public @Nullable byte[] serialize() { - return new JsonJobData.Builder() - .putString(DATA_ERROR_SOURCE, errorSource.serialize()) - .putLong(DATA_GIFT_MESSAGE_ID, giftMessageId) - .putBoolean(DATA_PRIMARY, makePrimary) - .putLong(DATA_UI_SESSION_KEY, uiSessionKey) - .serialize(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - if (getInputData() == null) { - Log.d(TAG, "No input data, assuming upstream job in chain failed and properly set error state. Failing without side effects."); - return; - } - - if (isForSubscription()) { - Log.d(TAG, "Marking subscription failure", true); - SignalStore.inAppPayments().markSubscriptionRedemptionFailed(); - MultiDeviceSubscriptionSyncRequestJob.enqueue(); - } else if (giftMessageId != NO_ID) { - SignalDatabase.messages().markGiftRedemptionFailed(giftMessageId); - } - - if (terminalDonation != null) { - SignalStore.inAppPayments().appendToTerminalDonationQueue(terminalDonation); - } - } - - @Override - public void onAdded() { - if (giftMessageId != NO_ID) { - SignalDatabase.messages().markGiftRedemptionStarted(giftMessageId); - } - } - - @Override - protected void onRun() throws Exception { - if (isForSubscription()) { - Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock(); - lock.lock(); - try { - doRun(); - } finally { - lock.unlock(); - } - } else { - doRun(); - } - } - - private void doRun() throws Exception { - JsonJobData inputData = getInputData() != null ? JsonJobData.deserialize(getInputData()) : null; - boolean isKeepAlive409 = inputData != null && inputData.getBooleanOrDefault(INPUT_KEEP_ALIVE_409, false); - - if (isKeepAlive409) { - Log.d(TAG, "Keep-Alive redemption job hit a 409. Exiting.", true); - return; - } - - ReceiptCredentialPresentation presentation = getPresentation(); - if (presentation == null) { - Log.d(TAG, "No presentation available. Exiting.", true); - return; - } - - byte[] rawTerminalDonation = inputData != null ? inputData.getStringAsBlob(INPUT_TERMINAL_DONATION) : null; - if (rawTerminalDonation != null) { - Log.d(TAG, "Retrieved terminal donation information from input data."); - terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation); - } else { - Log.d(TAG, "Input data does not contain terminal donation data. Creating one with sane defaults."); - terminalDonation = new TerminalDonationQueue.TerminalDonation.Builder() - .level(presentation.getReceiptLevel()) - .build(); - } - - Log.d(TAG, "Attempting to redeem token... isForSubscription: " + isForSubscription(), true); - ServiceResponse response = AppDependencies.getDonationsService() - .redeemDonationReceipt(presentation, - SignalStore.inAppPayments().getDisplayBadgesOnProfile(), - makePrimary); - - if (response.getApplicationError().isPresent()) { - if (response.getStatus() >= 500) { - Log.w(TAG, "Encountered a server exception " + response.getStatus(), response.getApplicationError().get(), true); - throw new RetryableException(); - } else { - Log.w(TAG, "Encountered a non-recoverable exception " + response.getStatus(), response.getApplicationError().get(), true); - DonationError.routeBackgroundError(context, DonationError.genericBadgeRedemptionFailure(errorSource)); - - if (isForOneTimeDonation()) { - DonationErrorValue donationErrorValue = new DonationErrorValue.Builder() - .type(DonationErrorValue.Type.REDEMPTION) - .code(Integer.toString(response.getStatus())) - .build(); - - SignalStore.inAppPayments().setPendingOneTimeDonationError( - donationErrorValue - ); - - terminalDonation = terminalDonation.newBuilder() - .error(donationErrorValue) - .build(); - } - - throw new IOException(response.getApplicationError().get()); - } - } else if (response.getExecutionError().isPresent()) { - Log.w(TAG, "Encountered a retryable exception", response.getExecutionError().get(), true); - throw new RetryableException(); - } - - Log.i(TAG, "Successfully redeemed token with response code " + response.getStatus() + "... isForSubscription: " + isForSubscription(), true); - enqueueDonationComplete(); - - if (isForSubscription()) { - Log.d(TAG, "Clearing subscription failure", true); - SignalStore.inAppPayments().clearSubscriptionRedemptionFailed(); - Log.i(TAG, "Recording end of period from active subscription", true); - SignalStore.inAppPayments() - .setSubscriptionEndOfPeriodRedeemed(SignalStore.inAppPayments() - .getSubscriptionEndOfPeriodRedemptionStarted()); - SignalStore.inAppPayments().clearSubscriptionReceiptCredential(); - } else if (giftMessageId != NO_ID) { - Log.d(TAG, "Marking gift redemption completed for " + giftMessageId); - SignalDatabase.messages().markGiftRedemptionCompleted(giftMessageId); - MessageTable.MarkedMessageInfo markedMessageInfo = SignalDatabase.messages().setIncomingMessageViewed(giftMessageId); - if (markedMessageInfo != null) { - Log.d(TAG, "Marked gift message viewed for " + giftMessageId); - MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId())); - } - } - - if (isForOneTimeDonation()) { - SignalStore.inAppPayments().setPendingOneTimeDonation(null); - } - } - - private @Nullable ReceiptCredentialPresentation getPresentation() throws InvalidInputException, NoSuchMessageException { - final ReceiptCredentialPresentation receiptCredentialPresentation; - - if (isForSubscription()) { - receiptCredentialPresentation = SignalStore.inAppPayments().getSubscriptionReceiptCredential(); - } else { - receiptCredentialPresentation = null; - } - - if (receiptCredentialPresentation != null) { - return receiptCredentialPresentation; - } if (giftMessageId == NO_ID) { - return getPresentationFromInputData(); - } else { - return getPresentationFromGiftMessage(); - } - } - - private @Nullable ReceiptCredentialPresentation getPresentationFromInputData() throws InvalidInputException { - JsonJobData inputData = JsonJobData.deserialize(getInputData()); - - if (inputData.isEmpty()) { - Log.w(TAG, "No input data. Exiting.", true); - return null; - } - - byte[] presentationBytes = inputData.getStringAsBlob(INPUT_RECEIPT_CREDENTIAL_PRESENTATION); - if (presentationBytes == null) { - Log.d(TAG, "No response data. Exiting.", true); - return null; - } - - return new ReceiptCredentialPresentation(presentationBytes); - } - - private @Nullable ReceiptCredentialPresentation getPresentationFromGiftMessage() throws InvalidInputException, NoSuchMessageException { - MessageRecord messageRecord = SignalDatabase.messages().getMessageRecord(giftMessageId); - - if (MessageRecordUtil.hasGiftBadge(messageRecord)) { - GiftBadge giftBadge = MessageRecordUtil.requireGiftBadge(messageRecord); - if (giftBadge.redemptionState == GiftBadge.RedemptionState.REDEEMED) { - Log.d(TAG, "Already redeemed this gift badge. Exiting.", true); - return null; - } else { - Log.d(TAG, "Attempting redemption of badge in state " + giftBadge.redemptionState.name()); - return new ReceiptCredentialPresentation(giftBadge.redemptionToken.toByteArray()); - } - } else { - Log.d(TAG, "No gift badge on message record. Exiting.", true); - return null; - } - } - - private boolean isForSubscription() { - return Objects.requireNonNull(getParameters().getQueue()).startsWith(SUBSCRIPTION_QUEUE); - } - - private boolean isForOneTimeDonation() { - return Objects.requireNonNull(getParameters().getQueue()).startsWith(ONE_TIME_QUEUE) && giftMessageId == NO_ID; - } - - private void enqueueDonationComplete() { - if (errorSource == DonationErrorSource.GIFT || errorSource == DonationErrorSource.GIFT_REDEMPTION) { - Log.i(TAG, "Skipping donation complete sheet for GIFT related redemption."); - return; - } - - if (errorSource == DonationErrorSource.KEEP_ALIVE) { - Log.i(TAG, "Skipping donation complete sheet for subscription KEEP_ALIVE jobchain."); - return; - } - - SignalStore.inAppPayments().appendToTerminalDonationQueue(terminalDonation); - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof RetryableException; - } - - private final static class RetryableException extends Exception { - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull DonationReceiptRedemptionJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - - String serializedErrorSource = data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.UNKNOWN.serialize()); - long messageId = data.getLongOrDefault(DATA_GIFT_MESSAGE_ID, NO_ID); - boolean primary = data.getBooleanOrDefault(DATA_PRIMARY, false); - DonationErrorSource errorSource = DonationErrorSource.deserialize(serializedErrorSource); - long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L); - - return new DonationReceiptRedemptionJob(messageId, primary, errorSource, uiSessionKey, parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt deleted file mode 100644 index f9f6d34d20..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ -package org.thoughtcrime.securesms.jobs - -import io.reactivex.rxjava3.core.Single -import org.signal.core.util.logging.Log -import org.signal.core.util.money.FiatMoney -import org.signal.donations.InAppPaymentType -import org.signal.donations.PaymentSourceType -import org.signal.donations.StripeApi -import org.signal.donations.StripeIntentAccessor -import org.signal.donations.json.StripeIntentStatus -import org.thoughtcrime.securesms.badges.Badges -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource -import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository -import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData -import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError -import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.Companion.toDonationErrorValue -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord -import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord -import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue -import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobmanager.Job -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.util.Environment -import org.whispersystems.signalservice.internal.ServiceResponse - -/** - * Proceeds with an externally approved (say, in a bank app) donation - * and continues to process it. - */ -@Deprecated("Replaced with InAppPaymentAuthCheckJob") -class ExternalLaunchDonationJob private constructor( - private val stripe3DSData: Stripe3DSData, - parameters: Parameters -) : BaseJob(parameters), StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper { - - private var donationError: DonationError? = null - - companion object { - const val KEY = "ExternalLaunchDonationJob" - - private val TAG = Log.tag(ExternalLaunchDonationJob::class.java) - - private fun createDonationError(stripe3DSData: Stripe3DSData, throwable: Throwable): DonationError { - val source = stripe3DSData.inAppPayment.type.toErrorSource() - return DonationError.PaymentSetupError.GenericError(source, throwable) - } - } - - private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, AppDependencies.okHttpClient) - - override fun serialize(): ByteArray { - return stripe3DSData.toProtoBytes() - } - - override fun getFactoryKey(): String = KEY - - override fun onFailure() { - if (donationError != null) { - when (stripe3DSData.inAppPayment.type) { - InAppPaymentType.ONE_TIME_DONATION -> { - SignalStore.inAppPayments.setPendingOneTimeDonation( - DonationSerializationHelper.createPendingOneTimeDonationProto( - Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!), - stripe3DSData.paymentSourceType, - stripe3DSData.inAppPayment.data.amount!!.toFiatMoney() - ).copy( - error = donationError?.toDonationErrorValue() - ) - ) - } - - InAppPaymentType.RECURRING_DONATION -> { - SignalStore.inAppPayments.appendToTerminalDonationQueue( - TerminalDonationQueue.TerminalDonation( - level = stripe3DSData.inAppPayment.data.level, - isLongRunningPaymentMethod = stripe3DSData.isLongRunning, - error = donationError?.toDonationErrorValue() - ) - ) - } - - else -> Log.w(TAG, "Job failed with donation error for type: ${stripe3DSData.inAppPayment.type}") - } - } - } - - override fun onRun() { - when (stripe3DSData.stripeIntentAccessor.objectType) { - StripeIntentAccessor.ObjectType.NONE -> { - Log.w(TAG, "NONE type does not require confirmation. Failing Permanently.") - throw Exception() - } - - StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> runForPaymentIntent() - StripeIntentAccessor.ObjectType.SETUP_INTENT -> runForSetupIntent() - } - } - - private fun runForPaymentIntent() { - Log.d(TAG, "Downloading payment intent...") - val stripePaymentIntent = stripeApi.getPaymentIntent(stripe3DSData.stripeIntentAccessor) - checkIntentStatus(stripePaymentIntent.status) - - Log.i(TAG, "Creating and inserting donation receipt record.", true) - val inAppPaymentReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentType.ONE_TIME_DONATION) { - InAppPaymentReceiptRecord.createForBoost(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney()) - } else { - InAppPaymentReceiptRecord.createForGift(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney()) - } - - SignalDatabase.donationReceipts.addReceipt(inAppPaymentReceiptRecord) - - Log.i(TAG, "Creating and inserting one-time pending donation.", true) - SignalStore.inAppPayments.setPendingOneTimeDonation( - DonationSerializationHelper.createPendingOneTimeDonationProto( - Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!), - stripe3DSData.paymentSourceType, - stripe3DSData.inAppPayment.data.amount.toFiatMoney() - ) - ) - - Log.i(TAG, "Continuing job chain...", true) - } - - private fun runForSetupIntent() { - Log.d(TAG, "Downloading setup intent...") - val stripeSetupIntent = stripeApi.getSetupIntent(stripe3DSData.stripeIntentAccessor) - checkIntentStatus(stripeSetupIntent.status) - - val subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION) - - Log.i(TAG, "Setting default payment method...", true) - val setPaymentMethodResponse = if (stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.IDEAL) { - AppDependencies.donationsService - .setDefaultIdealPaymentMethod(subscriber.subscriberId, stripeSetupIntent.id) - } else { - AppDependencies.donationsService - .setDefaultStripePaymentMethod(subscriber.subscriberId, stripeSetupIntent.paymentMethod!!) - } - - getResultOrThrow(setPaymentMethodResponse) - - Log.i(TAG, "Set default payment method via Signal service!", true) - Log.i(TAG, "Storing the subscription payment source type locally.", true) - SignalStore.inAppPayments.setSubscriptionPaymentSourceType(stripe3DSData.paymentSourceType) - - val subscriptionLevel = stripe3DSData.inAppPayment.data.level.toString() - - try { - val levelUpdateOperation = RecurringInAppPaymentRepository.getOrCreateLevelUpdateOperation(TAG, subscriptionLevel) - Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true) - - val updateSubscriptionLevelResponse = AppDependencies.donationsService.updateSubscriptionLevel( - subscriber.subscriberId, - subscriptionLevel, - subscriber.currency.currencyCode, - levelUpdateOperation.idempotencyKey.serialize(), - subscriber.type.lock - ) - - getResultOrThrow(updateSubscriptionLevelResponse, doOnApplicationError = { - SignalStore.inAppPayments.clearLevelOperations() - }) - - if (updateSubscriptionLevelResponse.status in listOf(200, 204)) { - Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${updateSubscriptionLevelResponse.status}", true) - SignalStore.inAppPayments.updateLocalStateForLocalSubscribe(subscriber.type) - SignalStore.inAppPayments.setVerifiedSubscription3DSData(stripe3DSData) - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - } else { - error("Unexpected status code ${updateSubscriptionLevelResponse.status} without an application error or execution error.") - } - } finally { - LevelUpdate.updateProcessingState(false) - } - } - - private fun checkIntentStatus(stripeIntentStatus: StripeIntentStatus?) { - when (stripeIntentStatus) { - null, StripeIntentStatus.SUCCEEDED -> { - Log.i(TAG, "Stripe Intent is in the SUCCEEDED state, we can proceed.", true) - } - - StripeIntentStatus.CANCELED -> { - Log.i(TAG, "Stripe Intent is cancelled, we cannot proceed.", true) - donationError = createDonationError(stripe3DSData, Exception("User cancelled payment.")) - throw donationError!! - } - - StripeIntentStatus.REQUIRES_PAYMENT_METHOD -> { - Log.i(TAG, "Stripe Intent payment failed, we cannot proceed.", true) - donationError = createDonationError(stripe3DSData, Exception("payment failed")) - throw donationError!! - } - - else -> { - Log.i(TAG, "Stripe Intent is still processing, retry later. $stripeIntentStatus", true) - throw RetryException() - } - } - } - - private fun getResultOrThrow( - serviceResponse: ServiceResponse, - doOnApplicationError: () -> Unit = {} - ): Result { - if (serviceResponse.result.isPresent) { - return serviceResponse.result.get() - } else if (serviceResponse.applicationError.isPresent) { - Log.w(TAG, "An application error was present. ${serviceResponse.status}", serviceResponse.applicationError.get(), true) - doOnApplicationError() - - SignalStore.inAppPayments.appendToTerminalDonationQueue( - TerminalDonationQueue.TerminalDonation( - level = stripe3DSData.inAppPayment.data.level, - isLongRunningPaymentMethod = stripe3DSData.isLongRunning, - error = DonationErrorValue( - DonationErrorValue.Type.PAYMENT, - code = serviceResponse.status.toString() - ) - ) - ) - - throw serviceResponse.applicationError.get() - } else if (serviceResponse.executionError.isPresent) { - Log.w(TAG, "An execution error was present. ${serviceResponse.status}", serviceResponse.executionError.get(), true) - throw RetryException(serviceResponse.executionError.get()) - } - - error("Should never get here.") - } - - override fun onShouldRetry(e: Exception): Boolean { - return e is RetryException - } - - class RetryException(cause: Throwable? = null) : Exception(cause) - - class Factory : Job.Factory { - override fun create(parameters: Parameters, serializedData: ByteArray?): ExternalLaunchDonationJob { - if (serializedData == null) { - error("Unexpected null value for serialized data") - } - - val stripe3DSData = parseSerializedData(serializedData) - - return ExternalLaunchDonationJob(stripe3DSData, parameters) - } - - companion object { - fun parseSerializedData(serializedData: ByteArray): Stripe3DSData { - return Stripe3DSData.fromProtoBytes(serializedData) - } - } - } - - override fun fetchPaymentIntent(price: FiatMoney, level: Long, sourceType: PaymentSourceType.Stripe): Single { - error("Not needed, this job should not be creating intents.") - } - - override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single { - error("Not needed, this job should not be creating intents.") - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GiftSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/GiftSendJob.kt deleted file mode 100644 index 70bd68f9e4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GiftSendJob.kt +++ /dev/null @@ -1,116 +0,0 @@ -package org.thoughtcrime.securesms.jobs - -import okio.ByteString.Companion.toByteString -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.badges.gifts.Gifts -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.database.RecipientTable -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.JsonJobData -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.sharing.MultiShareArgs -import org.thoughtcrime.securesms.sharing.MultiShareSender -import org.thoughtcrime.securesms.sms.MessageSender -import java.util.concurrent.TimeUnit - -/** - * Sends a message to the given recipient containing a redeemable badge token. - * This job assumes that the client has already determined whether the given recipient can receive a gift badge. - */ -@Deprecated("Replaced with InAppPaymentGiftSendJob") -class GiftSendJob private constructor(parameters: Parameters, private val recipientId: RecipientId, private val additionalMessage: String?) : Job(parameters) { - - companion object { - private val TAG = Log.tag(GiftSendJob::class.java) - - const val KEY = "SendGiftJob" - const val DATA_RECIPIENT_ID = "data.recipient.id" - const val DATA_ADDITIONAL_MESSAGE = "data.additional.message" - } - - constructor(recipientId: RecipientId, additionalMessage: String?) : - this( - parameters = Parameters.Builder() - .build(), - recipientId = recipientId, - additionalMessage = additionalMessage - ) - - override fun serialize(): ByteArray? = JsonJobData.Builder() - .putLong(DATA_RECIPIENT_ID, recipientId.toLong()) - .putString(DATA_ADDITIONAL_MESSAGE, additionalMessage) - .serialize() - - override fun getFactoryKey(): String = KEY - - override fun run(): Result { - Log.i(TAG, "Getting data and generating message for gift send to $recipientId") - - val token = JsonJobData.deserialize(this.inputData).getStringAsBlob(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION) ?: return Result.failure() - - val recipient = Recipient.resolved(recipientId) - - if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientTable.RegisteredState.REGISTERED) { - Log.w(TAG, "Invalid recipient $recipientId for gift send.") - return Result.failure() - } - - val thread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - - val outgoingMessage = Gifts.createOutgoingGiftMessage( - recipient = recipient, - expiresIn = TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()), - sentTimestamp = System.currentTimeMillis(), - giftBadge = GiftBadge(redemptionToken = token.toByteString()) - ) - - Log.i(TAG, "Sending gift badge to $recipientId...") - var didInsert = false - MessageSender.send(context, outgoingMessage, thread, MessageSender.SendType.SIGNAL, null) { - didInsert = true - } - - return if (didInsert) { - Log.i(TAG, "Successfully inserted outbox message for gift", true) - - val trimmedMessage = additionalMessage?.trim() - if (!trimmedMessage.isNullOrBlank()) { - Log.i(TAG, "Sending additional message...") - - val result = MultiShareSender.sendSync( - MultiShareArgs.Builder(setOf(ContactSearchKey.RecipientSearchKey(recipientId, false))) - .withDraftText(trimmedMessage) - .build() - ) - - if (result.containsFailures()) { - Log.w(TAG, "Failed to send additional message, but gift sent fine.", true) - } - - Result.success() - } else { - Result.success() - } - } else { - Log.w(TAG, "Failed to insert outbox message for gift", true) - Result.failure() - } - } - - override fun onFailure() { - Log.w(TAG, "Failed to submit send of gift badge to $recipientId") - } - - class Factory : Job.Factory { - override fun create(parameters: Parameters, serializedData: ByteArray?): GiftSendJob { - val data = JsonJobData.deserialize(serializedData) - val recipientId = RecipientId.from(data.getLong(DATA_RECIPIENT_ID)) - val additionalMessage = data.getStringOrDefault(DATA_ADDITIONAL_MESSAGE, null) - - return GiftSendJob(parameters, recipientId, additionalMessage) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt index 6f1c8de657..66e3421db8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt @@ -11,8 +11,6 @@ import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType -import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobStatus -import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord @@ -125,15 +123,6 @@ class InAppPaymentKeepAliveJob private constructor( return } - // Note that this can be removed once the old jobs are decommissioned. These jobs live in different queues, and should still be respected. - if (type == InAppPaymentSubscriberRecord.Type.DONATION) { - val legacyRedemptionStatus = DonationRedemptionJobWatcher.getSubscriptionRedemptionJobStatus() - if (legacyRedemptionStatus != DonationRedemptionJobStatus.None && legacyRedemptionStatus != DonationRedemptionJobStatus.FailedSubscription) { - info(type, "Already trying to redeem donation, current status: ${legacyRedemptionStatus.javaClass.simpleName}") - return - } - } - val activeInAppPayment = getActiveInAppPayment(subscriber, subscription) if (activeInAppPayment == null) { warn(type, "Failed to generate active in-app payment. Exiting") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 82831e390c..534ccfeeab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -127,7 +127,6 @@ public final class JobManagerFactories { put(BackupMessagesJob.KEY, new BackupMessagesJob.Factory()); put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory()); put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory()); - put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory()); put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory()); put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory()); put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory()); @@ -144,7 +143,6 @@ public final class JobManagerFactories { put(CreateReleaseChannelJob.KEY, new CreateReleaseChannelJob.Factory()); put(DeleteAbandonedAttachmentsJob.KEY, new DeleteAbandonedAttachmentsJob.Factory()); put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory()); - put(DonationReceiptRedemptionJob.KEY, new DonationReceiptRedemptionJob.Factory()); put(DownloadLatestEmojiDataJob.KEY, new DownloadLatestEmojiDataJob.Factory()); put(EmojiSearchIndexDownloadJob.KEY, new EmojiSearchIndexDownloadJob.Factory()); put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory()); @@ -153,7 +151,6 @@ public final class JobManagerFactories { put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory()); put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory()); put(GenerateAudioWaveFormJob.KEY, new GenerateAudioWaveFormJob.Factory()); - put(GiftSendJob.KEY, new GiftSendJob.Factory()); put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory()); put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory()); put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory()); @@ -203,7 +200,6 @@ public final class JobManagerFactories { put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); - put(ExternalLaunchDonationJob.KEY, new ExternalLaunchDonationJob.Factory()); put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); put(PushDistributionListSendJob.KEY, new PushDistributionListSendJob.Factory()); @@ -251,8 +247,6 @@ public final class JobManagerFactories { put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory()); put(StorageSyncJob.KEY, new StorageSyncJob.Factory()); - put(SubscriptionKeepAliveJob.KEY, new SubscriptionKeepAliveJob.Factory()); - put(SubscriptionReceiptRequestResponseJob.KEY, new SubscriptionReceiptRequestResponseJob.Factory()); put(SubscriberIdMigrationJob.KEY, new SubscriberIdMigrationJob.Factory()); put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory()); put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory()); @@ -323,46 +317,52 @@ public final class JobManagerFactories { put(WallpaperStorageMigrationJob.KEY, new WallpaperStorageMigrationJob.Factory()); // Dead jobs - put(FailingJob.KEY, new FailingJob.Factory()); - put(PassingMigrationJob.KEY, new PassingMigrationJob.Factory()); - put("PushContentReceiveJob", new FailingJob.Factory()); - put("AttachmentUploadJob", new FailingJob.Factory()); - put("MmsSendJob", new FailingJob.Factory()); - put("RefreshUnidentifiedDeliveryAbilityJob", new FailingJob.Factory()); - put("Argon2TestJob", new FailingJob.Factory()); - put("Argon2TestMigrationJob", new PassingMigrationJob.Factory()); - put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory()); - put("StorageSyncJob", new StorageSyncJob.Factory()); - put("WakeGroupV2Job", new FailingJob.Factory()); - put("LeaveGroupJob", new FailingJob.Factory()); - put("PushGroupUpdateJob", new FailingJob.Factory()); - put("RequestGroupInfoJob", new FailingJob.Factory()); - put("RotateSignedPreKeyJob", new PreKeysSyncJob.Factory()); - put("CreateSignedPreKeyJob", new PreKeysSyncJob.Factory()); - put("RefreshPreKeysJob", new PreKeysSyncJob.Factory()); - put("RecipientChangedNumberJob", new FailingJob.Factory()); - put("PushTextSendJob", new IndividualSendJob.Factory()); - put("MultiDevicePniIdentityUpdateJob", new FailingJob.Factory()); - put("MultiDeviceGroupUpdateJob", new FailingJob.Factory()); - put("CallSyncEventJob", new FailingJob.Factory()); - put("RegistrationPinV2MigrationJob", new FailingJob.Factory()); - put("KbsEnclaveMigrationWorkerJob", new FailingJob.Factory()); - put("KbsEnclaveMigrationJob", new PassingMigrationJob.Factory()); - put("ClearFallbackKbsEnclaveJob", new FailingJob.Factory()); - put("PushDecryptJob", new FailingJob.Factory()); - put("PushDecryptDrainedJob", new FailingJob.Factory()); - put("PushProcessJob", new FailingJob.Factory()); - put("DecryptionsDrainedMigrationJob", new PassingMigrationJob.Factory()); - put("MmsReceiveJob", new FailingJob.Factory()); - put("MmsDownloadJob", new FailingJob.Factory()); - put("SmsReceiveJob", new FailingJob.Factory()); - put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory()); - put("GroupV1MigrationJob", new FailingJob.Factory()); - put("NewRegistrationUsernameSyncJob", new FailingJob.Factory()); - put("SmsSendJob", new FailingJob.Factory()); - put("SmsSentJob", new FailingJob.Factory()); - put("MmsSendJobV2", new FailingJob.Factory()); - put("AttachmentUploadJobV2", new FailingJob.Factory()); + put(FailingJob.KEY, new FailingJob.Factory()); + put(PassingMigrationJob.KEY, new PassingMigrationJob.Factory()); + put("PushContentReceiveJob", new FailingJob.Factory()); + put("AttachmentUploadJob", new FailingJob.Factory()); + put("MmsSendJob", new FailingJob.Factory()); + put("RefreshUnidentifiedDeliveryAbilityJob", new FailingJob.Factory()); + put("Argon2TestJob", new FailingJob.Factory()); + put("Argon2TestMigrationJob", new PassingMigrationJob.Factory()); + put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory()); + put("StorageSyncJob", new StorageSyncJob.Factory()); + put("WakeGroupV2Job", new FailingJob.Factory()); + put("LeaveGroupJob", new FailingJob.Factory()); + put("PushGroupUpdateJob", new FailingJob.Factory()); + put("RequestGroupInfoJob", new FailingJob.Factory()); + put("RotateSignedPreKeyJob", new PreKeysSyncJob.Factory()); + put("CreateSignedPreKeyJob", new PreKeysSyncJob.Factory()); + put("RefreshPreKeysJob", new PreKeysSyncJob.Factory()); + put("RecipientChangedNumberJob", new FailingJob.Factory()); + put("PushTextSendJob", new IndividualSendJob.Factory()); + put("MultiDevicePniIdentityUpdateJob", new FailingJob.Factory()); + put("MultiDeviceGroupUpdateJob", new FailingJob.Factory()); + put("CallSyncEventJob", new FailingJob.Factory()); + put("RegistrationPinV2MigrationJob", new FailingJob.Factory()); + put("KbsEnclaveMigrationWorkerJob", new FailingJob.Factory()); + put("KbsEnclaveMigrationJob", new PassingMigrationJob.Factory()); + put("ClearFallbackKbsEnclaveJob", new FailingJob.Factory()); + put("PushDecryptJob", new FailingJob.Factory()); + put("PushDecryptDrainedJob", new FailingJob.Factory()); + put("PushProcessJob", new FailingJob.Factory()); + put("DecryptionsDrainedMigrationJob", new PassingMigrationJob.Factory()); + put("MmsReceiveJob", new FailingJob.Factory()); + put("MmsDownloadJob", new FailingJob.Factory()); + put("SmsReceiveJob", new FailingJob.Factory()); + put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory()); + put("GroupV1MigrationJob", new FailingJob.Factory()); + put("NewRegistrationUsernameSyncJob", new FailingJob.Factory()); + put("SmsSendJob", new FailingJob.Factory()); + put("SmsSentJob", new FailingJob.Factory()); + put("MmsSendJobV2", new FailingJob.Factory()); + put("AttachmentUploadJobV2", new FailingJob.Factory()); + put("SubscriptionKeepAliveJob", new FailingJob.Factory()); + put("ExternalLaunchDonationJob", new FailingJob.Factory()); + put("BoostReceiptCredentialsSubmissionJob", new FailingJob.Factory()); + put("SubscriptionReceiptCredentialsSubmissionJob", new FailingJob.Factory()); + put("DonationReceiptRedemptionJob", new FailingJob.Factory()); + put("SendGiftJob", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java deleted file mode 100644 index 54a851d159..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ /dev/null @@ -1,180 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository; -import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobStatus; -import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher; -import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord; -import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; -import org.whispersystems.signalservice.internal.EmptyResponse; -import org.whispersystems.signalservice.internal.ServiceResponse; - -import java.io.IOException; -import java.util.Locale; -import java.util.Objects; -import java.util.concurrent.locks.Lock; - -import okio.ByteString; - -/** - * Job that, once there is a valid local subscriber id, should be run every 3 days - * to ensure that a user's subscription does not lapse. - * - * @deprecated Replaced with InAppPaymentKeepAliveJob - */ -@Deprecated() -public class SubscriptionKeepAliveJob extends BaseJob { - - public static final String KEY = "SubscriptionKeepAliveJob"; - - private static final String TAG = Log.tag(SubscriptionKeepAliveJob.class); - - private SubscriptionKeepAliveJob(@NonNull Parameters parameters) { - super(parameters); - } - - @Override - public @Nullable byte[] serialize() { - return null; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - - } - - @Override - protected void onRun() throws Exception { - Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock(); - lock.lock(); - try { - doRun(); - } finally { - lock.unlock(); - } - } - - private void doRun() throws Exception { - InAppPaymentSubscriberRecord subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION); - if (subscriber == null) { - return; - } - - ServiceResponse response = AppDependencies.getDonationsService() - .putSubscription(subscriber.getSubscriberId()); - - verifyResponse(response); - Log.i(TAG, "Successful call to PUT subscription ID", true); - - ServiceResponse activeSubscriptionResponse = AppDependencies.getDonationsService() - .getSubscription(subscriber.getSubscriberId()); - - verifyResponse(activeSubscriptionResponse); - Log.i(TAG, "Successful call to GET active subscription", true); - - ActiveSubscription activeSubscription = activeSubscriptionResponse.getResult().get(); - if (activeSubscription.getActiveSubscription() == null) { - Log.i(TAG, "User does not have a subscription. Exiting.", true); - return; - } - - DonationRedemptionJobStatus status = DonationRedemptionJobWatcher.getSubscriptionRedemptionJobStatus(); - if (status != DonationRedemptionJobStatus.None.INSTANCE && status != DonationRedemptionJobStatus.FailedSubscription.INSTANCE) { - Log.i(TAG, "Already trying to redeem donation, current status: " + status.getClass().getSimpleName(), true); - return; - } - - final long endOfCurrentPeriod = activeSubscription.getActiveSubscription().getEndOfCurrentPeriod(); - if (endOfCurrentPeriod > SignalStore.inAppPayments().getLastEndOfPeriod()) { - Log.i(TAG, - String.format(Locale.US, - "Last end of period change. Requesting receipt refresh. (old: %d to new: %d)", - SignalStore.inAppPayments().getLastEndOfPeriod(), - activeSubscription.getActiveSubscription().getEndOfCurrentPeriod()), - true); - - SignalStore.inAppPayments().setLastEndOfPeriod(endOfCurrentPeriod); - SignalStore.inAppPayments().clearSubscriptionRequestCredential(); - SignalStore.inAppPayments().clearSubscriptionReceiptCredential(); - MultiDeviceSubscriptionSyncRequestJob.enqueue(); - } - - TerminalDonationQueue.TerminalDonation terminalDonation = new TerminalDonationQueue.TerminalDonation( - activeSubscription.getActiveSubscription().getLevel(), - Objects.equals(activeSubscription.getActiveSubscription().getPaymentMethod(), ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT), - null, - ByteString.EMPTY - ); - - if (endOfCurrentPeriod > SignalStore.inAppPayments().getSubscriptionEndOfPeriodConversionStarted()) { - Log.i(TAG, "Subscription end of period is after the conversion end of period. Storing it, generating a credential, and enqueuing the continuation job chain.", true); - SignalStore.inAppPayments().setSubscriptionEndOfPeriodConversionStarted(endOfCurrentPeriod); - SignalStore.inAppPayments().refreshSubscriptionRequestCredential(); - - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, terminalDonation).enqueue(); - } else if (endOfCurrentPeriod > SignalStore.inAppPayments().getSubscriptionEndOfPeriodRedemptionStarted()) { - if (SignalStore.inAppPayments().getSubscriptionRequestCredential() == null) { - Log.i(TAG, "We have not started a redemption, but do not have a request credential. Possible that the subscription changed.", true); - return; - } - - Log.i(TAG, "We have a request credential and have not yet turned it into a redeemable token.", true); - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, terminalDonation).enqueue(); - } else if (endOfCurrentPeriod > SignalStore.inAppPayments().getSubscriptionEndOfPeriodRedeemed()) { - if (SignalStore.inAppPayments().getSubscriptionReceiptCredential() == null) { - Log.i(TAG, "We have successfully started redemption but have no stored token. Possible that the subscription changed.", true); - return; - } - - Log.i(TAG, "We have a receipt credential and have not yet redeemed it.", true); - DonationReceiptRedemptionJob.createJobChainForKeepAlive().enqueue(); - } else { - Log.i(TAG, "Subscription is active, and end of current period (remote) is after the latest checked end of period (local). Nothing to do."); - } - } - - private void verifyResponse(@NonNull ServiceResponse response) throws Exception { - if (response.getExecutionError().isPresent()) { - Log.w(TAG, "Failed with an execution error. Scheduling retry.", response.getExecutionError().get(), true); - throw new RetryableException(); - } else if (response.getApplicationError().isPresent()) { - switch (response.getStatus()) { - case 403: - case 404: - Log.w(TAG, "Invalid or malformed subscriber id. Status: " + response.getStatus(), response.getApplicationError().get(), true); - throw new IOException(); - default: - Log.w(TAG, "An unknown server error occurred: " + response.getStatus(), response.getApplicationError().get(), true); - throw new RetryableException(); - } - } - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof RetryableException; - } - - private static class RetryableException extends Exception { - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull SubscriptionKeepAliveJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - return new SubscriptionKeepAliveJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java deleted file mode 100644 index 5af69163ac..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ /dev/null @@ -1,557 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import org.signal.core.util.Base64; -import org.signal.core.util.logging.Log; -import org.signal.donations.PaymentSourceType; -import org.signal.donations.StripeDeclineCode; -import org.signal.donations.StripeFailureCode; -import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.VerificationFailedException; -import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredential; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationsConfigurationExtensionsKt; -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository; -import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError; -import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource; -import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord; -import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord; -import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue; -import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -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.util.Locale; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; - -import okio.ByteString; - -/** - * Job responsible for submitting ReceiptCredentialRequest objects to the server until - * we get a response. - * - * * @deprecated Replaced with InAppPaymentRecurringContextJob - */ -@Deprecated() -public class SubscriptionReceiptRequestResponseJob extends BaseJob { - - private static final String TAG = Log.tag(SubscriptionReceiptRequestResponseJob.class); - - public static final String KEY = "SubscriptionReceiptCredentialsSubmissionJob"; - - private static final String DATA_REQUEST_BYTES = "data.request.bytes"; - private static final String DATA_SUBSCRIBER_ID = "data.subscriber.id"; - private static final String DATA_IS_FOR_KEEP_ALIVE = "data.is.for.keep.alive"; - private static final String DATA_UI_SESSION_KEY = "data.ui.session.key"; - private static final String DATA_TERMINAL_DONATION = "data.terminal.donation"; - - private final SubscriberId subscriberId; - private final boolean isForKeepAlive; - private final long uiSessionKey; - private TerminalDonationQueue.TerminalDonation terminalDonation; - - private static SubscriptionReceiptRequestResponseJob createJob(@NonNull SubscriberId subscriberId, - boolean isForKeepAlive, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - return new SubscriptionReceiptRequestResponseJob( - new Parameters - .Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue("ReceiptRedemption") - .setLifespan(terminalDonation.isLongRunningPaymentMethod ? TimeUnit.DAYS.toMillis(30) : TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - subscriberId, - isForKeepAlive, - uiSessionKey, - terminalDonation - ); - } - - public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) { - // TODO [alex] db on main? - InAppPaymentSubscriberRecord subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION); - SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, terminalDonation); - DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey, terminalDonation.isLongRunningPaymentMethod); - RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription(); - MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); - - return AppDependencies.getJobManager() - .startChain(requestReceiptJob) - .then(redeemReceiptJob) - .then(refreshOwnProfileJob) - .then(multiDeviceProfileContentUpdateJob); - } - - private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters, - @NonNull SubscriberId subscriberId, - boolean isForKeepAlive, - long uiSessionKey, - @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) - { - super(parameters); - this.subscriberId = subscriberId; - this.isForKeepAlive = isForKeepAlive; - this.uiSessionKey = uiSessionKey; - this.terminalDonation = terminalDonation; - } - - @Override - public @Nullable byte[] serialize() { - JsonJobData.Builder builder = new JsonJobData.Builder().putBlobAsString(DATA_SUBSCRIBER_ID, subscriberId.getBytes()) - .putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive) - .putLong(DATA_UI_SESSION_KEY, uiSessionKey) - .putBlobAsString(DATA_TERMINAL_DONATION, terminalDonation.encode()); - - return builder.serialize(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onFailure() { - if (terminalDonation.error != null) { - SignalStore.inAppPayments().appendToTerminalDonationQueue(terminalDonation); - } else { - Log.w(TAG, "Job is in terminal state without an error on TerminalDonation."); - } - } - - @Override - protected void onRun() throws Exception { - Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock(); - lock.lock(); - try { - doRun(); - } finally { - lock.unlock(); - } - } - - @Override - public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) { - if (terminalDonation.isLongRunningPaymentMethod) { - return TimeUnit.DAYS.toMillis(1); - } else { - return super.getNextRunAttemptBackoff(pastAttemptCount, exception); - } - } - - private void doRun() throws Exception { - ReceiptCredentialRequestContext requestContext = SignalStore.inAppPayments().getSubscriptionRequestCredential(); - ActiveSubscription activeSubscription = getLatestSubscriptionInformation(); - ActiveSubscription.Subscription subscription = activeSubscription.getActiveSubscription(); - - if (requestContext == null) { - Log.w(TAG, "Request context is null.", true); - throw new Exception("Cannot get a response without a request."); - } - - if (subscription == null) { - Log.w(TAG, "Subscription is null.", true); - throw new RetryableException(); - } else if (subscription.isFailedPayment()) { - ActiveSubscription.ChargeFailure chargeFailure = activeSubscription.getChargeFailure(); - if (chargeFailure != null) { - Log.w(TAG, "Subscription payment charge failure code: " + chargeFailure.getCode() + ", message: " + chargeFailure.getMessage(), true); - } - - if (isForKeepAlive) { - Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + "). Payment could still be retried by processor.", true); - throw new Exception("Payment renewal is in retry state, let keep-alive job restart process"); - } else { - Log.w(TAG, "New subscription has hit a payment failure. (status = " + subscription.getStatus() + ").", true); - onPaymentFailure(subscription, chargeFailure, false); - throw new Exception("New subscription has hit a payment failure: " + subscription.getStatus()); - } - } else if (!subscription.isActive()) { - ActiveSubscription.ChargeFailure chargeFailure = activeSubscription.getChargeFailure(); - if (chargeFailure != null) { - Log.w(TAG, "Subscription payment charge failure code: " + chargeFailure.getCode() + ", message: " + chargeFailure.getMessage(), true); - - if (!isForKeepAlive) { - Log.w(TAG, "Initial subscription payment failed, treating as a permanent failure."); - onPaymentFailure(subscription, chargeFailure, false); - throw new Exception("New subscription has hit a payment failure."); - } - } - - if (isForKeepAlive && subscription.isCanceled()) { - Log.w(TAG, "Permanent payment failure in renewing subscription. (status = " + subscription.getStatus() + ").", true); - onPaymentFailure(subscription, chargeFailure, true); - throw new Exception(); - } - - Log.w(TAG, "Subscription is not yet active. Status: " + subscription.getStatus(), true); - throw new RetryableException(); - } else { - Log.i(TAG, "Subscription is valid, proceeding with request for ReceiptCredentialResponse", true); - long storedEndOfPeriod = SignalStore.inAppPayments().getLastEndOfPeriod(); - if (storedEndOfPeriod < subscription.getEndOfCurrentPeriod()) { - Log.i(TAG, "Storing lastEndOfPeriod and syncing with linked devices", true); - SignalStore.inAppPayments().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod()); - MultiDeviceSubscriptionSyncRequestJob.enqueue(); - } - - if (SignalStore.inAppPayments().getSubscriptionEndOfPeriodConversionStarted() == 0L) { - Log.i(TAG, "Marking the start of initial conversion.", true); - SignalStore.inAppPayments().setSubscriptionEndOfPeriodConversionStarted(subscription.getEndOfCurrentPeriod()); - } - } - - Log.d(TAG, "Submitting receipt credential request."); - ServiceResponse response = AppDependencies.getDonationsService() - .submitReceiptCredentialRequestSync(subscriberId, requestContext.getRequest()); - - if (response.getApplicationError().isPresent()) { - handleApplicationError(response); - } else if (response.getResult().isPresent()) { - ReceiptCredential receiptCredential = getReceiptCredential(requestContext, response.getResult().get()); - - if (!isCredentialValid(subscription, receiptCredential)) { - onGenericRedemptionError(); - throw new IOException("Could not validate receipt credential"); - } - - ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential); - - Log.d(TAG, "Validated credential. Recording receipt and handing off to redemption job.", true); - SignalDatabase.donationReceipts().addReceipt(InAppPaymentReceiptRecord.createForSubscription(subscription)); - - SignalStore.inAppPayments().clearSubscriptionRequestCredential(); - SignalStore.inAppPayments().setSubscriptionReceiptCredential(receiptCredentialPresentation); - SignalStore.inAppPayments().setSubscriptionEndOfPeriodRedemptionStarted(subscription.getEndOfCurrentPeriod()); - - setOutputData(new JsonJobData.Builder() - .putBlobAsString(DonationReceiptRedemptionJob.INPUT_TERMINAL_DONATION, terminalDonation.encode()) - .build() - .serialize()); - } else { - Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orElse(null), true); - throw new RetryableException(); - } - } - - private @NonNull ActiveSubscription getLatestSubscriptionInformation() throws Exception { - ServiceResponse activeSubscription = AppDependencies.getDonationsService() - .getSubscription(subscriberId); - - if (activeSubscription.getResult().isPresent()) { - return activeSubscription.getResult().get(); - } else if (activeSubscription.getApplicationError().isPresent()) { - Log.w(TAG, "Unrecoverable error getting the user's current subscription. Failing.", activeSubscription.getApplicationError().get(), true); - onGenericRedemptionError(); - throw new IOException(activeSubscription.getApplicationError().get()); - } else { - throw new RetryableException(); - } - } - - private ReceiptCredentialPresentation getReceiptCredentialPresentation(@NonNull ReceiptCredential receiptCredential) throws RetryableException { - ClientZkReceiptOperations operations = AppDependencies.getClientZkReceiptOperations(); - - try { - return operations.createReceiptCredentialPresentation(receiptCredential); - } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredentialPresentation: encountered a verification failure in zk", e, true); - throw new RetryableException(); - } - } - - private ReceiptCredential getReceiptCredential(@NonNull ReceiptCredentialRequestContext requestContext, @NonNull ReceiptCredentialResponse response) throws RetryableException { - ClientZkReceiptOperations operations = AppDependencies.getClientZkReceiptOperations(); - - try { - return operations.receiveReceiptCredential(requestContext, response); - } catch (VerificationFailedException e) { - Log.w(TAG, "getReceiptCredential: encountered a verification failure in zk", e, true); - throw new RetryableException(); - } - } - - private void handleApplicationError(ServiceResponse response) throws Exception { - switch (response.getStatus()) { - case 204: - Log.w(TAG, "Payment is still processing. Trying again.", response.getApplicationError().get(), true); - SignalStore.inAppPayments().clearSubscriptionRedemptionFailed(); - throw new RetryableException(); - case 400: - Log.w(TAG, "Receipt credential request failed to validate.", response.getApplicationError().get(), true); - onGenericRedemptionError(); - throw new Exception(response.getApplicationError().get()); - case 402: - Log.w(TAG, "Payment looks like a failure but may be retried.", response.getApplicationError().get(), true); - throw new RetryableException(); - case 403: - Log.w(TAG, "SubscriberId password mismatch or account auth was present.", response.getApplicationError().get(), true); - onGenericRedemptionError(); - throw new Exception(response.getApplicationError().get()); - case 404: - Log.w(TAG, "SubscriberId not found or misformed.", response.getApplicationError().get(), true); - onGenericRedemptionError(); - throw new Exception(response.getApplicationError().get()); - case 409: - onAlreadyRedeemed(response); - break; - default: - Log.w(TAG, "Encountered a server failure response: " + response.getStatus(), response.getApplicationError().get(), true); - throw new RetryableException(); - } - } - - private void onGenericRedemptionError() { - terminalDonation = terminalDonation.newBuilder() - .error(new DonationErrorValue( - DonationErrorValue.Type.REDEMPTION, - "", - ByteString.EMPTY - )) - .build(); - - DonationError.routeBackgroundError( - context, - DonationError.genericBadgeRedemptionFailure(getErrorSource()) - ); - } - - private void onPaymentFailedError(DonationError.PaymentSetupError paymentFailure) { - terminalDonation = terminalDonation.newBuilder() - .error(DonationError.toDonationErrorValue(paymentFailure)) - .build(); - - DonationError.routeBackgroundError( - context, - paymentFailure, - terminalDonation.isLongRunningPaymentMethod - ); - } - - /** - * Handles state updates and error routing for a payment failure. - *

- * There are two ways this could go, depending on whether the job was created for a keep-alive chain. - *

- * 1. In the case of a normal chain (new subscription) We simply route the error out to the user. The payment failure would have occurred while trying to - * charge for the first month of their subscription, and are likely still on the "Subscribe" screen, so we can just display a dialog. - * 1. In the case of a keep-alive event, we want to book-keep the error to show the user on a subsequent launch, and we want to sync our failure state to - * linked devices. - */ - private void onPaymentFailure(@NonNull ActiveSubscription.Subscription subscription, @Nullable ActiveSubscription.ChargeFailure chargeFailure, boolean isForKeepAlive) { - InAppPaymentSubscriberRecord subscriberRecord = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION); - InAppPaymentsRepository.setShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriberRecord, true); - - if (isForKeepAlive) { - Log.d(TAG, "Subscription canceled during keep-alive. Setting UnexpectedSubscriptionCancelation state...", true); - SignalStore.inAppPayments().setUnexpectedSubscriptionCancelationChargeFailure(chargeFailure); - SignalStore.inAppPayments().setUnexpectedSubscriptionCancelationReason(subscription.getStatus()); - SignalStore.inAppPayments().setUnexpectedSubscriptionCancelationTimestamp(subscription.getEndOfCurrentPeriod()); - SignalStore.inAppPayments().setShowMonthlyDonationCanceledDialog(true); - - AppDependencies.getDonationsService().getDonationsConfiguration(Locale.getDefault()).getResult().ifPresent(config -> { - SignalStore.inAppPayments().setExpiredBadge(DonationsConfigurationExtensionsKt.getBadge(config, subscription.getLevel())); - }); - - MultiDeviceSubscriptionSyncRequestJob.enqueue(); - } else if (chargeFailure != null && subscription.getProcessor() == ActiveSubscription.Processor.STRIPE) { - Log.d(TAG, "Stripe charge failure detected: " + chargeFailure, true); - - StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason()); - StripeFailureCode failureCode = StripeFailureCode.Companion.getFromCode(chargeFailure.getCode()); - DonationError.PaymentSetupError paymentSetupError; - PaymentSourceType paymentSourceType = SignalStore.inAppPayments().getSubscriptionPaymentSourceType(); - boolean isStripeSource = paymentSourceType instanceof PaymentSourceType.Stripe; - - if (declineCode.isKnown() && isStripeSource) { - paymentSetupError = new DonationError.PaymentSetupError.StripeDeclinedError( - getErrorSource(), - new Exception(chargeFailure.getMessage()), - declineCode, - (PaymentSourceType.Stripe) paymentSourceType - ); - } else if (failureCode.isKnown() && isStripeSource) { - paymentSetupError = new DonationError.PaymentSetupError.StripeFailureCodeError( - getErrorSource(), - new Exception(chargeFailure.getMessage()), - failureCode, - (PaymentSourceType.Stripe) paymentSourceType - ); - } else if (isStripeSource) { - paymentSetupError = new DonationError.PaymentSetupError.StripeCodedError( - getErrorSource(), - new Exception("Card was declined. " + chargeFailure.getCode()), - chargeFailure.getCode() - ); - } else { - paymentSetupError = new DonationError.PaymentSetupError.GenericError( - getErrorSource(), - new Exception("Payment Failed for " + paymentSourceType.getCode()) - ); - } - - Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true); - onPaymentFailedError(paymentSetupError); - } else if (chargeFailure != null && subscription.getProcessor() == ActiveSubscription.Processor.BRAINTREE) { - Log.d(TAG, "PayPal charge failure detected: " + chargeFailure, true); - - - int code; - try { - code = Integer.parseInt(chargeFailure.getCode()); - } catch (NumberFormatException e) { - Log.w(TAG, "PayPal charge failure code had unexpected type."); - code = -1; - } - - PayPalDeclineCode declineCode = new PayPalDeclineCode(code); - DonationError.PaymentSetupError paymentSetupError; - PaymentSourceType paymentSourceType = SignalStore.inAppPayments().getSubscriptionPaymentSourceType(); - boolean isPayPalSource = paymentSourceType instanceof PaymentSourceType.PayPal; - - if (declineCode.getKnownCode() != null && isPayPalSource) { - paymentSetupError = new DonationError.PaymentSetupError.PayPalDeclinedError( - getErrorSource(), - new Exception(chargeFailure.getMessage()), - declineCode.getKnownCode() - ); - } else if (isPayPalSource) { - paymentSetupError = new DonationError.PaymentSetupError.PayPalCodedError( - getErrorSource(), - new Exception("Card was declined. " + chargeFailure.getCode()), - code - ); - } else { - paymentSetupError = new DonationError.PaymentSetupError.GenericError( - getErrorSource(), - new Exception("Payment Failed for " + paymentSourceType.getCode()) - ); - } - - Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true); - onPaymentFailedError(paymentSetupError); - } else { - Log.d(TAG, "Not for a keep-alive and we have a failure status. Routing a payment setup error...", true); - onPaymentFailedError(new DonationError.PaymentSetupError.GenericError( - getErrorSource(), - new Exception("Got a failure status from the subscription object.") - )); - } - } - - /** - * Handle 409 error code. This is a permanent failure for new subscriptions but an ignorable error for keep-alive messages. - */ - private void onAlreadyRedeemed(ServiceResponse response) throws Exception { - if (isForKeepAlive) { - Log.i(TAG, "KeepAlive: Latest paid receipt on subscription already redeemed with a different request credential, ignoring.", response.getApplicationError().get(), true); - setOutputData(new JsonJobData.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_KEEP_ALIVE_409, true).serialize()); - } else { - Log.w(TAG, "Latest paid receipt on subscription already redeemed with a different request credential.", response.getApplicationError().get(), true); - onGenericRedemptionError(); - throw new Exception(response.getApplicationError().get()); - } - } - - private DonationErrorSource getErrorSource() { - return isForKeepAlive ? DonationErrorSource.KEEP_ALIVE : DonationErrorSource.MONTHLY; - } - - /** - * 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 90 days from now - */ - private static boolean isCredentialValid(@NonNull ActiveSubscription.Subscription subscription, @NonNull ReceiptCredential receiptCredential) { - long now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); - long maxExpirationTime = now + TimeUnit.DAYS.toSeconds(90); - boolean isSameLevel = subscription.getLevel() == receiptCredential.getReceiptLevel(); - boolean isExpirationAfterSub = subscription.getEndOfCurrentPeriod() < receiptCredential.getReceiptExpirationTime(); - boolean isExpiration86400 = receiptCredential.getReceiptExpirationTime() % 86400 == 0; - boolean isExpirationInTheFuture = receiptCredential.getReceiptExpirationTime() > now; - boolean isExpirationWithinMax = receiptCredential.getReceiptExpirationTime() <= maxExpirationTime; - - Log.d(TAG, "Credential validation: isSameLevel(" + isSameLevel + - ") isExpirationAfterSub(" + isExpirationAfterSub + - ") isExpiration86400(" + isExpiration86400 + - ") isExpirationInTheFuture(" + isExpirationInTheFuture + - ") isExpirationWithinMax(" + isExpirationWithinMax + ")", true); - - return isSameLevel && isExpirationAfterSub && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinMax; - } - - @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 { - @Override - public @NonNull SubscriptionReceiptRequestResponseJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - JsonJobData data = JsonJobData.deserialize(serializedData); - - SubscriberId subscriberId = SubscriberId.fromBytes(data.getStringAsBlob(DATA_SUBSCRIBER_ID)); - boolean isForKeepAlive = data.getBooleanOrDefault(DATA_IS_FOR_KEEP_ALIVE, false); - String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null); - byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null; - long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L); - byte[] rawTerminalDonation = data.getStringAsBlob(DATA_TERMINAL_DONATION); - - ReceiptCredentialRequestContext requestContext; - if (requestContextBytes != null && SignalStore.inAppPayments().getSubscriptionRequestCredential() == null) { - try { - requestContext = new ReceiptCredentialRequestContext(requestContextBytes); - SignalStore.inAppPayments().setSubscriptionRequestCredential(requestContext); - } catch (InvalidInputException e) { - Log.e(TAG, "Failed to generate request context from bytes", e); - throw new AssertionError(e); - } - } - - TerminalDonationQueue.TerminalDonation terminalDonation = null; - if (rawTerminalDonation != null) { - try { - terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation); - } catch (IOException e) { - Log.e(TAG, "Failed to parse terminal donation. Generating a default."); - } - } - - if (terminalDonation == null) { - terminalDonation = new TerminalDonationQueue.TerminalDonation( - -1, - false, - null, - ByteString.EMPTY - ); - } - - return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, terminalDonation); - } - } -}