Implement donation receipts.

This commit is contained in:
Alex Hart
2022-02-22 13:41:36 -04:00
committed by Greyson Parrelli
parent 63dab3f4b0
commit 7b499f96be
35 changed files with 1286 additions and 11 deletions

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
@@ -95,7 +96,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationError.boostAmountTooSmall())
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationError.boostAmountTooLarge())
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationError.invalidCurrencyForBoost())
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(paymentData, result.paymentIntent)
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(price, paymentData, result.paymentIntent)
}
}
}
@@ -139,14 +140,15 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
}
}
private fun confirmPayment(paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
private fun confirmPayment(price: FiatMoney, paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
Log.d(TAG, "Confirming payment intent...", true)
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).onErrorResumeNext {
Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.BOOST, it))
}
val waitOnRedemption = Completable.create {
Log.d(TAG, "Confirmed payment intent.", true)
Log.d(TAG, "Confirmed payment intent. Recording boost receipt and submitting badge reimbursement job chain.", true)
SignalDatabase.donationReceipts.addReceipt(DonationReceiptRecord.createForBoost(price))
val countDownLatch = CountDownLatch(1)
var finalJobState: JobTracker.JobState? = null

View File

@@ -147,6 +147,14 @@ class ManageDonationsFragment : DSLSettingsFragment() {
icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
linkId = R.string.donate_url
)
clickPref(
title = DSLSettingsText.from(R.string.ManageDonationsFragment__tax_receipts),
icon = DSLSettingsIcon.from(R.drawable.ic_receipt_24),
onClick = {
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonationReceiptListFragment())
}
)
}
}

View File

@@ -0,0 +1,161 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.view.drawToBitmap
import androidx.fragment.app.viewModels
import com.google.android.material.button.MaterialButton
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
import java.io.ByteArrayOutputStream
import java.util.Locale
class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.donation_receipt_detail_fragment) {
private lateinit var progressDialog: ProgressDialog
private val viewModel: DonationReceiptDetailViewModel by viewModels(
factoryProducer = {
DonationReceiptDetailViewModel.Factory(
DonationReceiptDetailFragmentArgs.fromBundle(requireArguments()).id,
DonationReceiptDetailRepository()
)
}
)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
SplashImage.register(adapter)
val sharePngButton: MaterialButton = requireView().findViewById(R.id.share_png)
sharePngButton.isEnabled = false
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.donationReceiptRecord != null) {
adapter.submitList(getConfiguration(state.donationReceiptRecord, state.subscriptionName).toMappingModelList())
}
if (state.donationReceiptRecord != null && state.subscriptionName != null) {
sharePngButton.isEnabled = true
sharePngButton.setOnClickListener {
renderPng(state.donationReceiptRecord, state.subscriptionName)
}
}
}
}
private fun renderPng(record: DonationReceiptRecord, subscriptionName: String) {
progressDialog = ProgressDialog(requireContext())
progressDialog.show()
val today: String = DateUtils.formatDateWithDayOfWeek(Locale.getDefault(), System.currentTimeMillis())
val amount: String = FiatMoneyUtil.format(resources, record.amount)
val type: String = when (record.type) {
DonationReceiptRecord.Type.RECURRING -> getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, getString(R.string.DonationReceiptListFragment__recurring))
DonationReceiptRecord.Type.BOOST -> getString(R.string.DonationReceiptListFragment__one_time)
}
val datePaid: String = DateUtils.formatDate(Locale.getDefault(), record.timestamp)
SimpleTask.run(viewLifecycleOwner.lifecycle, {
val outputStream = ByteArrayOutputStream()
val view = LayoutInflater
.from(requireContext())
.inflate(R.layout.donation_receipt_png, null)
view.findViewById<TextView>(R.id.date).text = today
view.findViewById<TextView>(R.id.amount).text = amount
view.findViewById<TextView>(R.id.donation_type).text = type
view.findViewById<TextView>(R.id.date_paid).text = datePaid
view.measure(View.MeasureSpec.makeMeasureSpec(DONATION_RECEIPT_WIDTH, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
val bitmap = view.drawToBitmap()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream)
BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType("image/png")
.withFileName("Signal-Donation-Receipt.png")
.createForSingleSessionInMemory()
}, {
progressDialog.dismiss()
openShareSheet(it)
})
}
private fun openShareSheet(uri: Uri) {
val mimeType = Intent.normalizeMimeType("image/png")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setStream(uri)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No activity existed to share the media.", e)
Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
}
}
private fun getConfiguration(record: DonationReceiptRecord, subscriptionName: String?): DSLConfiguration {
return configure {
customPref(
SplashImage.Model(
splashImageResId = R.drawable.ic_signal_logo_type
)
)
textPref(
title = DSLSettingsText.from(
charSequence = FiatMoneyUtil.format(resources, record.amount),
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_Giant),
DSLSettingsText.CenterModifier
)
)
dividerPref()
textPref(
title = DSLSettingsText.from(R.string.DonationReceiptDetailsFragment__donation_type),
summary = DSLSettingsText.from(
when (record.type) {
DonationReceiptRecord.Type.RECURRING -> getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, getString(R.string.DonationReceiptListFragment__recurring))
DonationReceiptRecord.Type.BOOST -> getString(R.string.DonationReceiptListFragment__one_time)
}
)
)
textPref(
title = DSLSettingsText.from(R.string.DonationReceiptDetailsFragment__date_paid),
summary = record.let { DSLSettingsText.from(DateUtils.formatDateWithYear(Locale.getDefault(), it.timestamp)) }
)
}
}
companion object {
private const val DONATION_RECEIPT_WIDTH = 1916
private val TAG = Log.tag(DonationReceiptDetailFragment::class.java)
}
}

