diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java index 4cb2170ca2..ad8719dc4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -76,6 +76,17 @@ public class DonationReceiptRedemptionJob extends BaseJob { .build()); } + public static JobManager.Chain createJobChainForKeepAlive() { + DonationReceiptRedemptionJob redemptionJob = createJobForSubscription(DonationErrorSource.KEEP_ALIVE); + RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob(); + MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); + + return ApplicationDependencies.getJobManager() + .startChain(redemptionJob) + .then(refreshOwnProfileJob) + .then(multiDeviceProfileContentUpdateJob); + } + public static JobManager.Chain createJobChainForGift(long messageId, boolean primary) { DonationReceiptRedemptionJob redeemReceiptJob = new DonationReceiptRedemptionJob( messageId, @@ -139,6 +150,16 @@ public class DonationReceiptRedemptionJob extends BaseJob { @Override protected void onRun() throws Exception { + if (isForSubscription()) { + synchronized (SubscriptionReceiptRequestResponseJob.MUTEX) { + doRun(); + } + } else { + doRun(); + } + } + + private void doRun() throws Exception { ReceiptCredentialPresentation presentation = getPresentation(); if (presentation == null) { Log.d(TAG, "No presentation available. Exiting.", true); @@ -170,6 +191,11 @@ public class DonationReceiptRedemptionJob extends BaseJob { if (isForSubscription()) { Log.d(TAG, "Clearing subscription failure", true); SignalStore.donationsValues().clearSubscriptionRedemptionFailed(); + Log.i(TAG, "Recording end of period from active subscription", true); + SignalStore.donationsValues() + .setSubscriptionEndOfPeriodRedeemed(SignalStore.donationsValues() + .getSubscriptionEndOfPeriodRedemptionStarted()); + SignalStore.donationsValues().clearSubscriptionReceiptCredential(); } else if (giftMessageId != NO_ID) { Log.d(TAG, "Marking gift redemption completed for " + giftMessageId); SignalDatabase.mms().markGiftRedemptionCompleted(giftMessageId); @@ -182,7 +208,17 @@ public class DonationReceiptRedemptionJob extends BaseJob { } private @Nullable ReceiptCredentialPresentation getPresentation() throws InvalidInputException, NoSuchMessageException { - if (giftMessageId == NO_ID) { + final ReceiptCredentialPresentation receiptCredentialPresentation; + + if (isForSubscription()) { + receiptCredentialPresentation = SignalStore.donationsValues().getSubscriptionReceiptCredential(); + } else { + receiptCredentialPresentation = null; + } + + if (receiptCredentialPresentation != null) { + return receiptCredentialPresentation; + } if (giftMessageId == NO_ID) { return getPresentationFromInputData(); } else { return getPresentationFromGiftMessage(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java index c40d9927c7..7eac0d70a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -116,7 +117,8 @@ public class SubscriptionKeepAliveJob extends BaseJob { return; } - if (activeSubscription.getActiveSubscription().getEndOfCurrentPeriod() > SignalStore.donationsValues().getLastEndOfPeriod()) { + final long endOfCurrentPeriod = activeSubscription.getActiveSubscription().getEndOfCurrentPeriod(); + if (endOfCurrentPeriod > SignalStore.donationsValues().getLastEndOfPeriod()) { Log.i(TAG, String.format(Locale.US, "Last end of period change. Requesting receipt refresh. (old: %d to new: %d)", @@ -124,11 +126,36 @@ public class SubscriptionKeepAliveJob extends BaseJob { activeSubscription.getActiveSubscription().getEndOfCurrentPeriod()), true); - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true).enqueue(); - return; + SignalStore.donationsValues().setLastEndOfPeriod(endOfCurrentPeriod); + SignalStore.donationsValues().clearSubscriptionRequestCredential(); + SignalStore.donationsValues().clearSubscriptionReceiptCredential(); + MultiDeviceSubscriptionSyncRequestJob.enqueue(); } - Log.i(TAG, "Subscription is active, and end of current period (remote) is after the latest checked end of period (local). Nothing to do."); + if (endOfCurrentPeriod > SignalStore.donationsValues().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.donationsValues().setSubscriptionEndOfPeriodConversionStarted(endOfCurrentPeriod); + SignalStore.donationsValues().refreshSubscriptionRequestCredential(); + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true).enqueue(); + } else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedemptionStarted()) { + if (SignalStore.donationsValues().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).enqueue(); + } else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedeemed()) { + if (SignalStore.donationsValues().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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java index ca90c0b494..36153ae2e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -13,7 +13,6 @@ 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.SignalDatabase; @@ -25,13 +24,12 @@ import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.subscription.Subscriber; -import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; -import java.security.SecureRandom; import java.util.concurrent.TimeUnit; /** @@ -50,9 +48,8 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { public static final Object MUTEX = new Object(); - private final ReceiptCredentialRequestContext requestContext; - private final SubscriberId subscriberId; - private final boolean isForKeepAlive; + private final SubscriberId subscriberId; + private final boolean isForKeepAlive; private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive) { return new SubscriptionReceiptRequestResponseJob( @@ -64,28 +61,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build(), - generateRequestContext(), subscriberId, isForKeepAlive ); } - private static ReceiptCredentialRequestContext generateRequestContext() { - Log.d(TAG, "Generating request credentials context for token redemption...", true); - SecureRandom secureRandom = new SecureRandom(); - byte[] randomBytes = Util.getSecretBytes(ReceiptSerial.SIZE); - - try { - ReceiptSerial receiptSerial = new ReceiptSerial(randomBytes); - ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations(); - - return operations.createReceiptCredentialRequestContext(secureRandom, receiptSerial); - } catch (InvalidInputException | VerificationFailedException e) { - Log.e(TAG, "Failed to create credential.", e); - throw new AssertionError(e); - } - } - public static JobManager.Chain createSubscriptionContinuationJobChain() { return createSubscriptionContinuationJobChain(false); } @@ -105,12 +85,10 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { } private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters, - @NonNull ReceiptCredentialRequestContext requestContext, @NonNull SubscriberId subscriberId, boolean isForKeepAlive) { super(parameters); - this.requestContext = requestContext; this.subscriberId = subscriberId; this.isForKeepAlive = isForKeepAlive; } @@ -118,8 +96,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { @Override public @NonNull Data serialize() { Data.Builder builder = new Data.Builder().putBlobAsString(DATA_SUBSCRIBER_ID, subscriberId.getBytes()) - .putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive) - .putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize()); + .putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive); return builder.build(); } @@ -141,9 +118,15 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { } private void doRun() throws Exception { + ReceiptCredentialRequestContext requestContext = SignalStore.donationsValues().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(); @@ -180,9 +163,17 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { Log.w(TAG, "Subscription is marked as cancelled, but it's possible that the user cancelled and then later tried to resubscribe. Scheduling a retry.", true); throw new RetryableException(); } else { - Log.i(TAG, "Recording end of period from active subscription: " + subscription.getStatus(), true); - SignalStore.donationsValues().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod()); - MultiDeviceSubscriptionSyncRequestJob.enqueue(); + Log.i(TAG, "Subscription is valid, proceeding with request for ReceiptCredentialResponse", true); + long storedEndOfPeriod = SignalStore.donationsValues().getLastEndOfPeriod(); + if (storedEndOfPeriod < subscription.getEndOfCurrentPeriod()) { + SignalStore.donationsValues().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod()); + MultiDeviceSubscriptionSyncRequestJob.enqueue(); + } + + if (SignalStore.donationsValues().getSubscriptionEndOfPeriodConversionStarted() == 0L) { + Log.i(TAG, "Marking the start of initial conversion.", true); + SignalStore.donationsValues().setSubscriptionEndOfPeriodConversionStarted(subscription.getEndOfCurrentPeriod()); + } } Log.d(TAG, "Submitting receipt credential request."); @@ -192,7 +183,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { if (response.getApplicationError().isPresent()) { handleApplicationError(response); } else if (response.getResult().isPresent()) { - ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get()); + ReceiptCredential receiptCredential = getReceiptCredential(requestContext, response.getResult().get()); if (!isCredentialValid(subscription, receiptCredential)) { DonationError.routeDonationError(context, DonationError.genericBadgeRedemptionFailure(getErrorSource())); @@ -204,9 +195,9 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { Log.d(TAG, "Validated credential. Recording receipt and handing off to redemption job.", true); SignalDatabase.donationReceipts().addReceipt(DonationReceiptRecord.createForSubscription(subscription)); - setOutputData(new Data.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION, - receiptCredentialPresentation.serialize()) - .build()); + SignalStore.donationsValues().clearSubscriptionRequestCredential(); + SignalStore.donationsValues().setSubscriptionReceiptCredential(receiptCredentialPresentation); + SignalStore.donationsValues().setSubscriptionEndOfPeriodRedemptionStarted(subscription.getEndOfCurrentPeriod()); } else { Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orElse(null), true); throw new RetryableException(); @@ -239,7 +230,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { } } - private ReceiptCredential getReceiptCredential(@NonNull ReceiptCredentialResponse response) throws RetryableException { + private ReceiptCredential getReceiptCredential(@NonNull ReceiptCredentialRequestContext requestContext, @NonNull ReceiptCredentialResponse response) throws RetryableException { ClientZkReceiptOperations operations = ApplicationDependencies.getClientZkReceiptOperations(); try { @@ -282,17 +273,17 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { /** * 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. + * 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. + * linked devices. */ private void onPaymentFailure(@NonNull String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) { SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true); - if (isForKeepAlive){ + if (isForKeepAlive) { Log.d(TAG, "Is for a keep-alive and we have a status. Setting UnexpectedSubscriptionCancelation state...", true); SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(chargeFailure); SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(status); @@ -380,22 +371,21 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { public @NonNull SubscriptionReceiptRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) { SubscriberId subscriberId = SubscriberId.fromBytes(data.getStringAsBlob(DATA_SUBSCRIBER_ID)); boolean isForKeepAlive = data.getBooleanOrDefault(DATA_IS_FOR_KEEP_ALIVE, false); - byte[] requestContextBytes = data.getStringAsBlob(DATA_REQUEST_BYTES); + String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null); + byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null; ReceiptCredentialRequestContext requestContext; - if (requestContextBytes == null) { - Log.i(TAG, "Generating a request context for a legacy instance of SubscriptionReceiptRequestResponseJob", true); - requestContext = generateRequestContext(); - } else { + if (requestContextBytes != null && SignalStore.donationsValues().getSubscriptionRequestCredential() == null) { try { requestContext = new ReceiptCredentialRequestContext(requestContextBytes); + SignalStore.donationsValues().setSubscriptionRequestCredential(requestContext); } catch (InvalidInputException e) { Log.e(TAG, "Failed to generate request context from bytes", e); throw new AssertionError(e); } } - return new SubscriptionReceiptRequestResponseJob(parameters, requestContext, subscriberId, isForKeepAlive); + return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index c3f69b03a2..565a43e129 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -6,17 +6,25 @@ import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.Subject import org.signal.core.util.logging.Log import org.signal.donations.StripeApi +import org.signal.libsignal.zkgroup.InvalidInputException +import org.signal.libsignal.zkgroup.VerificationFailedException +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext +import org.signal.libsignal.zkgroup.receipts.ReceiptSerial import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.payments.currency.CurrencyUtil import org.thoughtcrime.securesms.subscription.LevelUpdateOperation import org.thoughtcrime.securesms.subscription.Subscriber +import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey import org.whispersystems.signalservice.api.subscriptions.SubscriberId import org.whispersystems.signalservice.internal.util.JsonUtil +import java.security.SecureRandom import java.util.Currency import java.util.Locale import java.util.concurrent.TimeUnit @@ -30,7 +38,14 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign private const val KEY_CURRENCY_CODE_ONE_TIME = "donation.currency.code.boost" private const val KEY_SUBSCRIBER_ID_PREFIX = "donation.subscriber.id." private const val KEY_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping" + + /** + * Our last known "end of period" for a subscription. This value is used to determine + * when a user should try to redeem a badge for their subscription, and as a hint that + * a user has an active subscription. + */ private const val KEY_LAST_END_OF_PERIOD_SECONDS = "donation.last.end.of.period" + private const val EXPIRED_BADGE = "donation.expired.badge" private const val EXPIRED_GIFT_BADGE = "donation.expired.gift.badge" private const val USER_MANUALLY_CANCELLED = "donation.user.manually.cancelled" @@ -44,6 +59,42 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign private const val SUBSCRIPTION_CANCELATION_TIMESTAMP = "donation.subscription.cancelation.timestamp" private const val SUBSCRIPTION_CANCELATION_WATERMARK = "donation.subscription.cancelation.watermark" private const val SHOW_CANT_PROCESS_DIALOG = "show.cant.process.dialog" + + /** + * The current request context for subscription. This should be stored until either + * it is successfully converted into a response, the end of period changes, or the user + * manually cancels the subscription. + */ + private const val SUBSCRIPTION_CREDENTIAL_REQUEST = "subscription.credential.request" + + /** + * The current response presentation that can be submitted for a badge. This should be + * stored until it is successfully redeemed, the end of period changes, or the user + * manually cancels their subscription. + */ + private const val SUBSCRIPTION_CREDENTIAL_RECEIPT = "subscription.credential.receipt" + + /** + * Notes the "end of period" time for the latest subscription that we have started + * to get a response presentation for. When this is equal to the latest "end of period" + * it can be assumed that we have a request context that can be safely reused. + */ + private const val SUBSCRIPTION_EOP_STARTED_TO_CONVERT = "subscription.eop.convert" + + /** + * Notes the "end of period" time for the latest subscription that we have started + * to redeem a response presentation for. When this is equal to the latest "end of + * period" it can be assumed that we have a response presentation that we can submit + * to get an active token for. + */ + private const val SUBSCRIPTION_EOP_STARTED_TO_REDEEM = "subscription.eop.redeem" + + /** + * Notes the "end of period" time for the latest subscription that we have successfully + * and fully redeemed a token for. If this is equal to the latest "end of period" it is + * assumed that there is no work to be done. + */ + private const val SUBSCRIPTION_EOP_REDEEMED = "subscription.eop.redeemed" } override fun onFirstEverAppLaunch() = Unit @@ -56,7 +107,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign SUBSCRIPTION_CANCELATION_REASON, SUBSCRIPTION_CANCELATION_TIMESTAMP, SUBSCRIPTION_CANCELATION_WATERMARK, - SHOW_CANT_PROCESS_DIALOG + SHOW_CANT_PROCESS_DIALOG, + SUBSCRIPTION_CREDENTIAL_REQUEST, + SUBSCRIPTION_CREDENTIAL_RECEIPT, + SUBSCRIPTION_EOP_STARTED_TO_CONVERT, + SUBSCRIPTION_EOP_STARTED_TO_REDEEM, + SUBSCRIPTION_EOP_REDEEMED ) private val subscriptionCurrencyPublisher: Subject by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) } @@ -313,6 +369,9 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign unexpectedSubscriptionCancelationReason = null unexpectedSubscriptionCancelationTimestamp = 0L + clearSubscriptionRequestCredential() + clearSubscriptionReceiptCredential() + val expiredBadge = getExpiredBadge() if (expiredBadge != null && expiredBadge.isSubscription()) { Log.d(TAG, "[updateLocalStateForManualCancellation] Clearing expired badge.") @@ -340,6 +399,8 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign setUnexpectedSubscriptionCancelationChargeFailure(null) unexpectedSubscriptionCancelationReason = null unexpectedSubscriptionCancelationTimestamp = 0L + refreshSubscriptionRequestCredential() + clearSubscriptionReceiptCredential() val expiredBadge = getExpiredBadge() if (expiredBadge != null && expiredBadge.isSubscription()) { @@ -348,4 +409,58 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign } } } + + fun refreshSubscriptionRequestCredential() { + putBlob(SUBSCRIPTION_CREDENTIAL_REQUEST, generateRequestCredential().serialize()) + } + + fun setSubscriptionRequestCredential(requestContext: ReceiptCredentialRequestContext) { + putBlob(SUBSCRIPTION_CREDENTIAL_REQUEST, requestContext.serialize()) + } + + fun getSubscriptionRequestCredential(): ReceiptCredentialRequestContext? { + val bytes = getBlob(SUBSCRIPTION_CREDENTIAL_REQUEST, null) ?: return null + + return ReceiptCredentialRequestContext(bytes) + } + + fun clearSubscriptionRequestCredential() { + remove(SUBSCRIPTION_CREDENTIAL_REQUEST) + } + + fun setSubscriptionReceiptCredential(receiptCredentialPresentation: ReceiptCredentialPresentation) { + putBlob(SUBSCRIPTION_CREDENTIAL_RECEIPT, receiptCredentialPresentation.serialize()) + } + + fun getSubscriptionReceiptCredential(): ReceiptCredentialPresentation? { + val bytes = getBlob(SUBSCRIPTION_CREDENTIAL_RECEIPT, null) ?: return null + + return ReceiptCredentialPresentation(bytes) + } + + fun clearSubscriptionReceiptCredential() { + remove(SUBSCRIPTION_CREDENTIAL_RECEIPT) + } + + var subscriptionEndOfPeriodConversionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_CONVERT, 0L) + var subscriptionEndOfPeriodRedemptionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_REDEEM, 0L) + var subscriptionEndOfPeriodRedeemed by longValue(SUBSCRIPTION_EOP_REDEEMED, 0L) + + private fun generateRequestCredential(): ReceiptCredentialRequestContext { + Log.d(TAG, "Generating request credentials context for token redemption...", true) + val secureRandom = SecureRandom() + val randomBytes = Util.getSecretBytes(ReceiptSerial.SIZE) + + return try { + val receiptSerial = ReceiptSerial(randomBytes) + val operations = ApplicationDependencies.getClientZkReceiptOperations() + operations.createReceiptCredentialRequestContext(secureRandom, receiptSerial) + } catch (e: InvalidInputException) { + Log.e(TAG, "Failed to create credential.", e) + throw AssertionError(e) + } catch (e: VerificationFailedException) { + Log.e(TAG, "Failed to create credential.", e) + throw AssertionError(e) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java index fd95a58adb..b9da5e61ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java @@ -24,13 +24,18 @@ final class LogSectionBadges implements LogSection { return "Self not yet available!"; } - return new StringBuilder().append("Badge Count : ").append(Recipient.self().getBadges().size()).append("\n") - .append("ExpiredBadge : ").append(SignalStore.donationsValues().getExpiredBadge() != null).append("\n") - .append("LastKeepAliveLaunchTime : ").append(SignalStore.donationsValues().getLastKeepAliveLaunchTime()).append("\n") - .append("LastEndOfPeriod : ").append(SignalStore.donationsValues().getLastEndOfPeriod()).append("\n") - .append("IsUserManuallyCancelled : ").append(SignalStore.donationsValues().isUserManuallyCancelled()).append("\n") - .append("DisplayBadgesOnProfile : ").append(SignalStore.donationsValues().getDisplayBadgesOnProfile()).append("\n") - .append("SubscriptionRedemptionFailed : ").append(SignalStore.donationsValues().getSubscriptionRedemptionFailed()).append("\n") - .append("ShouldCancelBeforeNextAttempt: ").append(SignalStore.donationsValues().getShouldCancelSubscriptionBeforeNextSubscribeAttempt()).append("\n"); + return new StringBuilder().append("Badge Count : ").append(Recipient.self().getBadges().size()).append("\n") + .append("ExpiredBadge : ").append(SignalStore.donationsValues().getExpiredBadge() != null).append("\n") + .append("LastKeepAliveLaunchTime : ").append(SignalStore.donationsValues().getLastKeepAliveLaunchTime()).append("\n") + .append("LastEndOfPeriod : ").append(SignalStore.donationsValues().getLastEndOfPeriod()).append("\n") + .append("SubscriptionEndOfPeriodConversionStarted: ").append(SignalStore.donationsValues().getSubscriptionEndOfPeriodConversionStarted()).append("\n") + .append("SubscriptionEndOfPeriodRedemptionStarted: ").append(SignalStore.donationsValues().getSubscriptionEndOfPeriodRedemptionStarted()).append("\n") + .append("SubscriptionEndOfPeriodRedeemed : ").append(SignalStore.donationsValues().getSubscriptionEndOfPeriodRedeemed()).append("\n") + .append("IsUserManuallyCancelled : ").append(SignalStore.donationsValues().isUserManuallyCancelled()).append("\n") + .append("DisplayBadgesOnProfile : ").append(SignalStore.donationsValues().getDisplayBadgesOnProfile()).append("\n") + .append("SubscriptionRedemptionFailed : ").append(SignalStore.donationsValues().getSubscriptionRedemptionFailed()).append("\n") + .append("ShouldCancelBeforeNextAttempt : ").append(SignalStore.donationsValues().getShouldCancelSubscriptionBeforeNextSubscribeAttempt()).append("\n") + .append("Has unconverted request context : ").append(SignalStore.donationsValues().getSubscriptionRequestCredential() != null).append("\n") + .append("Has unredeemed receipt presentation : ").append(SignalStore.donationsValues().getSubscriptionReceiptCredential() != null).append("\n"); } }