diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt index 560e0a09e7..92e100bd67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeRepository.kt @@ -13,8 +13,11 @@ class BadgeRepository(context: Context) { private val context = context.applicationContext - fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction { - val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) } + fun setVisibilityForAllBadges( + displayBadgesOnProfile: Boolean, + selfBadges: List = Recipient.self().badges + ): Completable = Completable.fromAction { + val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) } ProfileUtil.uploadProfileWithBadges(context, badges) val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt index 58249505a7..531744c006 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt @@ -154,18 +154,30 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet val subscriber = SignalStore.donationsValues().requireSubscriber() Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true) - ApplicationDependencies.getDonationsService().updateSubscriptionLevel( subscriber.subscriberId, subscriptionLevel, subscriber.currencyCode, - levelUpdateOperation.idempotencyKey.serialize() - ).flatMap(ServiceResponse::flattenResult).ignoreElement().andThen { - Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel", true) - SignalStore.donationsValues().clearUserManuallyCancelled() - SignalStore.donationsValues().clearLevelOperation() - LevelUpdate.updateProcessingState(false) - it.onComplete() + levelUpdateOperation.idempotencyKey.serialize(), + SubscriptionReceiptRequestResponseJob.MUTEX + ).flatMapCompletable { + if (it.status == 200 || it.status == 204) { + Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true) + SignalStore.donationsValues().clearUserManuallyCancelled() + SignalStore.donationsValues().clearLevelOperations() + LevelUpdate.updateProcessingState(false) + Completable.complete() + } else { + if (it.applicationError.isPresent) { + Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel with response code ${it.status}", it.applicationError.get(), true) + SignalStore.donationsValues().clearLevelOperations() + } else { + Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel", it.executionError.orNull(), true) + } + + LevelUpdate.updateProcessingState(false) + it.flattenResult().ignoreElement() + } }.andThen { Log.d(TAG, "Enqueuing request response job chain.", true) val jobId = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() @@ -205,14 +217,13 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet } } }.doOnError { - SignalStore.donationsValues().clearLevelOperation() LevelUpdate.updateProcessingState(false) }.subscribeOn(Schedulers.io()) } private fun getOrCreateLevelUpdateOperation(subscriptionLevel: String): Single = Single.fromCallable { - val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation() - if (levelUpdateOperation == null || subscriptionLevel != levelUpdateOperation.level) { + val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel) + if (levelUpdateOperation == null) { val newOperation = LevelUpdateOperation( idempotencyKey = IdempotencyKey.generate(), level = subscriptionLevel diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt index 89a6571574..b6c54b12c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt @@ -145,7 +145,7 @@ class SubscribeViewModel( onComplete = { eventPublisher.onNext(DonationEvent.SubscriptionCancelled) SignalStore.donationsValues().setLastEndOfPeriod(0L) - SignalStore.donationsValues().clearLevelOperation() + SignalStore.donationsValues().clearLevelOperations() SignalStore.donationsValues().markUserManuallyCancelled() refreshActiveSubscription() store.update { it.copy(stage = SubscribeState.Stage.READY) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt index dfec331018..ef772e94f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt @@ -150,7 +150,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh findNavController().popBackStack() } else { requireActivity().finish() - requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext())) + requireActivity().startActivity(AppSettingsActivity.manageSubscriptions(requireContext())) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index d5266bf076..3ba94c606f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -224,14 +224,7 @@ public class RefreshOwnProfileJob extends BaseJob { Log.d(TAG, "Detected mixed visibility of badges. Telling the server to mark them all visible.", true); BadgeRepository badgeRepository = new BadgeRepository(context); - badgeRepository.setVisibilityForAllBadges(true); - - DatabaseFactory.getRecipientDatabase(context) - .setBadges(Recipient.self().getId(), - appBadges.stream() - .map(Badge::setVisible) - .collect(Collectors.toList())); - + badgeRepository.setVisibilityForAllBadges(true, appBadges).blockingSubscribe(); } else { DatabaseFactory.getRecipientDatabase(context) .setBadges(Recipient.self().getId(), appBadges); 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 17ad230cb1..33b867147e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -41,6 +41,8 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { private static final String DATA_REQUEST_BYTES = "data.request.bytes"; private static final String DATA_SUBSCRIBER_ID = "data.subscriber.id"; + public static final Object MUTEX = new Object(); + private ReceiptCredentialRequestContext requestContext; private final SubscriberId subscriberId; @@ -107,6 +109,12 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { @Override protected void onRun() throws Exception { + synchronized (MUTEX) { + doRun(); + } + } + + private void doRun() throws Exception { ActiveSubscription.Subscription subscription = getLatestSubscriptionInformation(); if (subscription == null || !subscription.isActive()) { Log.d(TAG, "User does not have an active subscription. Exiting.", true); 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 b091e66ea7..96e673874a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -23,12 +23,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign private const val KEY_SUBSCRIPTION_CURRENCY_CODE = "donation.currency.code" private const val KEY_CURRENCY_CODE_BOOST = "donation.currency.code.boost" private const val KEY_SUBSCRIBER_ID_PREFIX = "donation.subscriber.id." - private const val KEY_IDEMPOTENCY = "donation.idempotency.key" - private const val KEY_LEVEL = "donation.level" private const val KEY_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping" private const val KEY_LAST_END_OF_PERIOD = "donation.last.end.of.period" private const val EXPIRED_BADGE = "donation.expired.badge" private const val USER_MANUALLY_CANCELLED = "donation.user.manually.cancelled" + private const val KEY_LEVEL_OPERATION_PREFIX = "donation.level.operation." + private const val KEY_LEVEL_HISTORY = "donation.level.history" } override fun onFirstEverAppLaunch() = Unit @@ -114,29 +114,36 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign subscriptionCurrencyPublisher.onNext(Currency.getInstance(currencyCode)) } - fun getLevelOperation(): LevelUpdateOperation? { - val level = getString(KEY_LEVEL, null) - val idempotencyKey = getBlob(KEY_IDEMPOTENCY, null)?.let { IdempotencyKey.fromBytes(it) } - - return if (level == null || idempotencyKey == null) { - null + fun getLevelOperation(level: String): LevelUpdateOperation? { + val idempotencyKey = getBlob("${KEY_LEVEL_OPERATION_PREFIX}$level", null) + return if (idempotencyKey != null) { + LevelUpdateOperation(IdempotencyKey.fromBytes(idempotencyKey), level) } else { - LevelUpdateOperation(idempotencyKey, level) + null } } fun setLevelOperation(levelUpdateOperation: LevelUpdateOperation) { - store.beginWrite() - .putString(KEY_LEVEL, levelUpdateOperation.level) - .putBlob(KEY_IDEMPOTENCY, levelUpdateOperation.idempotencyKey.bytes) - .apply() + addLevelToHistory(levelUpdateOperation.level) + putBlob("$KEY_LEVEL_OPERATION_PREFIX${levelUpdateOperation.level}", levelUpdateOperation.idempotencyKey.bytes) } - fun clearLevelOperation() { - store.beginWrite() - .remove(KEY_LEVEL) - .remove(KEY_IDEMPOTENCY) - .apply() + private fun getLevelHistory(): Set { + return getString(KEY_LEVEL_HISTORY, "").split(",").toSet() + } + + private fun addLevelToHistory(level: String) { + val levels = getLevelHistory() + level + putString(KEY_LEVEL_HISTORY, levels.joinToString(",")) + } + + fun clearLevelOperations() { + val levelHistory = getLevelHistory() + val write = store.beginWrite() + for (level in levelHistory) { + write.remove("${KEY_LEVEL_OPERATION_PREFIX}$level") + } + write.apply() } fun setExpiredBadge(badge: Badge?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index e760289c02..3edd292eaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -427,7 +427,7 @@ public final class FeatureFlags { * Whether or not donor badges should be displayed throughout the app. */ public static boolean displayDonorBadges() { - return getBoolean(DONOR_BADGES_DISPLAY, false); + return getBoolean(DONOR_BADGES_DISPLAY, Environment.IS_STAGING); } public static boolean cdsh() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java index 1d22bd2c45..1d59cb5fa2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -125,10 +125,18 @@ public class DonationsService { * @param level The new level to subscribe to * @param currencyCode The currencyCode the user is using for payment * @param idempotencyKey url-safe-base64-encoded random 16-byte value (see description) + * @param mutex A mutex to lock on to avoid a situation where this subscription update happens *as* we are trying to get a credential receipt. */ - public Single> updateSubscriptionLevel(SubscriberId subscriberId, String level, String currencyCode, String idempotencyKey) { + public Single> updateSubscriptionLevel(SubscriberId subscriberId, + String level, + String currencyCode, + String idempotencyKey, + Object mutex + ) { return createServiceResponse(() -> { - pushServiceSocket.updateSubscriptionLevel(subscriberId.serialize(), level, currencyCode, idempotencyKey); + synchronized(mutex) { + pushServiceSocket.updateSubscriptionLevel(subscriberId.serialize(), level, currencyCode, idempotencyKey); + } return new Pair<>(EmptyResponse.INSTANCE, 200); }); }