mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Implement bank transfer completed sheet.
This commit is contained in:
committed by
Cody Henthorne
parent
89199b81ab
commit
bf37c09ba0
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.completed
|
||||
|
||||
import android.content.DialogInterface
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Bottom Sheet displayed when the app notices that a long-running donation has
|
||||
* completed.
|
||||
*/
|
||||
class DonationCompletedBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_DONATION_COMPLETED = "arg.donation.completed"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, donationCompleted: DonationCompletedQueue.DonationCompleted) {
|
||||
DonationCompletedBottomSheet().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_DONATION_COMPLETED to donationCompleted.encode()
|
||||
)
|
||||
|
||||
show(fragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val donationCompleted: DonationCompletedQueue.DonationCompleted by lazy(LazyThreadSafetyMode.NONE) {
|
||||
DonationCompletedQueue.DonationCompleted.ADAPTER.decode(requireArguments().getByteArray(ARG_DONATION_COMPLETED)!!)
|
||||
}
|
||||
|
||||
private val viewModel: DonationCompletedViewModel by viewModel {
|
||||
DonationCompletedViewModel(donationCompleted, badgeRepository = BadgeRepository(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val badge by viewModel.badge
|
||||
val isToggleChecked by viewModel.isToggleChecked
|
||||
val toggleType by viewModel.toggleType
|
||||
|
||||
DonationCompletedSheetContent(
|
||||
badge = badge,
|
||||
isToggleChecked = isToggleChecked,
|
||||
toggleType = toggleType,
|
||||
onCheckChanged = viewModel::onToggleCheckChanged,
|
||||
onDoneClick = { dismissAllowingStateLoss() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
|
||||
viewModel.commitToggleState()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DonationCompletedSheetContentPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
DonationCompletedSheetContent(
|
||||
badge = null,
|
||||
isToggleChecked = false,
|
||||
toggleType = DonationCompletedViewModel.ToggleType.NONE,
|
||||
onCheckChanged = {},
|
||||
onDoneClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DonationCompletedSheetContent(
|
||||
badge: Badge?,
|
||||
isToggleChecked: Boolean,
|
||||
toggleType: DonationCompletedViewModel.ToggleType,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
onDoneClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
BadgeImage112(
|
||||
badge = badge,
|
||||
modifier = Modifier
|
||||
.padding(top = 21.dp, bottom = 16.dp)
|
||||
.size(80.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationCompletedBottomSheet__donation_complete),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 45.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationCompleteBottomSheet__your_bank_transfer_was_received),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, bottom = 24.dp)
|
||||
.padding(horizontal = 45.dp)
|
||||
)
|
||||
|
||||
if (toggleType == DonationCompletedViewModel.ToggleType.NONE) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
DonationToggleRow(
|
||||
checked = isToggleChecked,
|
||||
text = stringResource(id = toggleType.copyId),
|
||||
onCheckChanged = onCheckChanged
|
||||
)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onDoneClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(top = 48.dp, bottom = 56.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationPendingBottomSheet__done)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DonationToggleRow(
|
||||
checked: Boolean,
|
||||
text: String,
|
||||
onCheckChanged: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(vertical = 16.dp)
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckChanged,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.completed
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import java.util.Locale
|
||||
|
||||
class DonationCompletedRepository(
|
||||
private val donationsService: DonationsService = ApplicationDependencies.getDonationsService()
|
||||
) {
|
||||
fun getBadge(donationCompleted: DonationCompletedQueue.DonationCompleted): Single<Badge> {
|
||||
return Single
|
||||
.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.levels[donationCompleted.level.toInt()]!! }
|
||||
.map { Badges.fromServiceBadge(it.badge) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.completed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class DonationCompletedViewModel(
|
||||
donationCompleted: DonationCompletedQueue.DonationCompleted,
|
||||
repository: DonationCompletedRepository = DonationCompletedRepository(),
|
||||
private val badgeRepository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonationCompletedViewModel::class.java)
|
||||
}
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val internalBadge = mutableStateOf<Badge?>(null)
|
||||
private val internalToggleChecked = mutableStateOf(false)
|
||||
private val internalToggleType = mutableStateOf(ToggleType.NONE)
|
||||
|
||||
val badge: State<Badge?> = internalBadge
|
||||
val isToggleChecked: State<Boolean> = internalToggleChecked
|
||||
val toggleType: State<ToggleType> = internalToggleType
|
||||
|
||||
init {
|
||||
disposables += repository.getBadge(donationCompleted)
|
||||
.map { badge ->
|
||||
val hasOtherBadges = Recipient.self().badges.filterNot { it.id == badge.id }.isNotEmpty()
|
||||
val isDisplayingBadges = SignalStore.donationsValues().getDisplayBadgesOnProfile()
|
||||
|
||||
val toggleType = when {
|
||||
hasOtherBadges && isDisplayingBadges -> ToggleType.MAKE_FEATURED_BADGE
|
||||
else -> ToggleType.DISPLAY_ON_PROFILE
|
||||
}
|
||||
|
||||
badge to toggleType
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(
|
||||
onSuccess = { (badge, toggleType) ->
|
||||
internalBadge.value = badge
|
||||
internalToggleType.value = toggleType
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleCheckChanged(isChecked: Boolean) {
|
||||
internalToggleChecked.value = isChecked
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that the intention here is that these are able to complete outside of the scope of the ViewModel's lifecycle.
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
fun commitToggleState() {
|
||||
when (toggleType.value) {
|
||||
ToggleType.NONE -> Unit
|
||||
ToggleType.MAKE_FEATURED_BADGE -> {
|
||||
badgeRepository.setVisibilityForAllBadges(isToggleChecked.value).subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Failure while updating badge visibility", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ToggleType.DISPLAY_ON_PROFILE -> {
|
||||
val badge = this.badge.value
|
||||
if (badge == null) {
|
||||
Log.w(TAG, "No badge!")
|
||||
return
|
||||
}
|
||||
|
||||
badgeRepository.setFeaturedBadge(badge).subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Failure while updating featured badge", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
enum class ToggleType(@StringRes val copyId: Int) {
|
||||
NONE(-1),
|
||||
MAKE_FEATURED_BADGE(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__make_featured_badge),
|
||||
DISPLAY_ON_PROFILE(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__display_on_profile)
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UsernameOutOfSyncReminder;
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.DonationCompletedBottomSheet;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
@@ -133,6 +134,7 @@ import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs;
|
||||
@@ -521,6 +523,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure(),
|
||||
getParentFragmentManager());
|
||||
}
|
||||
} else {
|
||||
List<DonationCompletedQueue.DonationCompleted> donationCompletedList = SignalStore.donationsValues().consumeDonationCompletionList();
|
||||
for (DonationCompletedQueue.DonationCompleted donationCompleted : donationCompletedList) {
|
||||
DonationCompletedBottomSheet.show(getParentFragmentManager(), donationCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@ 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.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
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.SubscriptionLevels;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
@@ -59,8 +61,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
private final String paymentIntentId;
|
||||
private final long badgeLevel;
|
||||
private final DonationProcessor donationProcessor;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunning;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
|
||||
private static String resolveQueue(DonationErrorSource donationErrorSource, boolean isLongRunning) {
|
||||
String baseQueue = donationErrorSource == DonationErrorSource.BOOST ? BOOST_QUEUE : GIFT_QUEUE;
|
||||
@@ -96,7 +98,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
boolean isLongRunning)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, isLongRunning);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey, isLongRunning);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
||||
@@ -131,16 +133,16 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
boolean isLongRunningDonationPaymentType)
|
||||
{
|
||||
super(parameters);
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.donationErrorSource = donationErrorSource;
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunning = isLongRunning;
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.donationErrorSource = donationErrorSource;
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunningDonationPaymentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,7 +152,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
.putLong(DATA_BADGE_LEVEL, badgeLevel)
|
||||
.putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode())
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunning);
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType);
|
||||
|
||||
if (requestContext != null) {
|
||||
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
|
||||
@@ -170,7 +172,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (isLongRunning) {
|
||||
if (isLongRunningDonationPaymentType) {
|
||||
return TimeUnit.DAYS.toMillis(1);
|
||||
} else {
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
@@ -214,12 +216,24 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
setOutputData(new JsonJobData.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION,
|
||||
receiptCredentialPresentation.serialize())
|
||||
.serialize());
|
||||
|
||||
enqueueDonationComplete(receiptCredentialPresentation.getReceiptLevel());
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orElse(null), true);
|
||||
throw new RetryableException();
|
||||
}
|
||||
}
|
||||
|
||||
private void enqueueDonationComplete(long receiptLevel) {
|
||||
if (donationErrorSource != DonationErrorSource.GIFT || !isLongRunningDonationPaymentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignalStore.donationsValues().appendToDonationCompletionList(
|
||||
new DonationCompletedQueue.DonationCompleted.Builder().level(receiptLevel).build()
|
||||
);
|
||||
}
|
||||
|
||||
private void handleApplicationError(Context context, ServiceResponse<ReceiptCredentialResponse> response, @NonNull DonationErrorSource donationErrorSource) throws Exception {
|
||||
Throwable applicationException = response.getApplicationError().get();
|
||||
switch (response.getStatus()) {
|
||||
@@ -304,22 +318,22 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
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.BOOST.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);
|
||||
boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.BOOST.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);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
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, isLongRunning);
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
} else {
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunning);
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
@@ -44,18 +45,21 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
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";
|
||||
public static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
|
||||
private final long giftMessageId;
|
||||
private final boolean makePrimary;
|
||||
private final DonationErrorSource errorSource;
|
||||
private final long uiSessionKey;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
|
||||
public static DonationReceiptRedemptionJob createJobForSubscription(@NonNull DonationErrorSource errorSource, long uiSessionKey) {
|
||||
public static DonationReceiptRedemptionJob createJobForSubscription(@NonNull DonationErrorSource errorSource, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return new DonationReceiptRedemptionJob(
|
||||
NO_ID,
|
||||
false,
|
||||
errorSource,
|
||||
uiSessionKey,
|
||||
isLongRunningDonationPaymentType,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
@@ -66,12 +70,13 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
.build());
|
||||
}
|
||||
|
||||
public static DonationReceiptRedemptionJob createJobForBoost(long uiSessionKey) {
|
||||
public static DonationReceiptRedemptionJob createJobForBoost(long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return new DonationReceiptRedemptionJob(
|
||||
NO_ID,
|
||||
false,
|
||||
DonationErrorSource.BOOST,
|
||||
uiSessionKey,
|
||||
isLongRunningDonationPaymentType,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
@@ -82,7 +87,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
}
|
||||
|
||||
public static JobManager.Chain createJobChainForKeepAlive() {
|
||||
DonationReceiptRedemptionJob redemptionJob = createJobForSubscription(DonationErrorSource.KEEP_ALIVE, -1L);
|
||||
DonationReceiptRedemptionJob redemptionJob = createJobForSubscription(DonationErrorSource.KEEP_ALIVE, -1L, false);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
||||
@@ -98,6 +103,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
primary,
|
||||
DonationErrorSource.GIFT_REDEMPTION,
|
||||
-1L,
|
||||
false,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
@@ -115,12 +121,13 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
.then(multiDeviceProfileContentUpdateJob);
|
||||
}
|
||||
|
||||
private DonationReceiptRedemptionJob(long giftMessageId, boolean primary, @NonNull DonationErrorSource errorSource, long uiSessionKey, @NonNull Job.Parameters parameters) {
|
||||
private DonationReceiptRedemptionJob(long giftMessageId, boolean primary, @NonNull DonationErrorSource errorSource, long uiSessionKey, boolean isLongRunning, @NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
this.giftMessageId = giftMessageId;
|
||||
this.makePrimary = primary;
|
||||
this.errorSource = errorSource;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +137,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
.putLong(DATA_GIFT_MESSAGE_ID, giftMessageId)
|
||||
.putBoolean(DATA_PRIMARY, makePrimary)
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
@@ -201,6 +209,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully redeemed token with response code " + response.getStatus() + "... isForSubscription: " + isForSubscription(), true);
|
||||
enqueueDonationComplete(presentation.getReceiptLevel());
|
||||
|
||||
if (isForSubscription()) {
|
||||
Log.d(TAG, "Clearing subscription failure", true);
|
||||
@@ -278,6 +287,22 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
return Objects.equals(getParameters().getQueue(), SUBSCRIPTION_QUEUE);
|
||||
}
|
||||
|
||||
private void enqueueDonationComplete(long receiptLevel) {
|
||||
if (errorSource == DonationErrorSource.GIFT || errorSource == DonationErrorSource.GIFT_REDEMPTION) {
|
||||
Log.i(TAG, "Skipping donation complete sheet for GIFT related redemption.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiSessionKey == -1L || !isLongRunningDonationPaymentType) {
|
||||
Log.i(TAG, "Skipping donation complete sheet for state " + uiSessionKey + ", " + isLongRunningDonationPaymentType);
|
||||
return;
|
||||
}
|
||||
|
||||
SignalStore.donationsValues().appendToDonationCompletionList(
|
||||
new DonationCompletedQueue.DonationCompleted.Builder().level(receiptLevel).build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof RetryableException;
|
||||
@@ -291,13 +316,14 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
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);
|
||||
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);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
return new DonationReceiptRedemptionJob(messageId, primary, errorSource, uiSessionKey, parameters);
|
||||
return new DonationReceiptRedemptionJob(messageId, primary, errorSource, uiSessionKey, isLongRunningDonationPaymentType, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,34 +54,34 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
|
||||
private final SubscriberId subscriberId;
|
||||
private final boolean isForKeepAlive;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunning;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) {
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return new SubscriptionReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("ReceiptRedemption")
|
||||
.setMaxInstancesForQueue(1)
|
||||
.setLifespan(isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1))
|
||||
.setLifespan(isLongRunningDonationPaymentType ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
subscriberId,
|
||||
isForKeepAlive,
|
||||
uiSessionKey,
|
||||
isLongRunning
|
||||
isLongRunningDonationPaymentType
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, boolean isLongRunning) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey, isLongRunning);
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) {
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, isLongRunning);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey);
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey, isLongRunningDonationPaymentType);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
||||
@@ -96,13 +96,13 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
@NonNull SubscriberId subscriberId,
|
||||
boolean isForKeepAlive,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
boolean isLongRunningDonationPaymentType)
|
||||
{
|
||||
super(parameters);
|
||||
this.subscriberId = subscriberId;
|
||||
this.isForKeepAlive = isForKeepAlive;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunning = isLongRunning;
|
||||
this.subscriberId = subscriberId;
|
||||
this.isForKeepAlive = isForKeepAlive;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunningDonationPaymentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,7 +110,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
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)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunning);
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType);
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
@@ -437,12 +437,12 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
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);
|
||||
boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
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);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
ReceiptCredentialRequestContext requestContext;
|
||||
if (requestContextBytes != null && SignalStore.donationsValues().getSubscriptionRequestCredential() == null) {
|
||||
@@ -455,7 +455,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, isLongRunning);
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
@@ -108,6 +109,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
* awaiting the background task.
|
||||
*/
|
||||
private const val IS_GOOGLE_PAY_READY = "subscription.is.google.pay.ready"
|
||||
|
||||
/**
|
||||
* Appended to whenever we complete a donation redemption (or gift send) for a bank transfer.
|
||||
* Popped from whenever we enter the conversation list.
|
||||
*/
|
||||
private const val DONATION_COMPLETE_QUEUE = "donation.complete.queue"
|
||||
}
|
||||
|
||||
override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -470,6 +477,30 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
var subscriptionEndOfPeriodRedemptionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_REDEEM, 0L)
|
||||
var subscriptionEndOfPeriodRedeemed by longValue(SUBSCRIPTION_EOP_REDEEMED, 0L)
|
||||
|
||||
fun appendToDonationCompletionList(donationCompleted: DonationCompletedQueue.DonationCompleted) {
|
||||
synchronized(this) {
|
||||
val pendingBytes = getBlob(DONATION_COMPLETE_QUEUE, null)
|
||||
val queue: DonationCompletedQueue = pendingBytes?.let { DonationCompletedQueue.ADAPTER.decode(pendingBytes) } ?: DonationCompletedQueue()
|
||||
val newQueue: DonationCompletedQueue = queue.copy(donationsCompleted = queue.donationsCompleted + donationCompleted)
|
||||
|
||||
putBlob(DONATION_COMPLETE_QUEUE, newQueue.encode())
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeDonationCompletionList(): List<DonationCompletedQueue.DonationCompleted> {
|
||||
synchronized(this) {
|
||||
val pendingBytes = getBlob(DONATION_COMPLETE_QUEUE, null)
|
||||
if (pendingBytes == null) {
|
||||
return emptyList()
|
||||
} else {
|
||||
val queue: DonationCompletedQueue = DonationCompletedQueue.ADAPTER.decode(pendingBytes)
|
||||
remove(DONATION_COMPLETE_QUEUE)
|
||||
|
||||
return queue.donationsCompleted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateRequestCredential(): ReceiptCredentialRequestContext {
|
||||
Log.d(TAG, "Generating request credentials context for token redemption...", true)
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
@@ -272,3 +272,11 @@ message ThreadMergeEvent {
|
||||
message SessionSwitchoverEvent {
|
||||
string e164 = 1;
|
||||
}
|
||||
|
||||
message DonationCompletedQueue {
|
||||
message DonationCompleted {
|
||||
int64 level = 1;
|
||||
}
|
||||
|
||||
repeated DonationCompleted donationsCompleted = 1;
|
||||
}
|
||||
|
||||
@@ -5876,6 +5876,13 @@
|
||||
<!-- Confirmation button for donation pending sheet displayed after donating via a bank transfer. -->
|
||||
<string name="DonationPendingBottomSheet__done">Done</string>
|
||||
|
||||
<!-- Title of 'Donation Complete' sheet displayed after a bank transfer completes and the badge is redeemed -->
|
||||
<string name="DonationCompletedBottomSheet__donation_complete">Donation Complete</string>
|
||||
<!-- Text block of 'Donation Complete' sheet displayed after a bank transfer completes and the badge is redeemed -->
|
||||
<string name="DonationCompleteBottomSheet__your_bank_transfer_was_received">Your bank transfer was received. You can choose to display this badge on your profile to show off your support.</string>
|
||||
<!-- Button text of 'Donation Complete' sheet displayed after a bank transfer completes and the badge is redeemed to dismiss sheet -->
|
||||
<string name="DonationCompleteBottomSheet__done">Done</string>
|
||||
|
||||
<!-- StripePaymentInProgressFragment -->
|
||||
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user