View File

@@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.util.Locale
class DonationReceiptDetailRepository {
fun getSubscriptionLevelName(subscriptionLevel: Int): Single<String> {
return ApplicationDependencies
.getDonationsService()
.getSubscriptionLevels(Locale.getDefault())
.flatMap { it.flattenResult() }
.map { it.levels[subscriptionLevel.toString()] ?: throw Exception("Subscription level $subscriptionLevel not found") }
.map { it.name }
.subscribeOn(Schedulers.io())
}
fun getDonationReceiptRecord(id: Long): Single<DonationReceiptRecord> {
return Single.fromCallable<DonationReceiptRecord> {
SignalDatabase.donationReceipts.getReceipt(id)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
data class DonationReceiptDetailState(
val donationReceiptRecord: DonationReceiptRecord? = null,
val subscriptionName: String? = null
)

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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 org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.livedata.Store
class DonationReceiptDetailViewModel(id: Long, private val repository: DonationReceiptDetailRepository) : ViewModel() {
private val store = Store(DonationReceiptDetailState())
private val disposables = CompositeDisposable()
private var networkDisposable: Disposable
private val cachedRecord: Single<DonationReceiptRecord> = repository.getDonationReceiptRecord(id).cache()
val state: LiveData<DonationReceiptDetailState> = store.stateLiveData
init {
networkDisposable = InternetConnectionObserver
.observe()
.distinctUntilChanged()
.subscribe { isConnected ->
if (isConnected) {
retry()
}
}
refresh()
}
private fun retry() {
if (store.state.subscriptionName == null) {
refresh()
}
}
private fun refresh() {
disposables.clear()
disposables += cachedRecord.subscribe { record ->
store.update { it.copy(donationReceiptRecord = record) }
}
disposables += cachedRecord.flatMap {
if (it.subscriptionLevel > 0) {
repository.getSubscriptionLevelName(it.subscriptionLevel)
} else {
Single.just("")
}
}.subscribe { name ->
store.update { it.copy(subscriptionName = name) }
}
}
override fun onCleared() {
disposables.clear()
networkDisposable.dispose()
}
class Factory(private val id: Long, private val repository: DonationReceiptDetailRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.cast(DonationReceiptDetailViewModel(id, repository)) as T
}
}
}

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
data class DonationReceiptBadge(
val type: DonationReceiptRecord.Type,
val level: Int,
val badge: Badge
)

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import android.view.LayoutInflater
import android.view.ViewGroup
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.SectionHeaderPreference
import org.thoughtcrime.securesms.components.settings.SectionHeaderPreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.TextPreference
import org.thoughtcrime.securesms.components.settings.TextPreferenceViewHolder
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.toLocalDateTime
class DonationReceiptListAdapter(onModelClick: (DonationReceiptListItem.Model) -> Unit) : MappingAdapter(), StickyHeaderDecoration.StickyHeaderAdapter<SectionHeaderPreferenceViewHolder> {
init {
registerFactory(TextPreference::class.java, LayoutFactory({ TextPreferenceViewHolder(it) }, R.layout.dsl_preference_item))
DonationReceiptListItem.register(this, onModelClick)
}
override fun getHeaderId(position: Int): Long {
return when (val item = getItem(position)) {
is DonationReceiptListItem.Model -> item.record.timestamp.toLocalDateTime().year.toLong()
else -> StickyHeaderDecoration.StickyHeaderAdapter.NO_HEADER_ID
}
}
override fun onCreateHeaderViewHolder(parent: ViewGroup?, position: Int, type: Int): SectionHeaderPreferenceViewHolder {
return SectionHeaderPreferenceViewHolder(LayoutInflater.from(parent!!.context).inflate(R.layout.dsl_section_header, parent, false))
}
override fun onBindHeaderViewHolder(viewHolder: SectionHeaderPreferenceViewHolder?, position: Int, type: Int) {
viewHolder?.bind(SectionHeaderPreference(DSLSettingsText.from(getHeaderId(position).toString())))
}
}

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.BoldSelectionTabItem
import org.thoughtcrime.securesms.components.ControllableTabLayout
class DonationReceiptListFragment : Fragment(R.layout.donation_receipt_list_fragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val pager: ViewPager2 = view.findViewById(R.id.pager)
val tabs: ControllableTabLayout = view.findViewById(R.id.tabs)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
pager.adapter = DonationReceiptListPageAdapter(this)
BoldSelectionTabItem.registerListeners(tabs)
TabLayoutMediator(tabs, pager) { tab, position ->
tab.setText(
when (position) {
0 -> R.string.DonationReceiptListFragment__all
1 -> R.string.DonationReceiptListFragment__recurring
2 -> R.string.DonationReceiptListFragment__one_time
else -> error("Unsupported index $position")
}
)
}.attach()
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import android.view.View
import android.widget.TextView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
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 java.util.Locale
object DonationReceiptListItem {
fun register(adapter: MappingAdapter, onClick: (Model) -> Unit) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onClick) }, R.layout.donation_receipt_list_item))
}
class Model(
val record: DonationReceiptRecord,
val badge: Badge?
) : MappingModel<Model> {
override fun areContentsTheSame(newItem: Model): Boolean = record == newItem.record && badge == newItem.badge
override fun areItemsTheSame(newItem: Model): Boolean = record.id == newItem.record.id
}
private class ViewHolder(itemView: View, private val onClick: (Model) -> Unit) : MappingViewHolder<Model>(itemView) {
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
private val dateView: TextView = itemView.findViewById(R.id.date)
private val typeView: TextView = itemView.findViewById(R.id.type)
private val moneyView: TextView = itemView.findViewById(R.id.money)
override fun bind(model: Model) {
itemView.setOnClickListener { onClick(model) }
badgeView.setBadge(model.badge)
dateView.text = DateUtils.formatDate(Locale.getDefault(), model.record.timestamp)
typeView.setText(
when (model.record.type) {
DonationReceiptRecord.Type.RECURRING -> R.string.DonationReceiptListFragment__recurring
DonationReceiptRecord.Type.BOOST -> R.string.DonationReceiptListFragment__one_time
}
)
moneyView.text = FiatMoneyUtil.format(context.resources, model.record.amount)
}
}
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
class DonationReceiptListPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> DonationReceiptListPageFragment.create(null)
1 -> DonationReceiptListPageFragment.create(DonationReceiptRecord.Type.RECURRING)
2 -> DonationReceiptListPageFragment.create(DonationReceiptRecord.Type.BOOST)
else -> error("Unsupported position $position")
}
}
}

