kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add new joined donations screen.
rodzic
92f10b8a86
commit
a7ae6e62a3
|
@ -15,6 +15,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
|||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.thoughtcrime.securesms.badges.gifts.Gifts
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
|
@ -164,7 +165,7 @@ class GiftFlowViewModel(
|
|||
|
||||
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, GooglePayPaymentSource(paymentData), recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy(
|
||||
onError = this@GiftFlowViewModel::onPaymentFlowError,
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.databinding.SubscriptionPreferenceBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -19,7 +17,7 @@ import java.util.concurrent.TimeUnit
|
|||
*/
|
||||
object GiftRowItem {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.subscription_preference))
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, SubscriptionPreferenceBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(val giftBadge: Badge, val price: FiatMoney) : MappingModel<Model> {
|
||||
|
@ -28,23 +26,15 @@ object GiftRowItem {
|
|||
override fun areContentsTheSame(newItem: Model): Boolean = giftBadge == newItem.giftBadge && price == newItem.price
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badgeView = itemView.findViewById<BadgeImageView>(R.id.badge)
|
||||
private val titleView = itemView.findViewById<TextView>(R.id.title)
|
||||
private val checkView = itemView.findViewById<View>(R.id.check)
|
||||
private val taglineView = itemView.findViewById<TextView>(R.id.tagline)
|
||||
private val priceView = itemView.findViewById<TextView>(R.id.price)
|
||||
|
||||
class ViewHolder(binding: SubscriptionPreferenceBinding) : BindingViewHolder<Model, SubscriptionPreferenceBinding>(binding) {
|
||||
init {
|
||||
itemView.isSelected = true
|
||||
binding.root.isSelected = true
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
checkView.visible = false
|
||||
badgeView.setBadge(model.giftBadge)
|
||||
titleView.text = model.giftBadge.name
|
||||
taglineView.setText(R.string.GiftRowItem__send_a_gift_badge)
|
||||
binding.check.visible = false
|
||||
binding.badge.setBadge(model.giftBadge)
|
||||
binding.tagline.setText(R.string.GiftRowItem__send_a_gift_badge)
|
||||
|
||||
val price = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
|
@ -56,7 +46,7 @@ object GiftRowItem {
|
|||
|
||||
val duration = TimeUnit.MILLISECONDS.toDays(model.giftBadge.duration)
|
||||
|
||||
priceView.text = context.resources.getQuantityString(R.plurals.GiftRowItem_s_dot_d_day_duration, duration.toInt(), price, duration)
|
||||
binding.title.text = context.resources.getQuantityString(R.plurals.GiftRowItem_s_dot_d_day_duration, duration.toInt(), price, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
|||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* "Hero Image" for displaying an Avatar and badge. Allows the user to see what their profile will look like with a particular badge applied.
|
||||
*/
|
||||
object BadgePreview {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
|
@ -19,6 +22,11 @@ object BadgePreview {
|
|||
}
|
||||
|
||||
sealed class BadgeModel<T : BadgeModel<T>> : MappingModel<T> {
|
||||
|
||||
companion object {
|
||||
const val PAYLOAD_BADGE = "badge"
|
||||
}
|
||||
|
||||
abstract val badge: Badge?
|
||||
abstract val recipient: Recipient
|
||||
|
||||
|
@ -33,12 +41,20 @@ object BadgePreview {
|
|||
data class GiftedBadgeModel(override val badge: Badge?, override val recipient: Recipient) : BadgeModel<GiftedBadgeModel>()
|
||||
|
||||
override fun areItemsTheSame(newItem: T): Boolean {
|
||||
return badge?.id == newItem.badge?.id && recipient.id == newItem.recipient.id
|
||||
return recipient.id == newItem.recipient.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: T): Boolean {
|
||||
return badge == newItem.badge && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: T): Any? {
|
||||
return if (recipient.hasSameContent(newItem.recipient) && badge != newItem.badge) {
|
||||
PAYLOAD_BADGE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder<T : BadgeModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
|
|
|
@ -10,15 +10,15 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.databinding.ViewBadgeBottomSheetDialogFragmentBinding
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
|
@ -43,6 +43,8 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||
textSize = ViewUtil.spToPx(16f).toFloat()
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(ViewBadgeBottomSheetDialogFragmentBinding::bind)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||
}
|
||||
|
@ -50,41 +52,36 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
postponeEnterTransition()
|
||||
|
||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||
val action: MaterialButton = view.findViewById(R.id.action)
|
||||
val noSupport: View = view.findViewById(R.id.no_support)
|
||||
|
||||
if (getRecipientId() == Recipient.self().id) {
|
||||
action.visible = false
|
||||
binding.action.visible = false
|
||||
}
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (PlayServicesUtil.getPlayServicesStatus(requireContext()) != PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||
noSupport.visible = true
|
||||
action.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_open_20)
|
||||
action.setText(R.string.preferences__donate_to_signal)
|
||||
action.setOnClickListener {
|
||||
binding.noSupport.visible = true
|
||||
binding.action.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_open_20)
|
||||
binding.action.setText(R.string.preferences__donate_to_signal)
|
||||
binding.action.setOnClickListener {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
} else if (Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }) {
|
||||
action.setOnClickListener {
|
||||
binding.action.setOnClickListener {
|
||||
startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
}
|
||||
} else {
|
||||
action.visible = false
|
||||
binding.action.visible = false
|
||||
}
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
|
||||
LargeBadge.register(adapter)
|
||||
pager.adapter = adapter
|
||||
binding.pager.adapter = adapter
|
||||
adapter.submitList(listOf(LargeBadge.EmptyModel()))
|
||||
|
||||
TabLayoutMediator(tabs, pager) { _, _ ->
|
||||
TabLayoutMediator(binding.tabLayout, binding.pager) { _, _ ->
|
||||
}.attach()
|
||||
|
||||
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
if (adapter.getModel(position).map { it is LargeBadge.Model }.orElse(false)) {
|
||||
viewModel.onPageSelected(position)
|
||||
|
@ -101,7 +98,8 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
tabs.visible = state.allBadgesVisibleOnProfile.size > 1
|
||||
binding.tabLayout.visible = state.allBadgesVisibleOnProfile.size > 1
|
||||
binding.singlePageSpace.visible = state.allBadgesVisibleOnProfile.size > 1
|
||||
|
||||
var maxLines = 3
|
||||
state.allBadgesVisibleOnProfile.forEach { badge ->
|
||||
|
@ -117,8 +115,8 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||
}
|
||||
) {
|
||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||
if (state.selectedBadge != null && pager.currentItem != stateSelectedIndex) {
|
||||
pager.currentItem = stateSelectedIndex
|
||||
if (state.selectedBadge != null && binding.pager.currentItem != stateSelectedIndex) {
|
||||
binding.pager.currentItem = stateSelectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ sealed class DSLSettingsText {
|
|||
|
||||
object TitleLargeModifier : TextAppearanceModifier(R.style.Signal_Text_TitleLarge)
|
||||
object TitleMediumModifier : TextAppearanceModifier(R.style.Signal_Text_TitleMedium)
|
||||
object Body1BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Body1_Bold)
|
||||
object BodyLargeModifier : TextAppearanceModifier(R.style.Signal_Text_BodyLarge)
|
||||
|
||||
open class TextAppearanceModifier(@StyleRes private val textAppearance: Int) : Modifier {
|
||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
|||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -54,8 +55,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
|||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment()
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(DonateToSignalType.MONTHLY)
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(DonateToSignalType.ONE_TIME)
|
||||
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
|
||||
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface DonationPaymentComponent {
|
||||
val donationPaymentRepository: DonationPaymentRepository
|
||||
val googlePayResultPublisher: Subject<GooglePayResult>
|
||||
|
||||
class GooglePayResult(val requestCode: Int, val resultCode: Int, val data: Intent?)
|
||||
@Parcelize
|
||||
class GooglePayResult(val requestCode: Int, val resultCode: Int, val data: Intent?) : Parcelable
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.settings.app.subscription
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
@ -10,7 +9,6 @@ import org.signal.core.util.concurrent.SignalExecutors
|
|||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
|
@ -63,6 +61,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
|||
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())
|
||||
|
||||
fun isGooglePayAvailable(): Completable {
|
||||
return googlePayApi.queryIsReadyToPay()
|
||||
}
|
||||
|
||||
fun scheduleSyncForAccountRecordChange() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
scheduleSyncForAccountRecordChangeSync()
|
||||
|
@ -129,7 +131,13 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
|||
* @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 continuePayment(
|
||||
price: FiatMoney,
|
||||
paymentSource: StripeApi.PaymentSource,
|
||||
badgeRecipient: RecipientId,
|
||||
additionalMessage: String?,
|
||||
badgeLevel: Long
|
||||
): Completable {
|
||||
Log.d(TAG, "Creating payment intent for $price...", true)
|
||||
|
||||
return stripeApi.createPaymentIntent(price, badgeLevel)
|
||||
|
@ -151,17 +159,17 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
|||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationError.oneTimeDonationAmountTooSmall(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationError.oneTimeDonationAmountTooLarge(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationError.invalidCurrencyForOneTimeDonation(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(price, paymentData, result.paymentIntent, badgeRecipient, additionalMessage, badgeLevel)
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(price, paymentSource, result.paymentIntent, badgeRecipient, additionalMessage, badgeLevel)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun continueSubscriptionSetup(paymentData: PaymentData): Completable {
|
||||
fun continueSubscriptionSetup(paymentSource: StripeApi.PaymentSource): Completable {
|
||||
Log.d(TAG, "Continuing subscription setup...", true)
|
||||
return stripeApi.createSetupIntent()
|
||||
.flatMapCompletable { result ->
|
||||
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
|
||||
stripeApi.confirmSetupIntent(GooglePayPaymentSource(paymentData), result.setupIntent).doOnComplete {
|
||||
stripeApi.confirmSetupIntent(paymentSource, result.setupIntent).doOnComplete {
|
||||
Log.d(TAG, "Confirmed SetupIntent...", true)
|
||||
}
|
||||
}
|
||||
|
@ -203,12 +211,12 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
|||
}
|
||||
}
|
||||
|
||||
private fun confirmPayment(price: FiatMoney, paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable {
|
||||
private fun confirmPayment(price: FiatMoney, paymentSource: StripeApi.PaymentSource, paymentIntent: StripeApi.PaymentIntent, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable {
|
||||
val isBoost = badgeRecipient == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
|
||||
Log.d(TAG, "Confirming payment intent...", true)
|
||||
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).onErrorResumeNext {
|
||||
val confirmPayment = stripeApi.confirmPaymentIntent(paymentSource, paymentIntent).onErrorResumeNext {
|
||||
Completable.error(DonationError.getPaymentSetupError(donationErrorSource, it))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
|
@ -54,4 +58,11 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
|
|||
it.level
|
||||
}
|
||||
}
|
||||
|
||||
fun syncAccountRecord(): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
|||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
|
@ -144,7 +146,8 @@ data class Boost(
|
|||
itemView.isEnabled = model.isEnabled
|
||||
|
||||
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
||||
button.isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||
val isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||
button.isSelected = isSelected
|
||||
button.text = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
boost.price,
|
||||
|
@ -156,6 +159,13 @@ data class Boost(
|
|||
model.onBoostClick(it, boost)
|
||||
custom.clearFocus()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
val weight = if (isSelected) 500 else 400
|
||||
button.typeface = Typeface.create(null, weight, false)
|
||||
} else {
|
||||
button.typeface = if (isSelected) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
if (filter == null || filter?.currency != model.currency) {
|
||||
|
|
|
@ -1,303 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil.requireCoordinatorLayout
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* UX to allow users to donate ephemerally.
|
||||
*/
|
||||
class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
layoutId = R.layout.boost_bottom_sheet
|
||||
) {
|
||||
|
||||
private val viewModel: BoostViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BoostViewModel.Factory(BoostRepository(ApplicationDependencies.getDonationsService()), donationPaymentComponent.donationPaymentRepository, FETCH_BOOST_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var boost1AnimationView: LottieAnimationView
|
||||
private lateinit var boost2AnimationView: LottieAnimationView
|
||||
private lateinit var boost3AnimationView: LottieAnimationView
|
||||
private lateinit var boost4AnimationView: LottieAnimationView
|
||||
private lateinit var boost5AnimationView: LottieAnimationView
|
||||
private lateinit var boost6AnimationView: LottieAnimationView
|
||||
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private var errorDialog: DialogInterface? = null
|
||||
|
||||
private val sayThanks: CharSequence by lazy {
|
||||
SpannableStringBuilder(requireContext().getString(R.string.BoostFragment__make_a_one_time, 30))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.learnMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.sustainer_boost_and_badges))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
donationPaymentComponent = requireListener()
|
||||
viewModel.refresh()
|
||||
|
||||
CurrencySelection.register(adapter)
|
||||
BadgePreview.register(adapter)
|
||||
Boost.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
BoostAnimation.register(adapter)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
|
||||
boost1AnimationView = requireView().findViewById(R.id.boost1_animation)
|
||||
boost2AnimationView = requireView().findViewById(R.id.boost2_animation)
|
||||
boost3AnimationView = requireView().findViewById(R.id.boost3_animation)
|
||||
boost4AnimationView = requireView().findViewById(R.id.boost4_animation)
|
||||
boost5AnimationView = requireView().findViewById(R.id.boost5_animation)
|
||||
boost6AnimationView = requireView().findViewById(R.id.boost6_animation)
|
||||
|
||||
KeyboardAwareLinearLayout(requireContext()).apply {
|
||||
addOnKeyboardHiddenListener {
|
||||
recyclerView.post { recyclerView.requestLayout() }
|
||||
}
|
||||
|
||||
addOnKeyboardShownListener {
|
||||
recyclerView.post { recyclerView.scrollToPosition(adapter.itemCount - 1) }
|
||||
}
|
||||
|
||||
requireCoordinatorLayout().addView(this)
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event: DonationEvent ->
|
||||
when (event) {
|
||||
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(event.badge)
|
||||
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
|
||||
DonationEvent.SubscriptionCancelled -> Unit
|
||||
is DonationEvent.SubscriptionCancellationFailed -> Unit
|
||||
}
|
||||
}
|
||||
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
|
||||
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
|
||||
}
|
||||
|
||||
lifecycleDisposable += DonationError
|
||||
.getErrorsForSource(DonationErrorSource.BOOST)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { donationError ->
|
||||
onPaymentError(donationError)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BoostState): DSLConfiguration {
|
||||
if (state.stage == BoostState.Stage.PAYMENT_PIPELINE) {
|
||||
processingDonationPaymentDialog.show()
|
||||
} else {
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
return configure {
|
||||
customPref(BoostAnimation.Model())
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BoostFragment__give_signal_a_boost,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
sayThanks,
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(28f).toInt())
|
||||
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
selectedCurrency = state.currencySelection,
|
||||
isEnabled = state.stage == BoostState.Stage.READY,
|
||||
onClick = {
|
||||
findNavController().safeNavigate(BoostFragmentDirections.actionBoostFragmentToSetDonationCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray()))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == BoostState.Stage.INIT) {
|
||||
customPref(
|
||||
Boost.LoadingModel()
|
||||
)
|
||||
} else if (state.stage == BoostState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(20f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.retry()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
customPref(
|
||||
Boost.SelectionModel(
|
||||
boosts = state.boosts,
|
||||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isEnabled = state.stage == BoostState.Stage.READY,
|
||||
onBoostClick = { view, boost ->
|
||||
startAnimationAboveSelectedBoost(view)
|
||||
viewModel.setSelectedBoost(boost)
|
||||
},
|
||||
onCustomAmountChanged = {
|
||||
viewModel.setCustomAmount(it)
|
||||
},
|
||||
onCustomAmountFocusChanged = {
|
||||
if (it) {
|
||||
viewModel.setCustomAmountFocused()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
onClick = this@BoostFragment::onGooglePayButtonClicked,
|
||||
isEnabled = state.stage == BoostState.Stage.READY
|
||||
)
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
|
||||
onClick = {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGooglePayButtonClicked() {
|
||||
viewModel.requestTokenFromGooglePay(getString(R.string.preferences__one_time_donation))
|
||||
}
|
||||
|
||||
private fun onPaymentConfirmed(boostBadge: Badge) {
|
||||
findNavController().safeNavigate(
|
||||
BoostFragmentDirections.actionBoostFragmentToBoostThanksForYourSupportBottomSheetDialog(boostBadge).setIsBoost(true),
|
||||
NavOptions.Builder().setPopUpTo(R.id.boostFragment, true).build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
Log.w(TAG, "onPaymentError", throwable, true)
|
||||
|
||||
if (errorDialog != null) {
|
||||
Log.i(TAG, "Already displaying an error dialog. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun startAnimationAboveSelectedBoost(view: View) {
|
||||
val animationView = getAnimationContainer(view)
|
||||
val viewProjection = Projection.relativeToViewRoot(view, null)
|
||||
val animationProjection = Projection.relativeToViewRoot(animationView, null)
|
||||
val viewHorizontalCenter = viewProjection.x + viewProjection.width / 2f
|
||||
val animationHorizontalCenter = animationProjection.x + animationProjection.width / 2f
|
||||
val animationBottom = animationProjection.y + animationProjection.height
|
||||
|
||||
animationView.translationY = -(animationBottom - viewProjection.y) + (viewProjection.height / 2f)
|
||||
animationView.translationX = viewHorizontalCenter - animationHorizontalCenter
|
||||
|
||||
animationView.playAnimation()
|
||||
|
||||
viewProjection.release()
|
||||
animationProjection.release()
|
||||
}
|
||||
|
||||
private fun getAnimationContainer(view: View): LottieAnimationView {
|
||||
return when (view.id) {
|
||||
R.id.boost_1 -> boost1AnimationView
|
||||
R.id.boost_2 -> boost2AnimationView
|
||||
R.id.boost_3 -> boost3AnimationView
|
||||
R.id.boost_4 -> boost4AnimationView
|
||||
R.id.boost_5 -> boost5AnimationView
|
||||
R.id.boost_6 -> boost6AnimationView
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BoostFragment::class.java)
|
||||
private const val FETCH_BOOST_TOKEN_REQUEST_CODE = 2000
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
data class BoostState(
|
||||
val boostBadge: Badge? = null,
|
||||
val currencySelection: Currency,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val boosts: List<Boost> = listOf(),
|
||||
val selectedBoost: Boost? = null,
|
||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, currencySelection),
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
val stage: Stage = Stage.INIT,
|
||||
val supportedCurrencyCodes: List<String> = emptyList()
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
FAILURE
|
||||
}
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Currency
|
||||
|
||||
class BoostViewModel(
|
||||
private val boostRepository: BoostRepository,
|
||||
private val donationPaymentRepository: DonationPaymentRepository,
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(BoostState(currencySelection = SignalStore.donationsValues().getOneTimeCurrency()))
|
||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||
private val disposables = CompositeDisposable()
|
||||
private val networkDisposable: Disposable
|
||||
|
||||
val state: LiveData<BoostState> = store.stateLiveData
|
||||
val events: Observable<DonationEvent> = eventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private var boostToPurchase: Boost? = null
|
||||
|
||||
init {
|
||||
networkDisposable = InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
networkDisposable.dispose()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun getSupportedCurrencyCodes(): List<String> {
|
||||
return store.state.supportedCurrencyCodes
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == BoostState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = BoostState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val currencyObservable = SignalStore.donationsValues().observableOneTimeCurrency
|
||||
val allBoosts = boostRepository.getBoosts()
|
||||
val boostBadge = boostRepository.getBoostBadge()
|
||||
|
||||
disposables += Observable.combineLatest(currencyObservable, allBoosts.toObservable(), boostBadge.toObservable()) { currency, boostMap, badge ->
|
||||
val boostList = if (currency in boostMap) {
|
||||
boostMap[currency]!!
|
||||
} else {
|
||||
SignalStore.donationsValues().setOneTimeCurrency(PlatformCurrencyUtil.USD)
|
||||
listOf()
|
||||
}
|
||||
|
||||
BoostInfo(boostList, boostList[2], badge, boostMap.keys)
|
||||
}.subscribeBy(
|
||||
onNext = { info ->
|
||||
store.update {
|
||||
it.copy(
|
||||
boosts = info.boosts,
|
||||
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
|
||||
boostBadge = it.boostBadge ?: info.boostBadge,
|
||||
stage = if (it.stage == BoostState.Stage.INIT || it.stage == BoostState.Stage.FAILURE) BoostState.Stage.READY else it.stage,
|
||||
supportedCurrencyCodes = info.supportedCurrencies.map(Currency::getCurrencyCode)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Could not load boost information", throwable)
|
||||
store.update {
|
||||
it.copy(stage = BoostState.Stage.FAILURE)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
disposables += currencyObservable.subscribeBy { currency ->
|
||||
store.update {
|
||||
it.copy(
|
||||
currencySelection = currency,
|
||||
isCustomAmountFocused = false,
|
||||
customAmount = FiatMoney(
|
||||
BigDecimal.ZERO, currency
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode, resultCode, data, this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
if (boost != null) {
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
|
||||
|
||||
store.update { it.copy(stage = BoostState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
donationPaymentRepository.continuePayment(boost.price, paymentData, Recipient.self().id, null, SubscriptionLevels.BOOST_LEVEL.toLong()).subscribeBy(
|
||||
onError = { throwable ->
|
||||
store.update { it.copy(stage = BoostState.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.BOOST)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.boostBadge!!))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.BOOST, googlePayException))
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun requestTokenFromGooglePay(label: String) {
|
||||
val snapshot = store.state
|
||||
if (snapshot.selectedBoost == null) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(stage = BoostState.Stage.TOKEN_REQUEST) }
|
||||
|
||||
val boost = if (snapshot.isCustomAmountFocused) {
|
||||
Log.d(TAG, "Boosting with custom amount ${snapshot.customAmount}")
|
||||
Boost(snapshot.customAmount)
|
||||
} else {
|
||||
Log.d(TAG, "Boosting with preset amount ${snapshot.selectedBoost.price}")
|
||||
snapshot.selectedBoost
|
||||
}
|
||||
|
||||
boostToPurchase = boost
|
||||
donationPaymentRepository.requestTokenFromGooglePay(boost.price, label, fetchTokenRequestCode)
|
||||
}
|
||||
|
||||
fun setSelectedBoost(boost: Boost) {
|
||||
store.update {
|
||||
it.copy(
|
||||
isCustomAmountFocused = false,
|
||||
selectedBoost = boost
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomAmount(rawAmount: String) {
|
||||
val amount = StringUtil.stripBidiIndicator(rawAmount)
|
||||
val bigDecimalAmount: BigDecimal = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
val decimalFormat = DecimalFormat.getInstance() as DecimalFormat
|
||||
decimalFormat.isParseBigDecimal = true
|
||||
|
||||
try {
|
||||
decimalFormat.parse(amount) as BigDecimal
|
||||
} catch (e: NumberFormatException) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
store.update { it.copy(customAmount = FiatMoney(bigDecimalAmount, it.customAmount.currency)) }
|
||||
}
|
||||
|
||||
fun setCustomAmountFocused() {
|
||||
store.update { it.copy(isCustomAmountFocused = true) }
|
||||
}
|
||||
|
||||
private data class BoostInfo(val boosts: List<Boost>, val defaultBoost: Boost?, val boostBadge: Badge, val supportedCurrencies: Set<Currency>)
|
||||
|
||||
class Factory(
|
||||
private val boostRepository: BoostRepository,
|
||||
private val donationPaymentRepository: DonationPaymentRepository,
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(BoostViewModel(boostRepository, donationPaymentRepository, fetchTokenRequestCode))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BoostViewModel::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
|
||||
sealed class DonateToSignalAction {
|
||||
data class DisplayCurrencySelectionDialog(val donateToSignalType: DonateToSignalType, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayGatewaySelectorDialog(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class CancelSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class UpdateSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.WrapperDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripeAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripeActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Unified donation fragment which allows users to choose between monthly or one-time donations.
|
||||
*/
|
||||
class DonateToSignalFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.donate_to_signal_fragment
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonateToSignalFragment::class.java)
|
||||
}
|
||||
|
||||
class Dialog : WrapperDialogFragment() {
|
||||
|
||||
override fun getWrappedFragment(): Fragment {
|
||||
return NavHostFragment.create(
|
||||
R.navigation.donate_to_signal,
|
||||
arguments
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(donateToSignalType: DonateToSignalType): DialogFragment {
|
||||
return Dialog().apply {
|
||||
arguments = DonateToSignalFragmentArgs.Builder(donateToSignalType).build().toBundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val args: DonateToSignalFragmentArgs by navArgs()
|
||||
private val viewModel: DonateToSignalViewModel by viewModels(factoryProducer = {
|
||||
DonateToSignalViewModel.Factory(args.startType)
|
||||
})
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
factoryProducer = {
|
||||
donationPaymentComponent = requireListener()
|
||||
StripePaymentInProgressViewModel.Factory(donationPaymentComponent.donationPaymentRepository)
|
||||
}
|
||||
)
|
||||
|
||||
private val disposables = LifecycleDisposable()
|
||||
private val binding by ViewBinderDelegate(DonateToSignalFragmentBinding::bind)
|
||||
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private val supportTechSummary: CharSequence by lazy {
|
||||
SpannableStringBuilder(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant), requireContext().getString(R.string.DonateToSignalFragment__support_technology)))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToSubscribeLearnMoreBottomSheetDialog())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onToolbarNavigationClicked() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
donationPaymentComponent = requireListener()
|
||||
registerGooglePayCallback()
|
||||
|
||||
setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
|
||||
val response: GatewayResponse = bundle.getParcelable(GatewaySelectorBottomSheet.REQUEST_KEY)!!
|
||||
handleGatewaySelectionResponse(response)
|
||||
}
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: StripeActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
|
||||
handleStripeActionResult(result)
|
||||
}
|
||||
|
||||
val recyclerView = this.recyclerView!!
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
|
||||
KeyboardAwareLinearLayout(requireContext()).apply {
|
||||
addOnKeyboardHiddenListener {
|
||||
recyclerView.post { recyclerView.requestLayout() }
|
||||
}
|
||||
|
||||
addOnKeyboardShownListener {
|
||||
recyclerView.post { recyclerView.scrollToPosition(adapter.itemCount - 1) }
|
||||
}
|
||||
|
||||
(view as ViewGroup).addView(this)
|
||||
}
|
||||
|
||||
Boost.register(adapter)
|
||||
Subscription.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
BadgePreview.register(adapter)
|
||||
CurrencySelection.register(adapter)
|
||||
DonationPillToggle.register(adapter)
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
disposables += DonationError.getErrorsForSource(DonationErrorSource.BOOST).subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
}
|
||||
|
||||
disposables += DonationError.getErrorsForSource(DonationErrorSource.SUBSCRIPTION).subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
}
|
||||
|
||||
disposables += viewModel.actions.subscribe { action ->
|
||||
when (action) {
|
||||
is DonateToSignalAction.DisplayCurrencySelectionDialog -> {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToSetDonationCurrencyFragment(
|
||||
action.donateToSignalType == DonateToSignalType.ONE_TIME,
|
||||
action.supportedCurrencies.toTypedArray()
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
}
|
||||
is DonateToSignalAction.DisplayGatewaySelectorDialog -> {
|
||||
Log.d(TAG, "Presenting gateway selector for ${action.gatewayRequest}")
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.gatewayRequest)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
}
|
||||
is DonateToSignalAction.CancelSubscription -> {
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
StripeAction.CANCEL_SUBSCRIPTION,
|
||||
action.gatewayRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
is DonateToSignalAction.UpdateSubscription -> {
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
StripeAction.UPDATE_SUBSCRIPTION,
|
||||
action.gatewayRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposables += viewModel.state.subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: DonateToSignalState): DSLConfiguration {
|
||||
return configure {
|
||||
space(36.dp)
|
||||
|
||||
customPref(BadgePreview.BadgeModel.SubscriptionModel(state.badge))
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.DonateToSignalFragment__powered_by,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(supportTechSummary, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
selectedCurrency = state.selectedCurrency,
|
||||
isEnabled = state.canSetCurrency,
|
||||
onClick = {
|
||||
viewModel.requestChangeCurrency()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
customPref(
|
||||
DonationPillToggle.Model(
|
||||
isEnabled = state.areFieldsEnabled,
|
||||
selected = state.donateToSignalType,
|
||||
onClick = {
|
||||
viewModel.toggleDonationType()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(10.dp)
|
||||
|
||||
when (state.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||
}
|
||||
|
||||
space(20.dp)
|
||||
|
||||
if (state.donateToSignalType == DonateToSignalType.MONTHLY && state.monthlyDonationState.isSubscriptionActive) {
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
||||
isEnabled = state.canContinue,
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.SubscribeFragment__update_subscription_question)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.SubscribeFragment__you_will_be_charged_the_full_amount_s_of,
|
||||
FiatMoneyUtil.format(
|
||||
requireContext().resources,
|
||||
viewModel.getSelectedSubscriptionCost(),
|
||||
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||
)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.SubscribeFragment__update) { _, _ ->
|
||||
viewModel.updateSubscription()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
space(4.dp)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__cancel_subscription),
|
||||
isEnabled = state.areFieldsEnabled,
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.SubscribeFragment__confirm_cancellation)
|
||||
.setMessage(R.string.SubscribeFragment__you_wont_be_charged_again)
|
||||
.setPositiveButton(R.string.SubscribeFragment__confirm) { _, _ ->
|
||||
viewModel.cancelSubscription()
|
||||
}
|
||||
.setNegativeButton(R.string.SubscribeFragment__not_now) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.DonateToSignalFragment__continue),
|
||||
isEnabled = state.canContinue,
|
||||
onClick = {
|
||||
viewModel.requestSelectGateway()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.displayOneTimeSelection(areFieldsEnabled: Boolean, state: DonateToSignalState.OneTimeDonationState) {
|
||||
when (state.donationStage) {
|
||||
DonateToSignalState.DonationStage.INIT -> customPref(Boost.LoadingModel())
|
||||
DonateToSignalState.DonationStage.FAILURE -> customPref(NetworkFailure.Model { viewModel.retryOneTimeDonationState() })
|
||||
DonateToSignalState.DonationStage.READY -> {
|
||||
customPref(
|
||||
Boost.SelectionModel(
|
||||
boosts = state.boosts,
|
||||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isEnabled = areFieldsEnabled,
|
||||
onBoostClick = { view, boost ->
|
||||
startAnimationAboveSelectedBoost(view)
|
||||
viewModel.setSelectedBoost(boost)
|
||||
},
|
||||
onCustomAmountChanged = {
|
||||
viewModel.setCustomAmount(it)
|
||||
},
|
||||
onCustomAmountFocusChanged = {
|
||||
if (it) {
|
||||
viewModel.setCustomAmountFocused()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.displayMonthlySelection(areFieldsEnabled: Boolean, state: DonateToSignalState.MonthlyDonationState) {
|
||||
if (state.transactionState.isTransactionJobPending) {
|
||||
customPref(Subscription.LoaderModel())
|
||||
return
|
||||
}
|
||||
|
||||
when (state.donationStage) {
|
||||
DonateToSignalState.DonationStage.INIT -> customPref(Subscription.LoaderModel())
|
||||
DonateToSignalState.DonationStage.FAILURE -> customPref(NetworkFailure.Model { viewModel.retryMonthlyDonationState() })
|
||||
else -> {
|
||||
state.subscriptions.forEach { subscription ->
|
||||
|
||||
val isActive = state.activeLevel == subscription.level && state.isSubscriptionActive
|
||||
|
||||
val activePrice = state.activeSubscription?.let { sub ->
|
||||
val activeCurrency = Currency.getInstance(sub.currency)
|
||||
val activeAmount = sub.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
FiatMoney(activeAmount, activeCurrency)
|
||||
}
|
||||
|
||||
customPref(
|
||||
Subscription.Model(
|
||||
activePrice = if (isActive) activePrice else null,
|
||||
subscription = subscription,
|
||||
isSelected = state.selectedSubscription == subscription,
|
||||
isEnabled = areFieldsEnabled,
|
||||
isActive = isActive,
|
||||
willRenew = isActive && !state.isActiveSubscriptionEnding,
|
||||
onClick = { viewModel.setSelectedSubscription(it) },
|
||||
renewalTimestamp = state.renewalTimestamp,
|
||||
selectedCurrency = state.selectedCurrency
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> error("Credit cards are not currently supported.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStripeActionResult(result: StripeActionResult) {
|
||||
when (result.status) {
|
||||
StripeActionResult.Status.SUCCESS -> handleSuccessfulStripeActionResult(result)
|
||||
StripeActionResult.Status.FAILURE -> handleFailedStripeActionResult(result)
|
||||
}
|
||||
|
||||
viewModel.refreshActiveSubscription()
|
||||
}
|
||||
|
||||
private fun handleSuccessfulStripeActionResult(result: StripeActionResult) {
|
||||
if (result.action == StripeAction.CANCEL_SUBSCRIPTION) {
|
||||
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(result.request.badge))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailedStripeActionResult(result: StripeActionResult) {
|
||||
if (result.action == StripeAction.CANCEL_SUBSCRIPTION) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
|
||||
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Log.w(TAG, "Stripe action failed: ${result.action}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
||||
viewModel.provideGatewayRequest(gatewayResponse.request)
|
||||
donationPaymentComponent.donationPaymentRepository.requestTokenFromGooglePay(
|
||||
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
|
||||
label = gatewayResponse.request.label,
|
||||
requestCode = gatewayResponse.request.donateToSignalType.requestCode.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
private fun registerGooglePayCallback() {
|
||||
donationPaymentComponent.googlePayResultPublisher.subscribeBy(
|
||||
onNext = { paymentResult ->
|
||||
viewModel.consumeGatewayRequest()?.let {
|
||||
donationPaymentComponent.donationPaymentRepository.onActivityResult(
|
||||
paymentResult.requestCode,
|
||||
paymentResult.resultCode,
|
||||
paymentResult.data,
|
||||
paymentResult.requestCode,
|
||||
GooglePayRequestCallback(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showErrorDialog(throwable: Throwable) {
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun startAnimationAboveSelectedBoost(view: View) {
|
||||
val animationView = getAnimationContainer(view)
|
||||
val viewProjection = Projection.relativeToViewRoot(view, null)
|
||||
val animationProjection = Projection.relativeToViewRoot(animationView, null)
|
||||
val viewHorizontalCenter = viewProjection.x + viewProjection.width / 2f
|
||||
val animationHorizontalCenter = animationProjection.x + animationProjection.width / 2f
|
||||
val animationBottom = animationProjection.y + animationProjection.height
|
||||
|
||||
animationView.translationY = -(animationBottom - viewProjection.y) + (viewProjection.height / 2f)
|
||||
animationView.translationX = viewHorizontalCenter - animationHorizontalCenter
|
||||
|
||||
animationView.playAnimation()
|
||||
|
||||
viewProjection.release()
|
||||
animationProjection.release()
|
||||
}
|
||||
|
||||
private fun getAnimationContainer(view: View): LottieAnimationView {
|
||||
return when (view.id) {
|
||||
R.id.boost_1 -> binding.boost1Animation
|
||||
R.id.boost_2 -> binding.boost2Animation
|
||||
R.id.boost_3 -> binding.boost3Animation
|
||||
R.id.boost_4 -> binding.boost4Animation
|
||||
R.id.boost_5 -> binding.boost5Animation
|
||||
R.id.boost_6 -> binding.boost6Animation
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
inner class GooglePayRequestCallback(private val request: GatewayRequest) : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
Log.d(TAG, "Successfully retrieved payment data from Google Pay", true)
|
||||
stripePaymentViewModel.providePaymentData(paymentData)
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(StripeAction.PROCESS_NEW_DONATION, request))
|
||||
}
|
||||
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
Log.w(TAG, "Failed to retrieve payment data from Google Pay", googlePayException, true)
|
||||
|
||||
val error = DonationError.getGooglePayRequestTokenError(
|
||||
source = when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
|
||||
},
|
||||
throwable = googlePayException
|
||||
)
|
||||
|
||||
DonationError.routeDonationError(requireContext(), error)
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
Log.d(TAG, "Cancelled Google Pay.", true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class DonateToSignalState(
|
||||
val donateToSignalType: DonateToSignalType,
|
||||
val oneTimeDonationState: OneTimeDonationState = OneTimeDonationState(),
|
||||
val monthlyDonationState: MonthlyDonationState = MonthlyDonationState()
|
||||
) {
|
||||
|
||||
val areFieldsEnabled: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress
|
||||
}
|
||||
|
||||
val badge: Badge?
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.badge
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription?.badge
|
||||
}
|
||||
|
||||
val canSetCurrency: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
}
|
||||
|
||||
val selectedCurrency: Currency
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectedCurrency
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedCurrency
|
||||
}
|
||||
|
||||
val selectableCurrencyCodes: List<String>
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectableCurrencyCodes
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectableCurrencyCodes
|
||||
}
|
||||
|
||||
val level: Int
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> 1
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription!!.level
|
||||
}
|
||||
|
||||
val canContinue: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||
}
|
||||
|
||||
data class OneTimeDonationState(
|
||||
val badge: Badge? = null,
|
||||
val selectedCurrency: Currency = SignalStore.donationsValues().getOneTimeCurrency(),
|
||||
val boosts: List<Boost> = emptyList(),
|
||||
val selectedBoost: Boost? = null,
|
||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, selectedCurrency),
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
val donationStage: DonationStage = DonationStage.INIT,
|
||||
val selectableCurrencyCodes: List<String> = emptyList()
|
||||
) {
|
||||
val isSelectionValid: Boolean = if (isCustomAmountFocused) customAmount.amount > BigDecimal.ZERO else selectedBoost != null
|
||||
}
|
||||
|
||||
data class MonthlyDonationState(
|
||||
val selectedCurrency: Currency = SignalStore.donationsValues().getSubscriptionCurrency(),
|
||||
val subscriptions: List<Subscription> = emptyList(),
|
||||
private val _activeSubscription: ActiveSubscription? = null,
|
||||
val selectedSubscription: Subscription? = null,
|
||||
val donationStage: DonationStage = DonationStage.INIT,
|
||||
val selectableCurrencyCodes: List<String> = emptyList(),
|
||||
val transactionState: TransactionState = TransactionState()
|
||||
) {
|
||||
val isSubscriptionActive: Boolean = _activeSubscription?.isActive == true
|
||||
val activeLevel: Int? = _activeSubscription?.activeSubscription?.level
|
||||
val activeSubscription: ActiveSubscription.Subscription? = _activeSubscription?.activeSubscription
|
||||
val isActiveSubscriptionEnding: Boolean = _activeSubscription?.isActive == true && _activeSubscription.activeSubscription.willCancelAtPeriodEnd()
|
||||
val renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription?.endOfCurrentPeriod ?: 0L)
|
||||
val isSelectionValid = selectedSubscription != null && (!isSubscriptionActive || selectedSubscription.level != activeSubscription?.level)
|
||||
}
|
||||
|
||||
enum class DonationStage {
|
||||
INIT,
|
||||
READY,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
data class TransactionState(
|
||||
val isTransactionJobPending: Boolean = false,
|
||||
val isLevelUpdateInProgress: Boolean = false
|
||||
) {
|
||||
val isInProgress: Boolean = isTransactionJobPending || isLevelUpdateInProgress
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
|
||||
ONE_TIME(16141),
|
||||
MONTHLY(16142);
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.BoostRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.SubscriptionRedemptionJobWatcher
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Contains the logic to manage the UI of the unified donations screen.
|
||||
* Does not directly deal with performing payments, this ViewModel is
|
||||
* only in charge of rendering our "current view of the world."
|
||||
*/
|
||||
class DonateToSignalViewModel(
|
||||
startType: DonateToSignalType,
|
||||
private val subscriptionsRepository: SubscriptionsRepository,
|
||||
private val boostRepository: BoostRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonateToSignalViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(DonateToSignalState(donateToSignalType = startType))
|
||||
private val oneTimeDonationDisposables = CompositeDisposable()
|
||||
private val monthlyDonationDisposables = CompositeDisposable()
|
||||
private val networkDisposable = CompositeDisposable()
|
||||
private val _actions = PublishSubject.create<DonateToSignalAction>()
|
||||
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
|
||||
|
||||
private var gatewayRequest: GatewayRequest? = null
|
||||
|
||||
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
init {
|
||||
initializeOneTimeDonationState(boostRepository)
|
||||
initializeMonthlyDonationState(subscriptionsRepository)
|
||||
|
||||
networkDisposable += InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retryMonthlyDonationState()
|
||||
retryOneTimeDonationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retryMonthlyDonationState() {
|
||||
if (!monthlyDonationDisposables.isDisposed && store.state.monthlyDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
|
||||
store.update { it.copy(monthlyDonationState = it.monthlyDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
|
||||
initializeMonthlyDonationState(subscriptionsRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun retryOneTimeDonationState() {
|
||||
if (!oneTimeDonationDisposables.isDisposed && store.state.oneTimeDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
|
||||
initializeOneTimeDonationState(boostRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestChangeCurrency() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.canSetCurrency) {
|
||||
_actions.onNext(DonateToSignalAction.DisplayCurrencySelectionDialog(snapshot.donateToSignalType, snapshot.selectableCurrencyCodes))
|
||||
}
|
||||
}
|
||||
|
||||
fun requestSelectGateway() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.DisplayGatewaySelectorDialog(createGatewayRequest(snapshot)))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscription() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot)))
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.CancelSubscription(createGatewayRequest(snapshot)))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDonationType() {
|
||||
store.update { it.copy(donateToSignalType = it.donateToSignalType.next()) }
|
||||
}
|
||||
|
||||
fun setSelectedSubscription(subscription: Subscription) {
|
||||
store.update { it.copy(monthlyDonationState = it.monthlyDonationState.copy(selectedSubscription = subscription)) }
|
||||
}
|
||||
|
||||
fun setSelectedBoost(boost: Boost) {
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(selectedBoost = boost, isCustomAmountFocused = false)) }
|
||||
}
|
||||
|
||||
fun setCustomAmountFocused() {
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(isCustomAmountFocused = true)) }
|
||||
}
|
||||
|
||||
fun setCustomAmount(rawAmount: String) {
|
||||
val amount = StringUtil.stripBidiIndicator(rawAmount)
|
||||
val bigDecimalAmount: BigDecimal = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
val decimalFormat = DecimalFormat.getInstance() as DecimalFormat
|
||||
decimalFormat.isParseBigDecimal = true
|
||||
|
||||
try {
|
||||
decimalFormat.parse(amount) as BigDecimal
|
||||
} catch (e: NumberFormatException) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(customAmount = FiatMoney(bigDecimalAmount, it.oneTimeDonationState.customAmount.currency))) }
|
||||
}
|
||||
|
||||
fun getSelectedSubscriptionCost(): FiatMoney {
|
||||
return store.state.monthlyDonationState.selectedSubscription!!.prices.first { it.currency == store.state.selectedCurrency }
|
||||
}
|
||||
|
||||
fun refreshActiveSubscription() {
|
||||
subscriptionsRepository
|
||||
.getActiveSubscription()
|
||||
.subscribeBy(
|
||||
onSuccess = {
|
||||
_activeSubscription.onNext(it)
|
||||
},
|
||||
onError = {
|
||||
_activeSubscription.onNext(ActiveSubscription.EMPTY)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGatewayRequest(snapshot: DonateToSignalState): GatewayRequest {
|
||||
val amount = getAmount(snapshot)
|
||||
return GatewayRequest(
|
||||
donateToSignalType = snapshot.donateToSignalType,
|
||||
badge = snapshot.badge!!,
|
||||
label = snapshot.badge!!.description,
|
||||
price = amount.amount,
|
||||
currencyCode = amount.currency.currencyCode,
|
||||
level = snapshot.level.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAmount(snapshot: DonateToSignalState): FiatMoney {
|
||||
return when (snapshot.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOneTimeAmount(snapshot: DonateToSignalState.OneTimeDonationState): FiatMoney {
|
||||
return if (snapshot.isCustomAmountFocused) {
|
||||
snapshot.customAmount
|
||||
} else {
|
||||
snapshot.selectedBoost!!.price
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOneTimeDonationState(boostRepository: BoostRepository) {
|
||||
oneTimeDonationDisposables += boostRepository.getBoostBadge().subscribeBy(
|
||||
onSuccess = { badge ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) }
|
||||
},
|
||||
onError = {
|
||||
Log.w(TAG, "Could not load boost badge", it)
|
||||
}
|
||||
)
|
||||
|
||||
val boosts: Observable<Map<Currency, List<Boost>>> = boostRepository.getBoosts().toObservable()
|
||||
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
||||
|
||||
oneTimeDonationDisposables += Observable.combineLatest(boosts, oneTimeCurrency) { boostMap, currency ->
|
||||
val boostList = if (currency in boostMap) {
|
||||
boostMap[currency]!!
|
||||
} else {
|
||||
SignalStore.donationsValues().setOneTimeCurrency(PlatformCurrencyUtil.USD)
|
||||
listOf()
|
||||
}
|
||||
|
||||
Triple(boostList, currency, boostMap.keys)
|
||||
}.subscribeBy(
|
||||
onNext = { (boostList, currency, availableCurrencies) ->
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
oneTimeDonationState = state.oneTimeDonationState.copy(
|
||||
boosts = boostList,
|
||||
selectedCurrency = currency,
|
||||
donationStage = DonateToSignalState.DonationStage.READY,
|
||||
selectableCurrencyCodes = availableCurrencies.map(Currency::getCurrencyCode),
|
||||
isCustomAmountFocused = false,
|
||||
customAmount = FiatMoney(
|
||||
BigDecimal.ZERO, currency
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
Log.w(TAG, "Could not load boost information", it)
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.FAILURE)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeMonthlyDonationState(subscriptionsRepository: SubscriptionsRepository) {
|
||||
monitorLevelUpdateProcessing()
|
||||
|
||||
val allSubscriptions = subscriptionsRepository.getSubscriptions()
|
||||
ensureValidSubscriptionCurrency(allSubscriptions)
|
||||
monitorSubscriptionCurrency()
|
||||
monitorSubscriptionState(allSubscriptions)
|
||||
refreshActiveSubscription()
|
||||
}
|
||||
|
||||
private fun monitorLevelUpdateProcessing() {
|
||||
val isTransactionJobInProgress: Observable<Boolean> = SubscriptionRedemptionJobWatcher.watch().map {
|
||||
it.map { jobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> true
|
||||
JobTracker.JobState.RUNNING -> true
|
||||
else -> false
|
||||
}
|
||||
}.orElse(false)
|
||||
}
|
||||
|
||||
monthlyDonationDisposables += Observable
|
||||
.combineLatest(isTransactionJobInProgress, LevelUpdate.isProcessing, DonateToSignalState::TransactionState)
|
||||
.subscribeBy { transactionState ->
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
monthlyDonationState = state.monthlyDonationState.copy(
|
||||
transactionState = transactionState
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun monitorSubscriptionState(allSubscriptions: Single<List<Subscription>>) {
|
||||
monthlyDonationDisposables += Observable.combineLatest(allSubscriptions.toObservable(), _activeSubscription, ::Pair).subscribeBy(
|
||||
onNext = { (subs, active) ->
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
monthlyDonationState = state.monthlyDonationState.copy(
|
||||
subscriptions = subs,
|
||||
selectedSubscription = state.monthlyDonationState.selectedSubscription ?: resolveSelectedSubscription(active, subs),
|
||||
_activeSubscription = active,
|
||||
donationStage = DonateToSignalState.DonationStage.READY,
|
||||
selectableCurrencyCodes = subs.firstOrNull()?.prices?.map { it.currency.currencyCode } ?: emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
monthlyDonationState = state.monthlyDonationState.copy(
|
||||
donationStage = DonateToSignalState.DonationStage.FAILURE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveSelectedSubscription(activeSubscription: ActiveSubscription, subscriptions: List<Subscription>): Subscription? {
|
||||
return if (activeSubscription.isActive) {
|
||||
subscriptions.firstOrNull { it.level == activeSubscription.activeSubscription.level }
|
||||
} else {
|
||||
subscriptions.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureValidSubscriptionCurrency(allSubscriptions: Single<List<Subscription>>) {
|
||||
monthlyDonationDisposables += allSubscriptions.subscribeBy(
|
||||
onSuccess = { subscriptions ->
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
||||
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
|
||||
if (selectedCurrency !in priceCurrencies) {
|
||||
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $selectedCurrency isn't supported.")
|
||||
val usd = PlatformCurrencyUtil.USD
|
||||
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
||||
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
||||
subscriptionsRepository.syncAccountRecord().subscribe()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {}
|
||||
)
|
||||
}
|
||||
|
||||
private fun monitorSubscriptionCurrency() {
|
||||
monthlyDonationDisposables += SignalStore.donationsValues().observableSubscriptionCurrency.subscribe {
|
||||
store.update { state ->
|
||||
state.copy(monthlyDonationState = state.monthlyDonationState.copy(selectedCurrency = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
oneTimeDonationDisposables.clear()
|
||||
monthlyDonationDisposables.clear()
|
||||
networkDisposable.clear()
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
fun provideGatewayRequest(request: GatewayRequest) {
|
||||
Log.d(TAG, "Provided with a gateway request.")
|
||||
Preconditions.checkState(gatewayRequest == null)
|
||||
gatewayRequest = request
|
||||
}
|
||||
|
||||
fun consumeGatewayRequest(): GatewayRequest? {
|
||||
val request = gatewayRequest
|
||||
gatewayRequest = null
|
||||
return request
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startType: DonateToSignalType,
|
||||
private val subscriptionsRepository: SubscriptionsRepository = SubscriptionsRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val boostRepository: BoostRepository = BoostRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, boostRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.DonationPillToggleBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
object DonationPillToggle {
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, DonationPillToggleBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val isEnabled: Boolean,
|
||||
val selected: DonateToSignalType,
|
||||
val onClick: () -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return isEnabled == newItem.isEnabled && selected == newItem.selected
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(binding: DonationPillToggleBinding) : BindingViewHolder<Model, DonationPillToggleBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
when (model.selected) {
|
||||
DonateToSignalType.ONE_TIME -> {
|
||||
presentButtons(model, binding.oneTime, binding.monthly)
|
||||
}
|
||||
DonateToSignalType.MONTHLY -> {
|
||||
presentButtons(model, binding.monthly, binding.oneTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentButtons(model: Model, selected: MaterialButton, notSelected: MaterialButton) {
|
||||
selected.setOnClickListener(null)
|
||||
notSelected.setOnClickListener { model.onClick() }
|
||||
selected.isSelected = true
|
||||
notSelected.isSelected = false
|
||||
selected.setIconResource(R.drawable.ic_check_24)
|
||||
notSelected.icon = null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@Parcelize
|
||||
data class GatewayRequest(
|
||||
val donateToSignalType: DonateToSignalType,
|
||||
val badge: Badge,
|
||||
val label: String,
|
||||
val price: BigDecimal,
|
||||
val currencyCode: String,
|
||||
val level: Long
|
||||
) : Parcelable {
|
||||
val fiat: FiatMoney = FiatMoney(price, Currency.getInstance(currencyCode))
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class GatewayResponse(val gateway: Gateway, val request: GatewayRequest) : Parcelable {
|
||||
enum class Gateway {
|
||||
GOOGLE_PAY,
|
||||
PAYPAL,
|
||||
CREDIT_CARD
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
||||
/**
|
||||
* Entry point to capturing the necessary payment token to pay for a donation
|
||||
*/
|
||||
class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val args: GatewaySelectorBottomSheetArgs by navArgs()
|
||||
|
||||
private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = {
|
||||
GatewaySelectorViewModel.Factory(args, requireListener<DonationPaymentComponent>().donationPaymentRepository)
|
||||
})
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
lifecycleDisposable += viewModel.state.subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GatewaySelectorState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.badge,
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> presentMonthlyText()
|
||||
DonateToSignalType.ONE_TIME -> presentOneTimeText()
|
||||
}
|
||||
|
||||
space(68.dp)
|
||||
|
||||
if (state.isGooglePayAvailable) {
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
isEnabled = true,
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.GOOGLE_PAY, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// PayPal
|
||||
// Credit Card
|
||||
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentMonthlyText() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge, args.request.badge.name),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentOneTimeText() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, args.request.badge.name, 30),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "payment_checkout_mode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val badge: Badge,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class GatewaySelectorViewModel(
|
||||
args: GatewaySelectorBottomSheetArgs,
|
||||
private val repository: DonationPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(GatewaySelectorState(args.request.badge))
|
||||
|
||||
val state = store.stateFlowable
|
||||
|
||||
init {
|
||||
checkIfGooglePayIsAvailable()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
private fun checkIfGooglePayIsAvailable() {
|
||||
repository.isGooglePayAvailable().subscribeBy(
|
||||
onComplete = {
|
||||
store.update { it.copy(isGooglePayAvailable = true) }
|
||||
},
|
||||
onError = {
|
||||
store.update { it.copy(isGooglePayAvailable = false) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val args: GatewaySelectorBottomSheetArgs,
|
||||
private val repository: DonationPaymentRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(GatewaySelectorViewModel(args, repository)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
enum class StripeAction : Parcelable {
|
||||
PROCESS_NEW_DONATION,
|
||||
UPDATE_SUBSCRIPTION,
|
||||
CANCEL_SUBSCRIPTION
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
|
||||
@Parcelize
|
||||
class StripeActionResult(
|
||||
val action: StripeAction,
|
||||
val request: GatewayRequest,
|
||||
val status: Status
|
||||
) : Parcelable {
|
||||
enum class Status {
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.databinding.StripePaymentInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
||||
class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_in_progress_fragment) {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "REQUEST_KEY"
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(StripePaymentInProgressFragmentBinding::bind)
|
||||
private val args: StripePaymentInProgressFragmentArgs by navArgs()
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
private val viewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().donationPaymentRepository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = false
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
disposables += viewModel.state.subscribeBy { stage ->
|
||||
presentUiState(stage)
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
when (args.action) {
|
||||
StripeAction.PROCESS_NEW_DONATION -> {
|
||||
viewModel.processNewDonation(args.request)
|
||||
}
|
||||
StripeAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request)
|
||||
}
|
||||
StripeAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentUiState(stage: StripeStage) {
|
||||
when (stage) {
|
||||
StripeStage.INIT -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment)
|
||||
StripeStage.PAYMENT_PIPELINE -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment)
|
||||
StripeStage.FAILED -> {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
REQUEST_KEY to StripeActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
status = StripeActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
StripeStage.COMPLETE -> {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
REQUEST_KEY to StripeActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
status = StripeActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
StripeStage.CANCELLING -> binding.progressCardStatus.setText(R.string.StripePaymentInProgressFragment__cancelling)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
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.signal.donations.GooglePayPaymentSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
class StripePaymentInProgressViewModel(
|
||||
private val donationPaymentRepository: DonationPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StripePaymentInProgressViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(StripeStage.INIT)
|
||||
val state: Flowable<StripeStage> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private var paymentData: PaymentData? = null
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
fun processNewDonation(request: GatewayRequest) {
|
||||
val paymentData = this.paymentData ?: error("Cannot process new donation without payment data")
|
||||
this.paymentData = null
|
||||
|
||||
Preconditions.checkState(store.state != StripeStage.PAYMENT_PIPELINE)
|
||||
Log.d(TAG, "Proceeding with donation...")
|
||||
|
||||
return when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentData)
|
||||
DonateToSignalType.ONE_TIME -> proceedOneTime(request, paymentData)
|
||||
}
|
||||
}
|
||||
|
||||
fun providePaymentData(paymentData: PaymentData) {
|
||||
this.paymentData = paymentData
|
||||
}
|
||||
|
||||
private fun proceedMonthly(request: GatewayRequest, paymentData: PaymentData) {
|
||||
val ensureSubscriberId = donationPaymentRepository.ensureSubscriberId()
|
||||
val continueSetup = donationPaymentRepository.continueSubscriptionSetup(GooglePayPaymentSource(paymentData))
|
||||
val setLevel = donationPaymentRepository.setSubscriptionLevel(request.level.toString())
|
||||
|
||||
Log.d(TAG, "Starting subscription payment pipeline...", true)
|
||||
store.update { StripeStage.PAYMENT_PIPELINE }
|
||||
|
||||
val setup = ensureSubscriberId
|
||||
.andThen(cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(continueSetup)
|
||||
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it)) }
|
||||
|
||||
setup.andThen(setLevel).subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in subscription payment pipeline...", throwable, true)
|
||||
store.update { StripeStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished subscription payment pipeline...", true)
|
||||
store.update { StripeStage.COMPLETE }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelActiveSubscriptionIfNecessary(): Completable {
|
||||
return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable {
|
||||
if (it) {
|
||||
Log.d(TAG, "Cancelling active subscription...", true)
|
||||
donationPaymentRepository.cancelActiveSubscription().doOnComplete {
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
}
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedOneTime(request: GatewayRequest, paymentData: PaymentData) {
|
||||
Log.w(TAG, "Beginning one-time payment pipeline...", true)
|
||||
|
||||
donationPaymentRepository.continuePayment(request.fiat, GooglePayPaymentSource(paymentData), Recipient.self().id, null, SubscriptionLevels.BOOST_LEVEL.toLong()).subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { StripeStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed one-time payment pipeline...", true)
|
||||
store.update { StripeStage.COMPLETE }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
store.update { StripeStage.CANCELLING }
|
||||
disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Cancellation succeeded", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
donationPaymentRepository.scheduleSyncForAccountRecordChange()
|
||||
store.update { StripeStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Cancellation failed", throwable, true)
|
||||
store.update { StripeStage.FAILED }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription(request: GatewayRequest) {
|
||||
store.update { StripeStage.PAYMENT_PIPELINE }
|
||||
cancelActiveSubscriptionIfNecessary().andThen(donationPaymentRepository.setSubscriptionLevel(request.level.toString()))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { StripeStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
store.update { StripeStage.FAILED }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val donationPaymentRepository: DonationPaymentRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(StripePaymentInProgressViewModel(donationPaymentRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
enum class StripeStage {
|
||||
INIT,
|
||||
PAYMENT_PIPELINE,
|
||||
CANCELLING,
|
||||
FAILED,
|
||||
COMPLETE
|
||||
}
|
|
@ -53,15 +53,13 @@ object ActiveSubscriptionPreference {
|
|||
|
||||
val badge: BadgeImageView = itemView.findViewById(R.id.my_support_badge)
|
||||
val title: TextView = itemView.findViewById(R.id.my_support_title)
|
||||
val price: TextView = itemView.findViewById(R.id.my_support_price)
|
||||
val expiry: TextView = itemView.findViewById(R.id.my_support_expiry)
|
||||
val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadge(model.subscription.badge)
|
||||
title.text = model.subscription.name
|
||||
|
||||
price.text = context.getString(
|
||||
title.text = context.getString(
|
||||
R.string.MySupportPreference__s_per_month,
|
||||
FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
|
@ -69,6 +67,7 @@ object ActiveSubscriptionPreference {
|
|||
FiatMoneyUtil.formatOptions()
|
||||
)
|
||||
)
|
||||
|
||||
expiry.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
when (model.redemptionState) {
|
||||
|
|
|
@ -2,10 +2,11 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
|||
|
||||
import android.content.Intent
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheet
|
||||
|
@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
|||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
|
@ -26,6 +28,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
@ -37,13 +40,17 @@ import java.util.concurrent.TimeUnit
|
|||
* Fragment displayed when a user enters "Subscriptions" via app settings but is already
|
||||
* a subscriber. Used to manage their current subscription, view badges, and boost.
|
||||
*/
|
||||
class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback {
|
||||
class ManageDonationsFragment :
|
||||
DSLSettingsFragment(
|
||||
layoutId = R.layout.manage_donations_fragment
|
||||
),
|
||||
ExpiredGiftSheet.Callback {
|
||||
|
||||
private val supportTechSummary: CharSequence by lazy {
|
||||
SpannableStringBuilder(requireContext().getString(R.string.SubscribeFragment__make_a_recurring_monthly_donation))
|
||||
SpannableStringBuilder(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant), requireContext().getString(R.string.DonateToSignalFragment__support_technology)))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_button_secondary_text)) {
|
||||
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeLearnMoreBottomSheetDialog())
|
||||
}
|
||||
)
|
||||
|
@ -77,48 +84,88 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
}
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ManageDonationsState): DSLConfiguration {
|
||||
return configure {
|
||||
space(36.dp)
|
||||
|
||||
customPref(
|
||||
BadgePreview.BadgeModel.FeaturedModel(
|
||||
BadgePreview.BadgeModel.SubscriptionModel(
|
||||
badge = state.featuredBadge
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
space(12.dp)
|
||||
|
||||
sectionHeaderPref(
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||
R.string.DonateToSignalFragment__powered_by,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(8.dp)
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(supportTechSummary, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
primaryWrappedButton(
|
||||
text = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_to_signal),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.ONE_TIME))
|
||||
}
|
||||
)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
if (state.transactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.transactionState.activeSubscription.activeSubscription
|
||||
if (activeSubscription != null) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.level == it.level }
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { it.level == activeSubscription.level }
|
||||
if (subscription != null) {
|
||||
presentSubscriptionSettings(activeSubscription, subscription, state.getRedemptionState())
|
||||
presentSubscriptionSettings(activeSubscription, subscription, state.getMonthlyDonorRedemptionState())
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
} else if (state.hasOneTimeBadge) {
|
||||
presentActiveOneTimeDonorSettings()
|
||||
} else {
|
||||
presentNoSubscriptionSettings()
|
||||
presentNotADonorSettings(state.hasReceipts)
|
||||
}
|
||||
} else if (state.transactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
presentNetworkFailureSettings(state.getRedemptionState())
|
||||
presentNetworkFailureSettings(state.getMonthlyDonorRedemptionState(), state.hasReceipts)
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentNetworkFailureSettings(redemptionState: ManageDonationsState.SubscriptionRedemptionState) {
|
||||
private fun DSLConfiguration.presentActiveOneTimeDonorSettings() {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__my_support)
|
||||
|
||||
presentBadges()
|
||||
|
||||
presentOtherWaysToGive()
|
||||
|
||||
presentMore()
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentNetworkFailureSettings(redemptionState: ManageDonationsState.SubscriptionRedemptionState, hasReceipts: Boolean) {
|
||||
if (SignalStore.donationsValues().isLikelyASustainer()) {
|
||||
presentSubscriptionSettingsWithNetworkError(redemptionState)
|
||||
} else {
|
||||
presentNoSubscriptionSettings()
|
||||
presentNotADonorSettings(hasReceipts)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,16 +210,9 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
subscriptionBlock: DSLConfiguration.() -> Unit
|
||||
) {
|
||||
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||
dividerPref()
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.ManageDonationsFragment__my_subscription,
|
||||
DSLSettingsText.Body1BoldModifier, DSLSettingsText.BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(12f).toInt())
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__my_support)
|
||||
|
||||
subscriptionBlock()
|
||||
|
||||
|
@ -181,52 +221,23 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
icon = DSLSettingsIcon.from(R.drawable.ic_person_white_24dp),
|
||||
isEnabled = redemptionState != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__badges),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_badge_24),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToManageBadges())
|
||||
}
|
||||
)
|
||||
presentBadges()
|
||||
|
||||
presentOtherWaysToGive()
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__more)
|
||||
|
||||
presentDonationReceipts()
|
||||
|
||||
externalLinkPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__subscription_faq),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
|
||||
linkId = R.string.donate_url
|
||||
)
|
||||
presentMore()
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentNoSubscriptionSettings() {
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(supportTechSummary, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
tonalButton(
|
||||
text = DSLSettingsText.from(R.string.ManageDonationsFragment__make_a_monthly_donation),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_manageDonationsFragment_to_subscribeFragment)
|
||||
}
|
||||
)
|
||||
|
||||
private fun DSLConfiguration.presentNotADonorSettings(hasReceipts: Boolean) {
|
||||
presentOtherWaysToGive()
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__receipts)
|
||||
|
||||
presentDonationReceipts()
|
||||
if (hasReceipts) {
|
||||
presentMore()
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentOtherWaysToGive() {
|
||||
|
@ -234,14 +245,6 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__other_ways_to_give)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__one_time_donation),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_boost_24),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToBoosts())
|
||||
}
|
||||
)
|
||||
|
||||
if (FeatureFlags.giftBadgeSendSupport() && Recipient.self().giftBadgesCapability == Recipient.Capability.SUPPORTED) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__gift_a_badge),
|
||||
|
@ -253,7 +256,17 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentDonationReceipts() {
|
||||
private fun DSLConfiguration.presentBadges() {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__badges),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_badge_24),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToManageBadges())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentReceipts() {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__donation_receipts),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_receipt_24),
|
||||
|
@ -263,7 +276,21 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
|
|||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentMore() {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__more)
|
||||
|
||||
presentReceipts()
|
||||
|
||||
externalLinkPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__subscription_faq),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
|
||||
linkId = R.string.donate_url
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMakeAMonthlyDonation() {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,15 @@ import org.thoughtcrime.securesms.subscription.Subscription
|
|||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
|
||||
data class ManageDonationsState(
|
||||
val hasOneTimeBadge: Boolean = false,
|
||||
val hasReceipts: Boolean = false,
|
||||
val featuredBadge: Badge? = null,
|
||||
val transactionState: TransactionState = TransactionState.Init,
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
private val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
) {
|
||||
|
||||
fun getRedemptionState(): SubscriptionRedemptionState {
|
||||
fun getMonthlyDonorRedemptionState(): SubscriptionRedemptionState {
|
||||
return when (transactionState) {
|
||||
TransactionState.Init -> subscriptionRedemptionState
|
||||
TransactionState.NetworkFailure -> subscriptionRedemptionState
|
||||
|
|
|
@ -9,8 +9,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
|
@ -60,6 +62,18 @@ class ManageDonationsViewModel(
|
|||
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
|
||||
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription()
|
||||
|
||||
disposables += Recipient.observable(Recipient.self().id).map { it.badges }.subscribeBy { badges ->
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
hasOneTimeBadge = badges.any { it.isBoost() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Single.fromCallable { SignalDatabase.donationReceipts.hasReceipts() }.subscribeOn(Schedulers.io()).subscribe { hasReceipts ->
|
||||
store.update { it.copy(hasReceipts = hasReceipts) }
|
||||
}
|
||||
|
||||
disposables += SubscriptionRedemptionJobWatcher.watch().subscribeBy { jobStateOptional ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Button
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* UX for creating and changing a subscription
|
||||
*/
|
||||
class SubscribeFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.subscribe_fragment
|
||||
) {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val supportTechSummary: CharSequence by lazy {
|
||||
SpannableStringBuilder(requireContext().getString(R.string.SubscribeFragment__make_a_recurring_monthly_donation))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_button_secondary_text)) {
|
||||
findNavController().safeNavigate(SubscribeFragmentDirections.actionSubscribeFragmentToSubscribeLearnMoreBottomSheetDialog())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private lateinit var googlePayButtonViewHolder: GooglePayButton.ViewHolder
|
||||
private lateinit var updateSubscriptionButtonViewHolder: Button.ViewHolder<Button.Model.Primary>
|
||||
private lateinit var cancelSubscriptionButtonViewHolder: Button.ViewHolder<Button.Model.SecondaryNoOutline>
|
||||
|
||||
private var errorDialog: DialogInterface? = null
|
||||
|
||||
private val viewModel: SubscribeViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
SubscribeViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()), donationPaymentComponent.donationPaymentRepository, FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshActiveSubscription()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
donationPaymentComponent = requireListener()
|
||||
viewModel.refresh()
|
||||
|
||||
BadgePreview.register(adapter)
|
||||
CurrencySelection.register(adapter)
|
||||
Subscription.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
|
||||
googlePayButtonViewHolder = GooglePayButton.ViewHolder(requireView().findViewById(R.id.pay_button_wrapper))
|
||||
updateSubscriptionButtonViewHolder = Button.ViewHolder(requireView().findViewById(R.id.update_button_wrapper))
|
||||
cancelSubscriptionButtonViewHolder = Button.ViewHolder(requireView().findViewById(R.id.cancel_button_wrapper))
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
bindFixedButtons(state)
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe {
|
||||
when (it) {
|
||||
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(it.badge)
|
||||
DonationEvent.RequestTokenSuccess -> Log.w(TAG, "Successfully got request token from Google Pay")
|
||||
DonationEvent.SubscriptionCancelled -> onSubscriptionCancelled()
|
||||
is DonationEvent.SubscriptionCancellationFailed -> onSubscriptionFailedToCancel(it.throwable)
|
||||
}
|
||||
}
|
||||
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
|
||||
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
|
||||
}
|
||||
|
||||
lifecycleDisposable += DonationError
|
||||
.getErrorsForSource(DonationErrorSource.SUBSCRIPTION)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { donationError ->
|
||||
onPaymentError(donationError)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
processingDonationPaymentDialog.dismiss()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SubscribeState): DSLConfiguration {
|
||||
if (state.hasInProgressSubscriptionTransaction || state.stage == SubscribeState.Stage.PAYMENT_PIPELINE) {
|
||||
processingDonationPaymentDialog.show()
|
||||
} else {
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
val areFieldsEnabled = state.stage == SubscribeState.Stage.READY && !state.hasInProgressSubscriptionTransaction
|
||||
|
||||
return configure {
|
||||
customPref(BadgePreview.BadgeModel.SubscriptionModel(state.selectedSubscription?.badge))
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(supportTechSummary, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
selectedCurrency = state.currencySelection,
|
||||
isEnabled = areFieldsEnabled && state.activeSubscription?.isActive != true,
|
||||
onClick = {
|
||||
val selectableCurrencies = viewModel.getSelectableCurrencyCodes()
|
||||
if (selectableCurrencies != null) {
|
||||
findNavController().safeNavigate(SubscribeFragmentDirections.actionSubscribeFragmentToSetDonationCurrencyFragment(false, selectableCurrencies.toTypedArray()))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == SubscribeState.Stage.INIT) {
|
||||
customPref(
|
||||
Subscription.LoaderModel()
|
||||
)
|
||||
} else if (state.stage == SubscribeState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(69f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.refresh()
|
||||
}
|
||||
)
|
||||
space(DimensionUnit.DP.toPixels(75f).toInt())
|
||||
} else {
|
||||
state.subscriptions.forEach {
|
||||
|
||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level && state.activeSubscription.isActive
|
||||
|
||||
val activePrice = state.activeSubscription?.activeSubscription?.let { sub ->
|
||||
val activeCurrency = Currency.getInstance(sub.currency)
|
||||
val activeAmount = sub.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
FiatMoney(activeAmount, activeCurrency)
|
||||
}
|
||||
|
||||
customPref(
|
||||
Subscription.Model(
|
||||
activePrice = if (isActive) activePrice else null,
|
||||
subscription = it,
|
||||
isSelected = state.selectedSubscription == it,
|
||||
isEnabled = areFieldsEnabled,
|
||||
isActive = isActive,
|
||||
willRenew = isActive && !state.isSubscriptionExpiring(),
|
||||
onClick = { viewModel.setSelectedSubscription(it) },
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(state.activeSubscription?.activeSubscription?.endOfCurrentPeriod ?: 0L),
|
||||
selectedCurrency = state.currencySelection
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindFixedButtons(state: SubscribeState) {
|
||||
val areFieldsEnabled = state.stage == SubscribeState.Stage.READY && !state.hasInProgressSubscriptionTransaction
|
||||
|
||||
if (state.activeSubscription?.isActive == true) {
|
||||
val activeAndSameLevel = state.activeSubscription.isActive &&
|
||||
state.selectedSubscription?.level == state.activeSubscription.activeSubscription?.level
|
||||
|
||||
val updateModel = Button.Model.Primary(
|
||||
title = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
||||
icon = null,
|
||||
isEnabled = areFieldsEnabled && (!activeAndSameLevel || state.isSubscriptionExpiring()),
|
||||
onClick = {
|
||||
val price = viewModel.getPriceOfSelectedSubscription() ?: return@Primary
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.SubscribeFragment__update_subscription_question)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.SubscribeFragment__you_will_be_charged_the_full_amount_s_of,
|
||||
FiatMoneyUtil.format(
|
||||
requireContext().resources,
|
||||
price,
|
||||
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||
)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.SubscribeFragment__update) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
viewModel.updateSubscription()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
updateSubscriptionButtonViewHolder.bind(updateModel)
|
||||
|
||||
val cancelModel = Button.Model.SecondaryNoOutline(
|
||||
title = DSLSettingsText.from(R.string.SubscribeFragment__cancel_subscription),
|
||||
icon = null,
|
||||
isEnabled = areFieldsEnabled,
|
||||
onClick = {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.SubscribeFragment__confirm_cancellation)
|
||||
.setMessage(R.string.SubscribeFragment__you_wont_be_charged_again)
|
||||
.setPositiveButton(R.string.SubscribeFragment__confirm) { d, _ ->
|
||||
d.dismiss()
|
||||
viewModel.cancel()
|
||||
}
|
||||
.setNegativeButton(R.string.SubscribeFragment__not_now) { d, _ ->
|
||||
d.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
|
||||
cancelSubscriptionButtonViewHolder.bind(cancelModel)
|
||||
|
||||
updateSubscriptionButtonViewHolder.itemView.visible = true
|
||||
cancelSubscriptionButtonViewHolder.itemView.visible = true
|
||||
googlePayButtonViewHolder.itemView.visible = false
|
||||
} else {
|
||||
val googlePayModel = GooglePayButton.Model(
|
||||
onClick = this@SubscribeFragment::onGooglePayButtonClicked,
|
||||
isEnabled = areFieldsEnabled && state.selectedSubscription != null
|
||||
)
|
||||
|
||||
googlePayButtonViewHolder.bind(googlePayModel)
|
||||
|
||||
updateSubscriptionButtonViewHolder.itemView.visible = false
|
||||
cancelSubscriptionButtonViewHolder.itemView.visible = false
|
||||
googlePayButtonViewHolder.itemView.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGooglePayButtonClicked() {
|
||||
viewModel.requestTokenFromGooglePay()
|
||||
}
|
||||
|
||||
private fun onPaymentConfirmed(badge: Badge) {
|
||||
findNavController().safeNavigate(
|
||||
SubscribeFragmentDirections.actionSubscribeFragmentToSubscribeThanksForYourSupportBottomSheetDialog(badge).setIsBoost(false),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
Log.w(TAG, "onPaymentError", throwable, true)
|
||||
|
||||
if (errorDialog != null) {
|
||||
Log.i(TAG, "Already displaying an error dialog. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSubscriptionCancelled() {
|
||||
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
|
||||
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.home(requireContext()))
|
||||
}
|
||||
|
||||
private fun onSubscriptionFailedToCancel(throwable: Throwable) {
|
||||
Log.w(TAG, "Failed to cancel subscription", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
|
||||
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SubscribeFragment::class.java)
|
||||
private const val FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE = 1000
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Currency
|
||||
|
||||
data class SubscribeState(
|
||||
val currencySelection: Currency,
|
||||
val subscriptions: List<Subscription> = listOf(),
|
||||
val selectedSubscription: Subscription? = null,
|
||||
val activeSubscription: ActiveSubscription? = null,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val stage: Stage = Stage.INIT,
|
||||
val hasInProgressSubscriptionTransaction: Boolean = false,
|
||||
) {
|
||||
|
||||
fun isSubscriptionExpiring(): Boolean {
|
||||
return activeSubscription?.isActive == true && activeSubscription.activeSubscription.willCancelAtPeriodEnd()
|
||||
}
|
||||
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
CANCELLING,
|
||||
FAILURE
|
||||
}
|
||||
}
|
|
@ -1,307 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
|
||||
class SubscribeViewModel(
|
||||
private val subscriptionsRepository: SubscriptionsRepository,
|
||||
private val donationPaymentRepository: DonationPaymentRepository,
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(SubscribeState(currencySelection = SignalStore.donationsValues().getSubscriptionCurrency()))
|
||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||
private val disposables = CompositeDisposable()
|
||||
private val networkDisposable: Disposable
|
||||
|
||||
val state: LiveData<SubscribeState> = store.stateLiveData
|
||||
val events: Observable<DonationEvent> = eventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private var subscriptionToPurchase: Subscription? = null
|
||||
private val activeSubscriptionSubject = PublishSubject.create<ActiveSubscription>()
|
||||
|
||||
init {
|
||||
networkDisposable = InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
networkDisposable.dispose()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun getPriceOfSelectedSubscription(): FiatMoney? {
|
||||
return store.state.selectedSubscription?.prices?.first { it.currency == store.state.currencySelection }
|
||||
}
|
||||
|
||||
fun getSelectableCurrencyCodes(): List<String>? {
|
||||
return store.state.subscriptions.firstOrNull()?.prices?.map { it.currency.currencyCode }
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == SubscribeState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val currency: Observable<Currency> = SignalStore.donationsValues().observableSubscriptionCurrency
|
||||
val allSubscriptions: Single<List<Subscription>> = subscriptionsRepository.getSubscriptions()
|
||||
|
||||
refreshActiveSubscription()
|
||||
|
||||
disposables += LevelUpdate.isProcessing.subscribeBy {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
hasInProgressSubscriptionTransaction = it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += allSubscriptions.subscribeBy(
|
||||
onSuccess = { subscriptions ->
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
||||
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
|
||||
if (selectedCurrency !in priceCurrencies) {
|
||||
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $currency isn't supported.")
|
||||
val usd = PlatformCurrencyUtil.USD
|
||||
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
||||
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
||||
donationPaymentRepository.scheduleSyncForAccountRecordChange()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {}
|
||||
)
|
||||
|
||||
disposables += Observable.combineLatest(allSubscriptions.toObservable(), activeSubscriptionSubject, ::Pair).subscribeBy(
|
||||
onNext = { (subs, active) ->
|
||||
store.update {
|
||||
it.copy(
|
||||
subscriptions = subs,
|
||||
selectedSubscription = it.selectedSubscription ?: resolveSelectedSubscription(active, subs),
|
||||
activeSubscription = active,
|
||||
stage = if (it.stage == SubscribeState.Stage.INIT || it.stage == SubscribeState.Stage.FAILURE) SubscribeState.Stage.READY else it.stage,
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = this::handleSubscriptionDataLoadFailure
|
||||
)
|
||||
|
||||
disposables += currency.subscribe { selection ->
|
||||
store.update { it.copy(currencySelection = selection) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSubscriptionDataLoadFailure(throwable: Throwable) {
|
||||
Log.w(TAG, "Could not load subscription data", throwable)
|
||||
store.update {
|
||||
it.copy(stage = SubscribeState.Stage.FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshActiveSubscription() {
|
||||
subscriptionsRepository
|
||||
.getActiveSubscription()
|
||||
.subscribeBy(
|
||||
onSuccess = { activeSubscriptionSubject.onNext(it) },
|
||||
onError = { activeSubscriptionSubject.onNext(ActiveSubscription.EMPTY) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveSelectedSubscription(activeSubscription: ActiveSubscription, subscriptions: List<Subscription>): Subscription? {
|
||||
return if (activeSubscription.isActive) {
|
||||
subscriptions.firstOrNull { it.level == activeSubscription.activeSubscription.level }
|
||||
} else {
|
||||
subscriptions.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelActiveSubscriptionIfNecessary(): Completable {
|
||||
return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable {
|
||||
if (it) {
|
||||
donationPaymentRepository.cancelActiveSubscription().doOnComplete {
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
}
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.CANCELLING) }
|
||||
disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy(
|
||||
onComplete = {
|
||||
eventPublisher.onNext(DonationEvent.SubscriptionCancelled)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
refreshActiveSubscription()
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
donationPaymentRepository.scheduleSyncForAccountRecordChange()
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
},
|
||||
onError = { throwable ->
|
||||
eventPublisher.onNext(DonationEvent.SubscriptionCancellationFailed(throwable))
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
val subscription = subscriptionToPurchase
|
||||
subscriptionToPurchase = null
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode, resultCode, data, this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
if (subscription != null) {
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
|
||||
|
||||
val ensureSubscriberId = donationPaymentRepository.ensureSubscriberId()
|
||||
val continueSetup = donationPaymentRepository.continueSubscriptionSetup(paymentData)
|
||||
val setLevel = donationPaymentRepository.setSubscriptionLevel(subscription.level.toString())
|
||||
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
val setup = ensureSubscriberId
|
||||
.andThen(cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(continueSetup)
|
||||
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it)) }
|
||||
|
||||
setup.andThen(setLevel).subscribeBy(
|
||||
onError = { throwable ->
|
||||
refreshActiveSubscription()
|
||||
store.update { it.copy(stage = SubscribeState.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.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(subscription.badge))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.SUBSCRIPTION, googlePayException))
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
cancelActiveSubscriptionIfNecessary().andThen(donationPaymentRepository.setSubscriptionLevel(store.state.selectedSubscription!!.level.toString()))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.selectedSubscription!!.badge))
|
||||
},
|
||||
onError = { throwable ->
|
||||
store.update { it.copy(stage = SubscribeState.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.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun requestTokenFromGooglePay() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.selectedSubscription == null) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(stage = SubscribeState.Stage.TOKEN_REQUEST) }
|
||||
|
||||
val selectedCurrency = snapshot.currencySelection
|
||||
|
||||
subscriptionToPurchase = snapshot.selectedSubscription
|
||||
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedSubscription.prices.first { it.currency == selectedCurrency }, snapshot.selectedSubscription.name, fetchTokenRequestCode)
|
||||
}
|
||||
|
||||
fun setSelectedSubscription(subscription: Subscription) {
|
||||
store.update { it.copy(selectedSubscription = subscription) }
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val subscriptionsRepository: SubscriptionsRepository,
|
||||
private val donationPaymentRepository: DonationPaymentRepository,
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(SubscribeViewModel(subscriptionsRepository, donationPaymentRepository, fetchTokenRequestCode))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SubscribeViewModel::class.java)
|
||||
}
|
||||
}
|
|
@ -96,7 +96,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
|||
controlText.setText(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__make_featured_badge)
|
||||
controlNote.visible = true
|
||||
controlState = ControlState.FEATURE
|
||||
} else if (hasOtherBadges && !displayingBadges) {
|
||||
} else if (hasOtherBadges) {
|
||||
switch.isChecked = false
|
||||
controlText.setText(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__display_on_profile)
|
||||
controlNote.visible = false
|
||||
|
|
|
@ -158,6 +158,15 @@ class DSLConfiguration {
|
|||
children.add(preference)
|
||||
}
|
||||
|
||||
fun primaryWrappedButton(
|
||||
text: DSLSettingsText,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.PrimaryWrapped(text, null, isEnabled, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun tonalButton(
|
||||
text: DSLSettingsText,
|
||||
isEnabled: Boolean = true,
|
||||
|
|
|
@ -14,6 +14,7 @@ object Button {
|
|||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model.Primary::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_primary))
|
||||
mappingAdapter.registerFactory(Model.PrimaryWrapped::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_primary_wrapped))
|
||||
mappingAdapter.registerFactory(Model.Tonal::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_tonal))
|
||||
mappingAdapter.registerFactory(Model.SecondaryNoOutline::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.dsl_button_secondary))
|
||||
}
|
||||
|
@ -28,6 +29,9 @@ object Button {
|
|||
icon = icon,
|
||||
isEnabled = isEnabled
|
||||
) {
|
||||
/**
|
||||
* Large primary button with width set to match_parent
|
||||
*/
|
||||
class Primary(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
|
@ -35,6 +39,16 @@ object Button {
|
|||
onClick: () -> Unit
|
||||
) : Model<Primary>(title, icon, isEnabled, onClick)
|
||||
|
||||
/**
|
||||
* Large primary button with width set to wrap_content
|
||||
*/
|
||||
class PrimaryWrapped(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<PrimaryWrapped>(title, icon, isEnabled, onClick)
|
||||
|
||||
class Tonal(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
|
|
|
@ -60,7 +60,6 @@ import androidx.core.view.ViewKt;
|
|||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
@ -72,6 +71,7 @@ import com.google.android.material.snackbar.Snackbar;
|
|||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
|
@ -90,6 +90,8 @@ import org.thoughtcrime.securesms.components.menu.ActionItem;
|
|||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
|
@ -179,7 +181,6 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
|||
import org.thoughtcrime.securesms.util.SignalProxyUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TopToastPopup;
|
||||
|
@ -2057,11 +2058,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
|
||||
@Override
|
||||
public void onDonateClicked() {
|
||||
NavHostFragment navHostFragment = NavHostFragment.create(R.navigation.boosts);
|
||||
|
||||
requireActivity().getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(navHostFragment, "boost_nav")
|
||||
.add(DonateToSignalFragment.Dialog.create(DonateToSignalType.ONE_TIME), "one_time_nav")
|
||||
.commitNow();
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,13 @@ class DonationReceiptDatabase(context: Context, databaseHelper: SignalDatabase)
|
|||
)
|
||||
}
|
||||
|
||||
fun hasReceipts(): Boolean {
|
||||
return readableDatabase.query(TABLE_NAME, SqlUtil.COUNT, null, null, null, null, null, null).use {
|
||||
it.moveToFirst()
|
||||
it.getInt(0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
fun addReceipt(record: DonationReceiptRecord) {
|
||||
require(record.id == -1L)
|
||||
|
||||
|
|
|
@ -4,17 +4,17 @@ import android.animation.Animator
|
|||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.databinding.SubscriptionPreferenceBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
@ -35,7 +35,7 @@ data class Subscription(
|
|||
|
||||
companion object {
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.subscription_preference))
|
||||
adapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, SubscriptionPreferenceBinding::inflate))
|
||||
adapter.registerFactory(LoaderModel::class.java, LayoutFactory({ LoaderViewHolder(it) }, R.layout.subscription_preference_loader))
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ data class Subscription(
|
|||
val isActive: Boolean,
|
||||
val willRenew: Boolean,
|
||||
override val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
val onClick: (Subscription) -> Unit,
|
||||
val renewalTimestamp: Long,
|
||||
val selectedCurrency: Currency
|
||||
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
||||
|
@ -114,27 +114,18 @@ data class Subscription(
|
|||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
private val tagline: TextView = itemView.findViewById(R.id.tagline)
|
||||
private val price: TextView = itemView.findViewById(R.id.price)
|
||||
private val check: ImageView = itemView.findViewById(R.id.check)
|
||||
class ViewHolder(binding: SubscriptionPreferenceBinding) : BindingViewHolder<Model, SubscriptionPreferenceBinding>(binding) {
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.isEnabled = model.isEnabled
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
itemView.isSelected = model.isSelected
|
||||
binding.root.isEnabled = model.isEnabled
|
||||
binding.root.setOnClickListener { model.onClick(model.subscription) }
|
||||
binding.root.isSelected = model.isSelected
|
||||
|
||||
if (payload.isEmpty()) {
|
||||
badge.setBadge(model.subscription.badge)
|
||||
badge.isClickable = false
|
||||
binding.badge.setBadge(model.subscription.badge)
|
||||
binding.badge.isClickable = false
|
||||
}
|
||||
|
||||
title.text = model.subscription.name
|
||||
tagline.text = context.getString(R.string.Subscription__earn_a_s_badge, model.subscription.badge.name)
|
||||
|
||||
val formattedPrice = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
model.activePrice ?: model.subscription.prices.first { it.currency == model.selectedCurrency },
|
||||
|
@ -142,25 +133,18 @@ data class Subscription(
|
|||
)
|
||||
|
||||
if (model.isActive && model.willRenew) {
|
||||
price.text = context.getString(
|
||||
R.string.Subscription__s_per_month_dot_renews_s,
|
||||
formattedPrice,
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), model.renewalTimestamp)
|
||||
)
|
||||
binding.tagline.text = context.getString(R.string.Subscription__renews_s, DateUtils.formatDateWithYear(Locale.getDefault(), model.renewalTimestamp))
|
||||
} else if (model.isActive) {
|
||||
price.text = context.getString(
|
||||
R.string.Subscription__s_per_month_dot_expires_s,
|
||||
formattedPrice,
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), model.renewalTimestamp)
|
||||
)
|
||||
binding.tagline.text = context.getString(R.string.Subscription__expires_s, DateUtils.formatDateWithYear(Locale.getDefault(), model.renewalTimestamp))
|
||||
} else {
|
||||
price.text = context.getString(
|
||||
R.string.Subscription__s_per_month,
|
||||
formattedPrice
|
||||
)
|
||||
binding.tagline.text = context.getString(R.string.Subscription__get_a_s_badge, model.subscription.badge.name)
|
||||
}
|
||||
|
||||
check.visible = model.isActive
|
||||
binding.title.text = context.getString(
|
||||
R.string.Subscription__s_per_month,
|
||||
formattedPrice
|
||||
)
|
||||
binding.check.visible = model.isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_colorSecondaryContainer" android:state_selected="true" />
|
||||
<item android:color="@color/signal_colorSurfaceVariant" />
|
||||
</selector>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_accent_primary_transparent_15" android:state_selected="true" />
|
||||
<item android:color="@color/transparent" />
|
||||
<item android:color="@color/signal_colorSecondaryContainer" android:state_selected="true" />
|
||||
<item android:color="@color/signal_colorSurface2" />
|
||||
</selector>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_accent_primary" android:state_selected="true" />
|
||||
<item android:color="@color/signal_button_secondary_stroke" />
|
||||
<item android:color="@color/signal_colorPrimary" android:state_selected="true" />
|
||||
<item android:color="@color/transparent" />
|
||||
</selector>
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/core_grey_65" />
|
||||
<corners android:radius="@dimen/material_button_full_round_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/signal_button_secondary_stroke" />
|
||||
<corners android:radius="@dimen/material_button_full_round_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/signal_button_secondary_stroke" />
|
||||
<corners android:radius="38dp" />
|
||||
<solid android:color="@color/signal_colorSurface2" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/signal_button_secondary_stroke" />
|
||||
<corners android:radius="@dimen/material_button_full_round_corner_radius" />
|
||||
<solid android:color="@color/signal_colorSurface2" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/custom_donation_amount_background_selected" android:state_selected="true" />
|
||||
<item android:drawable="@drawable/custom_donation_amount_background_normal" />
|
||||
</selector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_colorSurface2" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_colorSecondaryContainer" />
|
||||
<stroke android:color="@color/signal_colorPrimary" android:width="2dp" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M7.896,15L3,10.26L4.224,9.075L7.896,12.63L15.776,5L17,6.185L7.896,15Z"
|
||||
android:fillColor="#151D2C"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M2,5.651L3.151,4.5L8,9.349L12.849,4.5L14,5.651L8,11.651L2,5.651Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:color="@color/signal_button_secondary_stroke" android:width="1.5dp" />
|
||||
<corners android:radius="38dp" />
|
||||
</shape>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_accent_primary_transparent_15" />
|
||||
<stroke android:color="@color/signal_accent_primary" android:width="1dp" />
|
||||
<corners android:radius="38dp" />
|
||||
</shape>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/rounded_outline_accent_38dp" android:state_selected="true" />
|
||||
<item android:drawable="@drawable/rounded_outline_38dp" />
|
||||
</selector>
|
|
@ -2,14 +2,15 @@
|
|||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:color="@color/signal_accent_primary" android:width="2dp" />
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="@color/signal_colorSecondaryContainer" />
|
||||
<stroke android:color="@color/signal_colorPrimary" android:width="2dp" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:color="@color/signal_divider_minor" android:width="1dp" />
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="@color/signal_colorSurface2" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -6,14 +6,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:alpha="0.8">
|
||||
|
||||
<View
|
||||
android:id="@+id/boost_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_height="58sp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -22,7 +22,7 @@
|
|||
<View
|
||||
android:id="@+id/boost_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_height="58sp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
|
@ -33,7 +33,7 @@
|
|||
<View
|
||||
android:id="@+id/boost_3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_height="58sp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_2"
|
||||
|
@ -42,8 +42,8 @@
|
|||
<View
|
||||
android:id="@+id/boost_4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_height="58sp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -52,9 +52,9 @@
|
|||
<View
|
||||
android:id="@+id/boost_5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_height="58sp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_6"
|
||||
|
@ -64,8 +64,8 @@
|
|||
<View
|
||||
android:id="@+id/boost_6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_height="58sp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_5"
|
||||
|
@ -74,8 +74,8 @@
|
|||
<View
|
||||
android:id="@+id/boost_custom"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_height="58sp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:background="@drawable/boost_loading_preference_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -17,12 +17,12 @@
|
|||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$3" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -35,12 +35,12 @@
|
|||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_3"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$5" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -51,12 +51,12 @@
|
|||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_2"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$10" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -64,16 +64,16 @@
|
|||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/boost_1"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$20" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -82,17 +82,17 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/boost_6"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_4"
|
||||
app:layout_constraintTop_toBottomOf="@id/boost_2"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$50" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -100,25 +100,25 @@
|
|||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:backgroundTint="@color/signal_selectable_button_background_tint"
|
||||
app:cornerRadius="38dp"
|
||||
app:cornerRadius="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/boost_5"
|
||||
app:layout_constraintTop_toBottomOf="@id/boost_3"
|
||||
app:strokeColor="@color/signal_selectable_button_stroke"
|
||||
app:strokeWidth="1.5dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="$100" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/boost_custom"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/rounded_outline_focusable_38dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:background="@drawable/custom_donation_amount_background"
|
||||
android:gravity="center"
|
||||
android:hint="@string/Boost__enter_custom_amount"
|
||||
android:imeOptions="actionDone"
|
||||
|
@ -130,5 +130,4 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/boost_4" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ProgressCard
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost1_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_smile" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost2_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_clap" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost3_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_heart_eyes" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost4_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_fire" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost5_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_shock" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/boost6_animation"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="360dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:lottie_rawRes="@raw/boost_rockets" />
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,158 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/dsl_settings_gutter">
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/start_barrier"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
app:barrierDirection="start"
|
||||
app:constraint_referenced_ids="monthly_invis_start,one_time_invis_start"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/end_barrier"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="monthly_invis_end,one_time_invis_end" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/center_guide"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<!-- Invisible Views -->
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/monthly_invis_start"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__monthly"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:visibility="invisible"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:icon="@drawable/ic_check_20"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/center_guide"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.Start" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/one_time_invis_start"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__one_time"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:visibility="invisible"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:icon="@drawable/ic_check_20"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/center_guide"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.Start" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/monthly_invis_end"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__monthly"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:visibility="invisible"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:icon="@drawable/ic_check_20"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/center_guide"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.End" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/one_time_invis_end"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__one_time"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:visibility="invisible"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:icon="@drawable/ic_check_20"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/center_guide"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.End" />
|
||||
|
||||
<!-- End invisible views -->
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/one_time"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__one_time"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/center_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_barrier"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.Start" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/monthly"
|
||||
style="@style/Signal.Widget.Button.Medium.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/DonationPillToggle__monthly"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
app:backgroundTint="@color/donation_pill_toggle_background_tint"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp"
|
||||
app:iconTint="@color/signal_colorOnSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_barrier"
|
||||
app:layout_constraintStart_toEndOf="@id/center_guide"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.DonationTogglePill.End"
|
||||
tools:icon="@drawable/ic_check_20" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
tools:text="Primary button" />
|
||||
</FrameLayout>
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,24 +1,24 @@
|
|||
<?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"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
app:cardCornerRadius="10dp"
|
||||
app:cardElevation="0dp">
|
||||
app:cardElevation="0dp"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_outline">
|
||||
android:background="@drawable/subscription_row_item_background">
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/my_support_badge"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:badge_size="xlarge"
|
||||
app:layout_constraintBottom_toBottomOf="@id/my_support_expiry"
|
||||
|
@ -26,22 +26,25 @@
|
|||
app:layout_constraintTop_toTopOf="@id/my_support_title"
|
||||
tools:src="@drawable/test_gradient" />
|
||||
|
||||
<ProgressBar
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/my_support_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?circularProgressIndicatorStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/my_support_badge"
|
||||
app:layout_constraintEnd_toEndOf="@id/my_support_badge"
|
||||
app:layout_constraintStart_toStartOf="@id/my_support_badge"
|
||||
app:layout_constraintTop_toTopOf="@id/my_support_badge" />
|
||||
app:layout_constraintTop_toTopOf="@id/my_support_badge"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/my_support_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -49,33 +52,19 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Subscription Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/my_support_price"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/my_support_badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/my_support_title"
|
||||
tools:text="Earn a badge!" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/my_support_expiry"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_marginBottom="11dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/my_support_badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/my_support_price"
|
||||
app:layout_constraintTop_toBottomOf="@id/my_support_title"
|
||||
tools:text="$400.00" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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="match_parent"
|
||||
android:minWidth="232dp"
|
||||
android:minHeight="215dp"
|
||||
app:cardCornerRadius="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minWidth="232dp"
|
||||
android:minHeight="215dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progress_card_child"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progress_card_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/SubscribeFragment__processing_payment"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<include
|
||||
android:id="@+id/donate_with_googlepay"
|
||||
layout="@layout/donate_with_googlepay_button" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@id/fixed_button_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fixed_button_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/update_button_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/dsl_button_primary" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/cancel_button_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/dsl_button_secondary" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/pay_button_wrapper"
|
||||
layout="@layout/google_pay_button_pref" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,26 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subscription_currency_selection_donation_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="13dp"
|
||||
android:gravity="center_vertical|end"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/SubscribeFragment__currency"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintEnd_toStartOf="@id/subscription_currency_selection_spinner"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/subscription_currency_selection_spinner"
|
||||
|
@ -30,14 +16,16 @@
|
|||
android:drawablePadding="8dp"
|
||||
android:gravity="center"
|
||||
android:minHeight="32dp"
|
||||
android:padding="12dp"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:textAppearance="@style/Signal.Text.LabelMedium"
|
||||
android:textColor="@color/currency_selector_text_color"
|
||||
app:drawableEndCompat="@drawable/ic_chevron_down_20"
|
||||
app:drawableTint="@color/conversation_mention_background_color"
|
||||
app:layout_constraintBottom_toBottomOf="@id/subscription_currency_selection_donation_amount"
|
||||
app:drawableEndCompat="@drawable/ic_chevron_16"
|
||||
app:drawableTint="@color/signal_colorOutline"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/subscription_currency_selection_donation_amount"
|
||||
app:layout_constraintTop_toTopOf="@id/subscription_currency_selection_donation_amount"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="USD" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,20 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@drawable/selectable_rounded_outline"
|
||||
android:padding="16dp">
|
||||
android:background="@drawable/subscription_row_item_background"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
app:badge_size="xlarge"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -49,24 +49,10 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
tools:text="Earn a badge!" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/price"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/tagline"
|
||||
tools:text="$400.00" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -8,25 +8,25 @@
|
|||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@drawable/subscription_loading_preference_background" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@drawable/subscription_loading_preference_background" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@drawable/subscription_loading_preference_background" />
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
|
@ -63,4 +62,9 @@
|
|||
app:iconTint="@color/signal_colorOnSurfaceVariant"
|
||||
tools:icon="@drawable/ic_open_20" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/single_page_space"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,17 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="36dp"
|
||||
android:contentDescription="@string/BadgesOverviewFragment__featured_badge"
|
||||
app:badge_size="xlarge"
|
||||
tools:src="@drawable/test_gradient" />
|
||||
|
@ -22,7 +23,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title2"
|
||||
android:textAppearance="@style/Signal.Text.TitleLarge"
|
||||
tools:text="Signal Sustainer" />
|
||||
|
||||
<TextView
|
||||
|
@ -38,7 +39,8 @@
|
|||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
tools:text="Paige supports Signal by making a monthly donation. Get your own badge by donating below." />
|
||||
|
||||
</LinearLayout>
|
|
@ -101,20 +101,6 @@
|
|||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_subscribeFragment"
|
||||
app:destination="@id/subscribeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_boostsFragment"
|
||||
app:destination="@id/boosts"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_storyPrivacySettings"
|
||||
app:destination="@+id/story_privacy_settings"
|
||||
|
@ -173,8 +159,6 @@
|
|||
|
||||
<include app:graph="@navigation/app_settings_change_number" />
|
||||
|
||||
<include app:graph="@navigation/boosts" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/advancedPinSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedAdvancedPinPreferenceFragment"
|
||||
|
@ -437,16 +421,6 @@
|
|||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_subscriptions"
|
||||
app:destination="@id/subscribeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_manageDonations"
|
||||
app:destination="@id/manageDonationsFragment"
|
||||
|
@ -457,6 +431,23 @@
|
|||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_donateToSignal"
|
||||
app:destination="@id/donate_to_signal"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true">
|
||||
|
||||
<argument
|
||||
android:name="start_type"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType"
|
||||
app:nullable="false" />
|
||||
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_notificationProfiles"
|
||||
app:destination="@id/notificationProfilesFragment"
|
||||
|
@ -502,7 +493,7 @@
|
|||
<fragment
|
||||
android:id="@+id/internalSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalSettingsFragment"
|
||||
android:label="internal_settings_fragment" >
|
||||
android:label="internal_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_donorErrorConfigurationFragment"
|
||||
app:destination="@id/donorErrorConfigurationFragment" />
|
||||
|
@ -529,15 +520,6 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.manage.ManageDonationsFragment"
|
||||
android:label="manage_donations_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_subscribeFragment"
|
||||
app:destination="@id/subscribeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_manage_badges"
|
||||
app:destination="@id/manage_badges"
|
||||
|
@ -545,14 +527,6 @@
|
|||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_boosts"
|
||||
app:destination="@id/boosts"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_donationReceiptListFragment"
|
||||
app:destination="@id/donationReceiptListFragment"
|
||||
|
@ -563,6 +537,19 @@
|
|||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_subscribeLearnMoreBottomSheetDialog"
|
||||
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_donateToSignalFragment"
|
||||
app:destination="@id/donate_to_signal"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="start_type"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -590,42 +577,7 @@
|
|||
app:argType="long" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/subscribeFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.subscribe.SubscribeFragment"
|
||||
android:label="subscribe_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_subscribeFragment_to_setDonationCurrencyFragment"
|
||||
app:destination="@id/setDonationCurrencyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_subscribeFragment_to_subscribeLearnMoreBottomSheetDialog"
|
||||
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_subscribeFragment_to_subscribeThanksForYourSupportBottomSheetDialog"
|
||||
app:destination="@id/subscribeThanksForYourSupportBottomSheetDialog" />
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/setDonationCurrencyFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.currency.SetCurrencyFragment"
|
||||
android:label="set_currency_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
app:argType="boolean" />
|
||||
|
||||
<argument
|
||||
android:name="supportedCurrencyCodes"
|
||||
app:argType="string[]" />
|
||||
</dialog>
|
||||
<include app:graph="@navigation/donate_to_signal" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/subscribeLearnMoreBottomSheetDialog"
|
||||
|
@ -633,23 +585,6 @@
|
|||
android:label="subscribe_learn_more_bottom_sheet_dialog"
|
||||
tools:layout="@layout/subscribe_learn_more_bottom_sheet_dialog_fragment" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/subscribeThanksForYourSupportBottomSheetDialog"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment"
|
||||
android:label="subscribe_thanks_for_your_support_bottom_sheet_dialog"
|
||||
tools:layout="@layout/thanks_for_your_support_bottom_sheet_dialog_fragment">
|
||||
|
||||
<argument
|
||||
android:name="badge"
|
||||
app:argType="org.thoughtcrime.securesms.badges.models.Badge"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</dialog>
|
||||
|
||||
<include app:graph="@navigation/manage_badges" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/boosts"
|
||||
app:startDestination="@id/boostFragment">
|
||||
|
||||
<dialog
|
||||
android:id="@+id/boostFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.boost.BoostFragment"
|
||||
android:label="boost_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_boostFragment_to_setDonationCurrencyFragment"
|
||||
app:destination="@id/setDonationCurrencyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_boostFragment_to_boostThanksForYourSupportBottomSheetDialog"
|
||||
app:destination="@id/boostThanksForYourSupportBottomSheetDialog" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/setDonationCurrencyFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.currency.SetCurrencyFragment"
|
||||
android:label="set_currency_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
app:argType="boolean" />
|
||||
|
||||
<argument
|
||||
android:name="supportedCurrencyCodes"
|
||||
app:argType="string[]" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/boostThanksForYourSupportBottomSheetDialog"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment"
|
||||
android:label="boost_thanks_for_your_support_bottom_sheet_dialog"
|
||||
tools:layout="@layout/thanks_for_your_support_bottom_sheet_dialog_fragment">
|
||||
|
||||
<argument
|
||||
android:name="badge"
|
||||
app:argType="org.thoughtcrime.securesms.badges.models.Badge"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</dialog>
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/donate_to_signal"
|
||||
app:startDestination="@id/donateToSignalFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donateToSignalFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment"
|
||||
android:label="donate_to_signal_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="start_type"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType"
|
||||
app:nullable="false" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_setDonationCurrencyFragment"
|
||||
app:destination="@id/setDonationCurrencyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_gatewaySelectorBottomSheetDialog"
|
||||
app:destination="@id/gatewaySelectorBottomSheet" />
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_stripePaymentInProgressFragment"
|
||||
app:destination="@id/stripePaymentInProgressFragment" />
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_thanksForYourSupportBottomSheetDialog"
|
||||
app:destination="@id/thanksForYourSupportBottomSheetDialog" />
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_subscribeLearnMoreBottomSheetDialog"
|
||||
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/gatewaySelectorBottomSheet"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet"
|
||||
android:label="gateway_selector_bottom_sheet"
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet">
|
||||
|
||||
<argument
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/setDonationCurrencyFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.currency.SetCurrencyFragment"
|
||||
android:label="set_currency_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
app:argType="boolean" />
|
||||
|
||||
<argument
|
||||
android:name="supportedCurrencyCodes"
|
||||
app:argType="string[]" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/stripePaymentInProgressFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
|
||||
android:label="stripe_payment_in_progress_fragment"
|
||||
tools:layout="@layout/stripe_payment_in_progress_fragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripeAction"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/thanksForYourSupportBottomSheetDialog"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment"
|
||||
android:label="thanks_for_your_support_bottom_sheet_dialog"
|
||||
tools:layout="@layout/thanks_for_your_support_bottom_sheet_dialog_fragment">
|
||||
|
||||
<argument
|
||||
android:name="badge"
|
||||
app:argType="org.thoughtcrime.securesms.badges.models.Badge"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="isBoost"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/subscribeLearnMoreBottomSheetDialog"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.subscribe.SubscribeLearnMoreBottomSheetDialogFragment"
|
||||
android:label="subscribe_learn_more_bottom_sheet_dialog"
|
||||
tools:layout="@layout/subscribe_learn_more_bottom_sheet_dialog_fragment" />
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="ShapeAppearanceOverlay.Signal.DonationTogglePill.End" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopLeft">12dp</item>
|
||||
<item name="cornerSizeBottomLeft">12dp</item>
|
||||
<item name="cornerSizeTopRight">0dp</item>
|
||||
<item name="cornerSizeBottomRight">0dp </item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.DonationTogglePill.Start" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopLeft">0dp</item>
|
||||
<item name="cornerSizeBottomLeft">0dp</item>
|
||||
<item name="cornerSizeTopRight">12dp</item>
|
||||
<item name="cornerSizeBottomRight">12dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -210,6 +210,9 @@
|
|||
<item name="android:windowAnimationStyle">@style/TextSecure.Animation.FullScreenDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.DayNight.Dialog.FullScreen.Donate">
|
||||
</style>
|
||||
|
||||
<style name="Signal.Media.Dialog.AddMessageDialog" parent="TextSecure.MediaPreview">
|
||||
<item name="android:windowAnimationStyle">@style/TextSecure.Animation.AddMessageDialog</item>
|
||||
</style>
|
||||
|
|
|
@ -4397,10 +4397,8 @@
|
|||
|
||||
<string name="ImageView__badge">Badge</string>
|
||||
|
||||
<string name="SubscribeFragment__signal_is_powered_by_people_like_you">Signal is powered by people like you.</string>
|
||||
<string name="SubscribeFragment__support_technology_that_is_built_for_you">Support technology that is built for you—not for your data—by joining the community of people that sustain it.</string>
|
||||
<string name="SubscribeFragment__support_technology_that_is_built_for_you_not">Support technology that is built for you, not for your data, by joining the community that sustains Signal.</string>
|
||||
<string name="SubscribeFragment__make_a_recurring_monthly_donation">Make a recurring monthly donation to Signal to support technology built for you, not your data.</string>
|
||||
<string name="SubscribeFragment__currency">Currency</string>
|
||||
<string name="SubscribeFragment__more_payment_options">More Payment Options</string>
|
||||
<string name="SubscribeFragment__cancel_subscription">Cancel Subscription</string>
|
||||
|
@ -4415,8 +4413,10 @@
|
|||
<string name="SubscribeFragment__you_will_be_charged_the_full_amount_s_of">You will be charged the full amount (%1$s) of the new subscription price today. Your subscription will renew monthly.</string>
|
||||
|
||||
<string name="Subscription__s_per_month">%s/month</string>
|
||||
<string name="Subscription__s_per_month_dot_renews_s">%1$s/month · Renews %2$s</string>
|
||||
<string name="Subscription__s_per_month_dot_expires_s">%1$s/month · Expires %2$s</string>
|
||||
<!-- Shown when a subscription is active and isn't going to expire at the end of the term -->
|
||||
<string name="Subscription__renews_s">Renews %1$s</string>
|
||||
<!-- Shown when a subscription is active and is going to expire at the end of the term -->
|
||||
<string name="Subscription__expires_s">Expires %1$s</string>
|
||||
|
||||
<!-- First small text blurb on learn more sheet -->
|
||||
<string name="SubscribeLearnMoreBottomSheetDialogFragment__signal_is_a_nonprofit_with_no">Signal is a nonprofit with no advertisers or investors, sustained only by the people who use and value it. Make a recurring monthly donation and receive a profile badge to share your support.</string>
|
||||
|
@ -4438,14 +4438,14 @@
|
|||
<string name="BecomeASustainerFragment__get_badges">Get badges for your profile by supporting Signal.</string>
|
||||
<string name="BecomeASustainerFragment__signal_is_a_non_profit">Signal is a nonprofit with no advertisers or investors, supported only by people like you.</string>
|
||||
|
||||
<!-- Button label for creating a monthly donation -->
|
||||
<string name="ManageDonationsFragment__make_a_monthly_donation">Make a monthly donation</string>
|
||||
<!-- Button label for creating a donation -->
|
||||
<string name="ManageDonationsFragment__donate_to_signal">Donate to Signal</string>
|
||||
<!-- Heading for more area of manage subscriptions page -->
|
||||
<string name="ManageDonationsFragment__more">More</string>
|
||||
<!-- Heading for receipts area of manage subscriptions page -->
|
||||
<string name="ManageDonationsFragment__receipts">Receipts</string>
|
||||
<!-- Heading for my subscription area of manage subscriptions page -->
|
||||
<string name="ManageDonationsFragment__my_subscription">My subscription</string>
|
||||
<string name="ManageDonationsFragment__my_support">My support</string>
|
||||
<string name="ManageDonationsFragment__manage_subscription">Manage subscription</string>
|
||||
<!-- Label for Donation Receipts button -->
|
||||
<string name="ManageDonationsFragment__donation_receipts">Donation Receipts</string>
|
||||
|
@ -4457,10 +4457,6 @@
|
|||
<!-- Preference label to launch badge gifting -->
|
||||
<string name="ManageDonationsFragment__gift_a_badge">Gift a badge</string>
|
||||
|
||||
<string name="BoostFragment__give_signal_a_boost">Give Signal a Boost</string>
|
||||
<!-- Description text in boost sheet -->
|
||||
<string name="BoostFragment__make_a_one_time">Make a one-time donation and earn a Boost badge for %1$d days.</string>
|
||||
|
||||
<string name="Boost__enter_custom_amount">Enter Custom Amount</string>
|
||||
<string name="Boost__one_time_contribution">One-time contribution</string>
|
||||
|
||||
|
@ -4501,7 +4497,7 @@
|
|||
|
||||
<string name="Subscription__please_contact_support_for_more_information">Please contact support for more information.</string>
|
||||
<string name="Subscription__contact_support">Contact Support</string>
|
||||
<string name="Subscription__earn_a_s_badge">Earn a %1$s badge</string>
|
||||
<string name="Subscription__get_a_s_badge">Get a %1$s badge</string>
|
||||
|
||||
<string name="SubscribeFragment__processing_payment">Processing payment…</string>
|
||||
<!-- Displayed in notification when user payment fails to process on Stripe -->
|
||||
|
@ -5485,6 +5481,31 @@
|
|||
<string name="ExportSmsFullError__please_try_again_if_the_problem_persists">Please try again. If the problem persists, </string>
|
||||
|
||||
|
||||
<!-- DonateToSignalFragment -->
|
||||
<!-- Title below avatar -->
|
||||
<string name="DonateToSignalFragment__powered_by">Powered by people like you.</string>
|
||||
<!-- Continue button label -->
|
||||
<string name="DonateToSignalFragment__continue">Continue</string>
|
||||
<!-- Description below title -->
|
||||
<string name="DonateToSignalFragment__support_technology">Support technology built for you, not your data, by joining the community that sustains Signal.</string>
|
||||
<!-- Donation pill toggle monthly text -->
|
||||
<string name="DonationPillToggle__monthly">Monthly</string>
|
||||
<!-- Donation pill toggle one-time text -->
|
||||
<string name="DonationPillToggle__one_time">One-time</string>
|
||||
|
||||
<!-- GatewaySelectorBottomSheet -->
|
||||
<!-- Sheet title when subscribing -->
|
||||
<string name="GatewaySelectorBottomSheet__donate_s_month_to_signal">Donate %1$s/month to Signal</string>
|
||||
<!-- Sheet summary when subscribing -->
|
||||
<string name="GatewaySelectorBottomSheet__get_a_s_badge">Get a %1$s badge</string>
|
||||
<!-- Sheet title when giving a one-time donation -->
|
||||
<string name="GatewaySelectorBottomSheet__donate_s_to_signal">Donate %1$s to Signal</string>
|
||||
<!-- Sheet summary when giving a one-time donation -->
|
||||
<string name="GatewaySelectorBottomSheet__get_a_s_badge_for_d_days">Get a %1$s badge for %2$d days</string>
|
||||
|
||||
<!-- StripePaymentInProgressFragment -->
|
||||
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -538,6 +538,22 @@
|
|||
<item name="cornerSize">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.DonationTogglePill.Start" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopLeft">12dp</item>
|
||||
<item name="cornerSizeBottomLeft">12dp</item>
|
||||
<item name="cornerSizeTopRight">0dp</item>
|
||||
<item name="cornerSizeBottomRight">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.DonationTogglePill.End" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopLeft">0dp</item>
|
||||
<item name="cornerSizeBottomLeft">0dp</item>
|
||||
<item name="cornerSizeTopRight">12dp</item>
|
||||
<item name="cornerSizeBottomRight">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.Circle" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">50%</item>
|
||||
|
|
Ładowanie…
Reference in New Issue