kopia lustrzana https://github.com/ryukoposting/Signal-Android
Verify recipient before launching google pay sheet in badge gifting flow.
rodzic
dc5f7d0906
commit
0f08acbc04
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
||||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
|
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
||||||
|
import org.thoughtcrime.securesms.util.Debouncer
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||||
|
|
||||||
|
@ -67,10 +68,12 @@ class GiftFlowConfirmationFragment :
|
||||||
private val lifecycleDisposable = LifecycleDisposable()
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
private var errorDialog: DialogInterface? = null
|
private var errorDialog: DialogInterface? = null
|
||||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||||
|
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
|
||||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||||
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
||||||
|
|
||||||
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
|
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
|
||||||
|
private val debouncer = Debouncer(100L)
|
||||||
|
|
||||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
RecipientPreference.register(adapter)
|
RecipientPreference.register(adapter)
|
||||||
|
@ -85,6 +88,11 @@ class GiftFlowConfirmationFragment :
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
|
verifyingRecipientDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(R.layout.verifying_recipient_payment_dialog)
|
||||||
|
.setCancelable(false)
|
||||||
|
.create()
|
||||||
|
|
||||||
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
|
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
|
||||||
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
|
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
|
||||||
|
|
||||||
|
@ -122,10 +130,17 @@ class GiftFlowConfirmationFragment :
|
||||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
|
||||||
|
if (state.stage == GiftFlowState.Stage.RECIPIENT_VERIFICATION) {
|
||||||
|
debouncer.publish { verifyingRecipientDonationPaymentDialog.show() }
|
||||||
|
} else {
|
||||||
|
debouncer.clear()
|
||||||
|
verifyingRecipientDonationPaymentDialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
if (state.stage == GiftFlowState.Stage.PAYMENT_PIPELINE) {
|
if (state.stage == GiftFlowState.Stage.PAYMENT_PIPELINE) {
|
||||||
processingDonationPaymentDialog.show()
|
processingDonationPaymentDialog.show()
|
||||||
} else {
|
} else {
|
||||||
processingDonationPaymentDialog.hide()
|
processingDonationPaymentDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
textInputViewHolder.bind(
|
textInputViewHolder.bind(
|
||||||
|
@ -175,6 +190,8 @@ class GiftFlowConfirmationFragment :
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
textInputViewHolder.onDetachedFromWindow()
|
textInputViewHolder.onDetachedFromWindow()
|
||||||
processingDonationPaymentDialog.dismiss()
|
processingDonationPaymentDialog.dismiss()
|
||||||
|
debouncer.clear()
|
||||||
|
verifyingRecipientDonationPaymentDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
|
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
|
||||||
|
|
|
@ -20,8 +20,9 @@ data class GiftFlowState(
|
||||||
enum class Stage {
|
enum class Stage {
|
||||||
INIT,
|
INIT,
|
||||||
READY,
|
READY,
|
||||||
|
RECIPIENT_VERIFICATION,
|
||||||
TOKEN_REQUEST,
|
TOKEN_REQUEST,
|
||||||
PAYMENT_PIPELINE,
|
PAYMENT_PIPELINE,
|
||||||
FAILURE
|
FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Intent
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.gms.wallet.PaymentData
|
import com.google.android.gms.wallet.PaymentData
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
@ -128,9 +129,20 @@ class GiftFlowViewModel(
|
||||||
fun requestTokenFromGooglePay(label: String) {
|
fun requestTokenFromGooglePay(label: String) {
|
||||||
val giftLevel = store.state.giftLevel ?: return
|
val giftLevel = store.state.giftLevel ?: return
|
||||||
val giftPrice = store.state.giftPrices[store.state.currency] ?: return
|
val giftPrice = store.state.giftPrices[store.state.currency] ?: return
|
||||||
|
val giftRecipient = store.state.recipient?.id ?: return
|
||||||
|
|
||||||
this.giftToPurchase = Gift(giftLevel, giftPrice)
|
this.giftToPurchase = Gift(giftLevel, giftPrice)
|
||||||
donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE)
|
|
||||||
|
store.update { it.copy(stage = GiftFlowState.Stage.RECIPIENT_VERIFICATION) }
|
||||||
|
disposables += donationPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(giftRecipient)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeBy(
|
||||||
|
onComplete = {
|
||||||
|
store.update { it.copy(stage = GiftFlowState.Stage.TOKEN_REQUEST) }
|
||||||
|
donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE)
|
||||||
|
},
|
||||||
|
onError = this::onPaymentFlowError
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActivityResult(
|
fun onActivityResult(
|
||||||
|
@ -153,16 +165,7 @@ class GiftFlowViewModel(
|
||||||
store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) }
|
store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) }
|
||||||
|
|
||||||
donationPaymentRepository.continuePayment(gift.price, paymentData, recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy(
|
donationPaymentRepository.continuePayment(gift.price, paymentData, recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy(
|
||||||
onError = { throwable ->
|
onError = this@GiftFlowViewModel::onPaymentFlowError,
|
||||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
|
||||||
val donationError: DonationError = if (throwable is DonationError) {
|
|
||||||
throwable
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
|
|
||||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT)
|
|
||||||
}
|
|
||||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
|
||||||
},
|
|
||||||
onComplete = {
|
onComplete = {
|
||||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!))
|
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!))
|
||||||
|
@ -185,6 +188,17 @@ class GiftFlowViewModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onPaymentFlowError(throwable: Throwable) {
|
||||||
|
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||||
|
val donationError: DonationError = if (throwable is DonationError) {
|
||||||
|
throwable
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
|
||||||
|
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT)
|
||||||
|
}
|
||||||
|
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getLoadState(
|
private fun getLoadState(
|
||||||
oldState: GiftFlowState,
|
oldState: GiftFlowState,
|
||||||
giftPrices: Map<Currency, FiatMoney>? = null,
|
giftPrices: Map<Currency, FiatMoney>? = null,
|
||||||
|
|
|
@ -60,7 +60,6 @@ import java.util.concurrent.TimeUnit
|
||||||
*/
|
*/
|
||||||
class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||||
|
|
||||||
private val application = activity.application
|
|
||||||
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
||||||
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
|
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
|
||||||
|
|
||||||
|
@ -92,19 +91,16 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param price The amount to charce the local user
|
* Verifies that the given recipient is a supported target for a gift.
|
||||||
* @param paymentData PaymentData from Google Pay that describes the payment method
|
|
||||||
* @param badgeRecipient Who will be getting the badge
|
|
||||||
* @param additionalMessage An additional message to send along with the badge (only used if badge recipient is not self)
|
|
||||||
*/
|
*/
|
||||||
fun continuePayment(price: FiatMoney, paymentData: PaymentData, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable {
|
fun verifyRecipientIsAllowedToReceiveAGift(badgeRecipient: RecipientId): Completable {
|
||||||
val verifyRecipient = Completable.fromAction {
|
return Completable.fromAction {
|
||||||
Log.d(TAG, "Verifying badge recipient $badgeRecipient", true)
|
Log.d(TAG, "Verifying badge recipient $badgeRecipient", true)
|
||||||
val recipient = Recipient.resolved(badgeRecipient)
|
val recipient = Recipient.resolved(badgeRecipient)
|
||||||
|
|
||||||
if (recipient.isSelf) {
|
if (recipient.isSelf) {
|
||||||
Log.d(TAG, "Badge recipient is self, so this is a boost. Skipping verification.", true)
|
Log.d(TAG, "Cannot send a gift to self.", true)
|
||||||
return@fromAction
|
throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientDatabase.RegisteredState.REGISTERED) {
|
if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientDatabase.RegisteredState.REGISTERED) {
|
||||||
|
@ -124,11 +120,19 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||||
Log.w(TAG, "Failed to retrieve profile for recipient.", e, true)
|
Log.w(TAG, "Failed to retrieve profile for recipient.", e, true)
|
||||||
throw DonationError.GiftRecipientVerificationError.FailedToFetchProfile(e)
|
throw DonationError.GiftRecipientVerificationError.FailedToFetchProfile(e)
|
||||||
}
|
}
|
||||||
}
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
return verifyRecipient.doOnComplete {
|
/**
|
||||||
Log.d(TAG, "Creating payment intent for $price...", true)
|
* @param price The amount to charce the local user
|
||||||
}.andThen(stripeApi.createPaymentIntent(price, badgeLevel))
|
* @param paymentData PaymentData from Google Pay that describes the payment method
|
||||||
|
* @param badgeRecipient Who will be getting the badge
|
||||||
|
* @param additionalMessage An additional message to send along with the badge (only used if badge recipient is not self)
|
||||||
|
*/
|
||||||
|
fun continuePayment(price: FiatMoney, paymentData: PaymentData, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable {
|
||||||
|
Log.d(TAG, "Creating payment intent for $price...", true)
|
||||||
|
|
||||||
|
return stripeApi.createPaymentIntent(price, badgeLevel)
|
||||||
.onErrorResumeNext {
|
.onErrorResumeNext {
|
||||||
if (it is DonationError) {
|
if (it is DonationError) {
|
||||||
Single.error(it)
|
Single.error(it)
|
||||||
|
|
|
@ -71,12 +71,20 @@ class DonationErrorParams<V> private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <V> getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback<V>): DonationErrorParams<V> {
|
private fun <V> getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback<V>): DonationErrorParams<V> {
|
||||||
return DonationErrorParams(
|
return when (verificationError) {
|
||||||
title = R.string.DonationsErrors__recipient_verification_failed,
|
is DonationError.GiftRecipientVerificationError.FailedToFetchProfile -> DonationErrorParams(
|
||||||
message = R.string.DonationsErrors__target_does_not_support_gifting,
|
title = R.string.DonationsErrors__could_not_verify_recipient,
|
||||||
positiveAction = callback.onContactSupport(context),
|
message = R.string.DonationsErrors__please_check_your_network_connection,
|
||||||
negativeAction = null
|
positiveAction = callback.onOk(context),
|
||||||
)
|
negativeAction = null
|
||||||
|
)
|
||||||
|
else -> DonationErrorParams(
|
||||||
|
title = R.string.DonationsErrors__recipient_verification_failed,
|
||||||
|
message = R.string.DonationsErrors__target_does_not_support_gifting,
|
||||||
|
positiveAction = callback.onOk(context),
|
||||||
|
negativeAction = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="80dp"
|
||||||
|
android:layout_marginEnd="80dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="64dp"
|
||||||
|
android:paddingBottom="64dp">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/processing_card_progress"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/processing_card_text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/processing_card_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/GiftFlowConfirmationFragment__verifying_recipient"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/processing_card_progress" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
|
@ -4234,8 +4234,14 @@
|
||||||
<string name="ViewBadgeBottomSheetDialogFragment__your_device_doesn_t_support_google_pay_so_you_can_t_subscribe_to_earn_a_badge_you_can_still_support_signal_by_making_a_donation_on_our_website">Your device doesn\'t support Google Pay, so you can\'t subscribe to earn a badge. You can still support Signal by making a donation on our website.</string>
|
<string name="ViewBadgeBottomSheetDialogFragment__your_device_doesn_t_support_google_pay_so_you_can_t_subscribe_to_earn_a_badge_you_can_still_support_signal_by_making_a_donation_on_our_website">Your device doesn\'t support Google Pay, so you can\'t subscribe to earn a badge. You can still support Signal by making a donation on our website.</string>
|
||||||
<string name="NetworkFailure__network_error_check_your_connection_and_try_again">Network error. Check your connection and try again.</string>
|
<string name="NetworkFailure__network_error_check_your_connection_and_try_again">Network error. Check your connection and try again.</string>
|
||||||
<string name="NetworkFailure__retry">Retry</string>
|
<string name="NetworkFailure__retry">Retry</string>
|
||||||
|
<!-- Displayed as a dialog title when the selected recipient for a gift doesn't support gifting -->
|
||||||
<string name="DonationsErrors__recipient_verification_failed">Recipient verification failed.</string>
|
<string name="DonationsErrors__recipient_verification_failed">Recipient verification failed.</string>
|
||||||
|
<!-- Displayed as a dialog message when the selected recipient for a gift doesn't support gifting -->
|
||||||
<string name="DonationsErrors__target_does_not_support_gifting">Target does not support gifting.</string>
|
<string name="DonationsErrors__target_does_not_support_gifting">Target does not support gifting.</string>
|
||||||
|
<!-- Displayed as a dialog title when the user's profile could not be fetched, likely due to lack of internet -->
|
||||||
|
<string name="DonationsErrors__could_not_verify_recipient">Could not verify recipient.</string>
|
||||||
|
<!-- Displayed as a dialog message when the user's profile could not be fetched, likely due to lack of internet -->
|
||||||
|
<string name="DonationsErrors__please_check_your_network_connection">Please check your network connection and try again.</string>
|
||||||
|
|
||||||
<!-- Gift message view title -->
|
<!-- Gift message view title -->
|
||||||
<string name="GiftMessageView__gift_badge">Gift badge</string>
|
<string name="GiftMessageView__gift_badge">Gift badge</string>
|
||||||
|
@ -4746,6 +4752,8 @@
|
||||||
<string name="GiftFlowConfirmationFragment__one_time_donation">One-time donation</string>
|
<string name="GiftFlowConfirmationFragment__one_time_donation">One-time donation</string>
|
||||||
<!-- Hint for add message input -->
|
<!-- Hint for add message input -->
|
||||||
<string name="GiftFlowConfirmationFragment__add_a_message">Add a message</string>
|
<string name="GiftFlowConfirmationFragment__add_a_message">Add a message</string>
|
||||||
|
<!-- Displayed in the dialog while verifying the chosen recipient -->
|
||||||
|
<string name="GiftFlowConfirmationFragment__verifying_recipient">Verifying recipient…</string>
|
||||||
<!-- Title for sheet shown when opening a redeemed gift -->
|
<!-- Title for sheet shown when opening a redeemed gift -->
|
||||||
<string name="ViewReceivedGiftBottomSheet__s_sent_you_a_gift">%1$s sent you a gift</string>
|
<string name="ViewReceivedGiftBottomSheet__s_sent_you_a_gift">%1$s sent you a gift</string>
|
||||||
<!-- Title for sheet shown when opening a sent gift -->
|
<!-- Title for sheet shown when opening a sent gift -->
|
||||||
|
|
Ładowanie…
Reference in New Issue