View File

@@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.TextPreference
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class DonationReceiptListPageFragment : Fragment(R.layout.donation_receipt_list_page_fragment) {
private val viewModel: DonationReceiptListPageViewModel by viewModels(factoryProducer = {
DonationReceiptListPageViewModel.Factory(type, DonationReceiptListPageRepository())
})
private val sharedViewModel: DonationReceiptListViewModel by viewModels(
ownerProducer = { requireParentFragment() },
factoryProducer = {
DonationReceiptListViewModel.Factory(DonationReceiptListRepository())
}
)
private val type: DonationReceiptRecord.Type?
get() = requireArguments().getString(ARG_TYPE)?.let { DonationReceiptRecord.Type.fromCode(it) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = DonationReceiptListAdapter { model ->
findNavController().safeNavigate(DonationReceiptListFragmentDirections.actionDonationReceiptListFragmentToDonationReceiptDetailFragment(model.record.id))
}
view.findViewById<RecyclerView>(R.id.recycler).apply {
this.adapter = adapter
addItemDecoration(StickyHeaderDecoration(adapter, false, true, 0))
}
LiveDataUtil.combineLatest(
viewModel.state,
sharedViewModel.state
) { records, badges ->
records.map { DonationReceiptListItem.Model(it, getBadgeForRecord(it, badges)) }
}.observe(viewLifecycleOwner) { records ->
adapter.submitList(
records +
TextPreference(
title = null,
summary = DSLSettingsText.from(
R.string.DonationReceiptListFragment__if_you_have,
DSLSettingsText.TextAppearanceModifier(R.style.TextAppearance_Signal_Subtitle)
)
)
)
}
}
private fun getBadgeForRecord(record: DonationReceiptRecord, badges: List<DonationReceiptBadge>): Badge? {
return when (record.type) {
DonationReceiptRecord.Type.BOOST -> badges.firstOrNull { it.type == DonationReceiptRecord.Type.BOOST }?.badge
else -> badges.firstOrNull { it.level == record.subscriptionLevel }?.badge
}
}
companion object {
private const val ARG_TYPE = "arg_type"
fun create(type: DonationReceiptRecord.Type?): Fragment {
return DonationReceiptListPageFragment().apply {
arguments = Bundle().apply {
putString(ARG_TYPE, type?.code)
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
class DonationReceiptListPageRepository {
fun getRecords(type: DonationReceiptRecord.Type?): Single<List<DonationReceiptRecord>> {
return Single.fromCallable {
SignalDatabase.donationReceipts.getReceipts(type)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
class DonationReceiptListPageViewModel(type: DonationReceiptRecord.Type?, repository: DonationReceiptListPageRepository) : ViewModel() {
private val disposables = CompositeDisposable()
private val internalState = MutableLiveData<List<DonationReceiptRecord>>()
val state: LiveData<List<DonationReceiptRecord>> = internalState
init {
disposables += repository.getRecords(type)
.subscribe { records ->
internalState.postValue(records)
}
}
override fun onCleared() {
disposables.clear()
}
class Factory(private val type: DonationReceiptRecord.Type?, private val repository: DonationReceiptListPageRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.cast(DonationReceiptListPageViewModel(type, repository)) as T
}
}
}

View File

@@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.util.Locale
class DonationReceiptListRepository {
fun getBadges(): Single<List<DonationReceiptBadge>> {
val boostBadges: Single<List<DonationReceiptBadge>> = ApplicationDependencies.getDonationsService().getBoostBadge(Locale.getDefault())
.map { response ->
if (response.result.isPresent) {
listOf(DonationReceiptBadge(DonationReceiptRecord.Type.BOOST, -1, Badges.fromServiceBadge(response.result.get())))
} else {
emptyList()
}
}
val subBadges: Single<List<DonationReceiptBadge>> = ApplicationDependencies.getDonationsService().getSubscriptionLevels(Locale.getDefault())
.map { response ->
if (response.result.isPresent) {
response.result.get().levels.map {
DonationReceiptBadge(
level = it.key.toInt(),
badge = Badges.fromServiceBadge(it.value.badge),
type = DonationReceiptRecord.Type.RECURRING
)
}
} else {
emptyList()
}
}
return boostBadges.zipWith(subBadges) { a, b -> a + b }.subscribeOn(Schedulers.io())
}
}

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.util.InternetConnectionObserver
class DonationReceiptListViewModel(private val repository: DonationReceiptListRepository) : ViewModel() {
private val disposables = CompositeDisposable()
private val internalState = MutableLiveData<List<DonationReceiptBadge>>(emptyList())
private var networkDisposable: Disposable
val state: LiveData<List<DonationReceiptBadge>> = internalState
init {
networkDisposable = InternetConnectionObserver
.observe()
.distinctUntilChanged()
.subscribe { isConnected ->
if (isConnected) {
retry()
}
}
refresh()
}
private fun retry() {
if (internalState.value?.isEmpty() == true) {
refresh()
}
}
private fun refresh() {
disposables.clear()
disposables += repository.getBadges().subscribe { badges ->
internalState.postValue(badges)
}
}
override fun onCleared() {
disposables.clear()
networkDisposable.dispose()
}
class Factory(private val repository: DonationReceiptListRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.cast(DonationReceiptListViewModel(repository)) as T
}
}
}