mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 03:35:58 +00:00
Implement donation receipts.
This commit is contained in:
committed by
Greyson Parrelli
parent
63dab3f4b0
commit
7b499f96be
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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())))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.SqlUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
class DonationReceiptDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
|
||||
companion object {
|
||||
private const val TABLE_NAME = "donation_receipt"
|
||||
|
||||
private const val ID = "_id"
|
||||
private const val TYPE = "receipt_type"
|
||||
private const val DATE = "receipt_date"
|
||||
private const val AMOUNT = "amount"
|
||||
private const val CURRENCY = "currency"
|
||||
private const val SUBSCRIPTION_LEVEL = "subscription_level"
|
||||
|
||||
@JvmField
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$TYPE TEXT NOT NULL,
|
||||
$DATE INTEGER NOT NULL,
|
||||
$AMOUNT TEXT NOT NULL,
|
||||
$CURRENCY TEXT NOT NULL,
|
||||
$SUBSCRIPTION_LEVEL INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX IF NOT EXISTS donation_receipt_type_index ON $TABLE_NAME ($TYPE)",
|
||||
"CREATE INDEX IF NOT EXISTS donation_receipt_date_index ON $TABLE_NAME ($DATE)"
|
||||
)
|
||||
}
|
||||
|
||||
fun addReceipt(record: DonationReceiptRecord) {
|
||||
require(record.id == -1L)
|
||||
|
||||
val values = contentValuesOf(
|
||||
AMOUNT to record.amount.amount.toString(),
|
||||
CURRENCY to record.amount.currency.currencyCode,
|
||||
DATE to record.timestamp,
|
||||
TYPE to record.type.code,
|
||||
SUBSCRIPTION_LEVEL to record.subscriptionLevel
|
||||
)
|
||||
|
||||
writableDatabase.insert(TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
fun getReceipt(id: Long): DonationReceiptRecord? {
|
||||
readableDatabase.query(TABLE_NAME, null, ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToNext()) {
|
||||
readRecord(cursor)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getReceipts(type: DonationReceiptRecord.Type?): List<DonationReceiptRecord> {
|
||||
val (where, whereArgs) = if (type != null) {
|
||||
"$TYPE = ?" to SqlUtil.buildArgs(type.code)
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
|
||||
readableDatabase.query(TABLE_NAME, null, where, whereArgs, null, null, "$DATE DESC").use { cursor ->
|
||||
val results = ArrayList<DonationReceiptRecord>(cursor.count)
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(readRecord(cursor))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRecord(cursor: Cursor): DonationReceiptRecord {
|
||||
return DonationReceiptRecord(
|
||||
id = CursorUtil.requireLong(cursor, ID),
|
||||
type = DonationReceiptRecord.Type.fromCode(CursorUtil.requireString(cursor, TYPE)),
|
||||
amount = FiatMoney(
|
||||
BigDecimal(CursorUtil.requireString(cursor, AMOUNT)),
|
||||
Currency.getInstance(CursorUtil.requireString(cursor, CURRENCY))
|
||||
),
|
||||
timestamp = CursorUtil.requireLong(cursor, DATE),
|
||||
subscriptionLevel = CursorUtil.requireInt(cursor, SUBSCRIPTION_LEVEL)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val groupCallRingDatabase: GroupCallRingDatabase = GroupCallRingDatabase(context, this)
|
||||
val reactionDatabase: ReactionDatabase = ReactionDatabase(context, this)
|
||||
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
|
||||
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.enableWriteAheadLogging()
|
||||
@@ -103,6 +104,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
db.execSQL(AvatarPickerDatabase.CREATE_TABLE)
|
||||
db.execSQL(GroupCallRingDatabase.CREATE_TABLE)
|
||||
db.execSQL(ReactionDatabase.CREATE_TABLE)
|
||||
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
||||
@@ -123,6 +125,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS)
|
||||
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||
@@ -466,5 +469,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
@get:JvmName("notificationProfiles")
|
||||
val notificationProfiles: NotificationProfileDatabase
|
||||
get() = instance!!.notificationProfileDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("donationReceipts")
|
||||
val donationReceipts: DonationReceiptDatabase
|
||||
get() = instance!!.donationReceiptDatabase
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +48,6 @@ import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.AssertionError
|
||||
import java.util.ArrayList
|
||||
import java.util.HashSet
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
||||
@@ -190,8 +187,9 @@ object SignalDatabaseMigrations {
|
||||
private const val MESSAGE_RANGES = 128
|
||||
private const val REACTION_TRIGGER_FIX = 129
|
||||
private const val PNI_STORES = 130
|
||||
private const val DONATION_RECEIPTS = 131
|
||||
|
||||
const val DATABASE_VERSION = 130
|
||||
const val DATABASE_VERSION = 131
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2397,6 +2395,25 @@ object SignalDatabaseMigrations {
|
||||
db.execSQL("DROP TABLE sessions")
|
||||
db.execSQL("ALTER TABLE sessions_tmp RENAME TO sessions")
|
||||
}
|
||||
|
||||
if (oldVersion < DONATION_RECEIPTS) {
|
||||
db.execSQL(
|
||||
// language=sql
|
||||
"""
|
||||
CREATE TABLE donation_receipt (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
receipt_type TEXT NOT NULL,
|
||||
receipt_date INTEGER NOT NULL,
|
||||
amount TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
subscription_level INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS donation_receipt_type_index ON donation_receipt (receipt_type);")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS donation_receipt_date_index ON donation_receipt (receipt_date);")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Currency
|
||||
|
||||
data class DonationReceiptRecord(
|
||||
val id: Long = -1L,
|
||||
val amount: FiatMoney,
|
||||
val timestamp: Long,
|
||||
val type: Type,
|
||||
val subscriptionLevel: Int
|
||||
) {
|
||||
enum class Type(val code: String) {
|
||||
RECURRING("recurring"),
|
||||
BOOST("boost");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): Type {
|
||||
return values().first { it.code == code }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createForSubscription(subscription: ActiveSubscription.Subscription): DonationReceiptRecord {
|
||||
val activeCurrency = Currency.getInstance(subscription.currency)
|
||||
val activeAmount = subscription.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
return DonationReceiptRecord(
|
||||
id = -1L,
|
||||
amount = FiatMoney(activeAmount, activeCurrency),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
subscriptionLevel = subscription.level,
|
||||
type = Type.RECURRING
|
||||
)
|
||||
}
|
||||
|
||||
fun createForBoost(amount: FiatMoney): DonationReceiptRecord {
|
||||
return DonationReceiptRecord(
|
||||
id = -1L,
|
||||
amount = amount,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
subscriptionLevel = -1,
|
||||
type = Type.BOOST
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import org.signal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
import org.signal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
@@ -168,7 +170,9 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
throw new IOException("Could not validate receipt credential");
|
||||
}
|
||||
|
||||
Log.d(TAG, "Validated credential. Handing off to redemption job.", true);
|
||||
Log.d(TAG, "Validated credential. Recording receipt and handing off to redemption job.", true);
|
||||
SignalDatabase.donationReceipts().addReceipt(DonationReceiptRecord.createForSubscription(subscription));
|
||||
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential);
|
||||
setOutputData(new Data.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION,
|
||||
receiptCredentialPresentation.serialize())
|
||||
|
||||
Reference in New Issue
Block a user