kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement better state management and recoverability for donation badge jobs.
rodzic
23a328f12d
commit
4620eade58
|
@ -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();
|
||||
|
|
|
@ -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 <T> void verifyResponse(@NonNull ServiceResponse<T> response) throws Exception {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* There are two ways this could go, depending on whether the job was created for a keep-alive chain.
|
||||
*
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Currency> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue