Implement backup receipt generation.

This commit is contained in:
Alex Hart
2024-07-22 16:42:36 -03:00
committed by Nicholas Tinsley
parent 82c3265be5
commit 816c9360cd
24 changed files with 259 additions and 175 deletions

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
import android.content.Intent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -37,6 +38,7 @@ import androidx.navigation.navArgument
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
@@ -45,9 +47,10 @@ import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Texts
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.receipts.ReceiptImageRenderer
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.Nav
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import java.math.BigDecimal
@@ -97,18 +100,41 @@ class RemoteBackupsPaymentHistoryFragment : ComposeFragment() {
PaymentHistoryDetails(
record = record,
onNavigationClick = onNavigationClick,
onShareClick = {} // TODO [message-backups] Generate shareable png
onShareClick = this@RemoteBackupsPaymentHistoryFragment::onShareClick
)
if (state.displayProgressDialog) {
Dialogs.IndeterminateProgressDialog()
}
}
}
}
private fun onShareClick(record: InAppPaymentReceiptRecord) {
viewModel.onStartRenderingBitmap()
ReceiptImageRenderer.renderPng(
requireContext(),
viewLifecycleOwner,
record,
getString(R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup),
object : ReceiptImageRenderer.Callback {
override fun onBitmapRendered() {
viewModel.onEndRenderingBitmap()
}
override fun onStartActivity(intent: Intent) {
startActivity(intent)
}
}
)
}
}
@Composable
private fun PaymentHistoryContent(
state: RemoteBackupsPaymentHistoryState,
onNavigationClick: () -> Unit,
onRecordClick: (DonationReceiptRecord) -> Unit
onRecordClick: (InAppPaymentReceiptRecord) -> Unit
) {
Scaffolds.Settings(
title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_history),
@@ -158,8 +184,8 @@ private fun rememberYear(timestamp: Long): Int {
@Composable
private fun PaymentHistoryRow(
record: DonationReceiptRecord,
onRecordClick: (DonationReceiptRecord) -> Unit
record: InAppPaymentReceiptRecord,
onRecordClick: (InAppPaymentReceiptRecord) -> Unit
) {
val date = remember(record.timestamp) {
DateUtils.formatDateWithYear(Locale.getDefault(), record.timestamp)
@@ -196,9 +222,9 @@ private fun PaymentHistoryRow(
@Composable
private fun PaymentHistoryDetails(
record: DonationReceiptRecord,
record: InAppPaymentReceiptRecord,
onNavigationClick: () -> Unit,
onShareClick: () -> Unit
onShareClick: (InAppPaymentReceiptRecord) -> Unit
) {
Scaffolds.Settings(
title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_details),
@@ -249,7 +275,7 @@ private fun PaymentHistoryDetails(
Spacer(modifier = Modifier.weight(1f))
Buttons.LargePrimary(
onClick = onShareClick,
onClick = { onShareClick(record) },
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 24.dp)
@@ -300,12 +326,12 @@ private fun PaymentDetailsContentPreview() {
}
}
private fun testRecord(): DonationReceiptRecord {
return DonationReceiptRecord(
private fun testRecord(): InAppPaymentReceiptRecord {
return InAppPaymentReceiptRecord(
id = 1,
amount = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
timestamp = 1718739691000,
type = DonationReceiptRecord.Type.RECURRING_BACKUP,
type = InAppPaymentReceiptRecord.Type.RECURRING_BACKUP,
subscriptionLevel = 201
)
}

View File

@@ -6,11 +6,11 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
object RemoteBackupsPaymentHistoryRepository {
fun getReceipts(): List<DonationReceiptRecord> {
return SignalDatabase.donationReceipts.getReceipts(DonationReceiptRecord.Type.RECURRING_BACKUP)
fun getReceipts(): List<InAppPaymentReceiptRecord> {
return SignalDatabase.donationReceipts.getReceipts(InAppPaymentReceiptRecord.Type.RECURRING_BACKUP)
}
}

View File

@@ -8,9 +8,10 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
@Stable
data class RemoteBackupsPaymentHistoryState(
val records: PersistentMap<Long, DonationReceiptRecord> = persistentMapOf()
val records: PersistentMap<Long, InAppPaymentReceiptRecord> = persistentMapOf(),
val displayProgressDialog: Boolean = false
)

View File

@@ -29,4 +29,12 @@ class RemoteBackupsPaymentHistoryViewModel : ViewModel() {
internalStateFlow.update { state -> state.copy(records = receipts.associateBy { it.id }.toPersistentMap()) }
}
}
fun onStartRenderingBitmap() {
internalStateFlow.update { it.copy(displayProgressDialog = true) }
}
fun onEndRenderingBitmap() {
internalStateFlow.update { it.copy(displayProgressDialog = false) }
}
}

View File

@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.InAppPaymentOneTimeContextJob
@@ -106,16 +106,16 @@ class OneTimeInAppPaymentRepository(private val donationsService: DonationsServi
}
return Single.fromCallable {
val donationReceiptRecord = if (isBoost) {
DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
val inAppPaymentReceiptRecord = if (isBoost) {
InAppPaymentReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
} else {
DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
InAppPaymentReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
}
val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() }
val donationTypeLabel = inAppPaymentReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() }
Log.d(TAG, "Confirmed payment intent. Recording $donationTypeLabel receipt and submitting badge reimbursement job chain.", true)
SignalDatabase.donationReceipts.addReceipt(donationReceiptRecord)
SignalDatabase.donationReceipts.addReceipt(inAppPaymentReceiptRecord)
SignalDatabase.inAppPayments.update(
inAppPayment = inAppPayment.copy(

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts
import android.content.ActivityNotFoundException
import android.content.Context
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.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils
import java.io.ByteArrayOutputStream
import java.util.Locale
/**
* Generates a receipt PNG for an in-app payment.
*/
object ReceiptImageRenderer {
private const val DONATION_RECEIPT_WIDTH = 1916
private val TAG = Log.tag(ReceiptImageRenderer::class.java)
fun renderPng(
context: Context,
lifecycleOwner: LifecycleOwner,
record: InAppPaymentReceiptRecord,
subscriptionName: String,
callback: Callback
) {
val today: String = DateUtils.formatDateWithDayOfWeek(Locale.getDefault(), System.currentTimeMillis())
val amount: String = FiatMoneyUtil.format(context.resources, record.amount)
val type: String = when (record.type) {
InAppPaymentReceiptRecord.Type.RECURRING_DONATION, InAppPaymentReceiptRecord.Type.RECURRING_BACKUP -> context.getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, context.getString(R.string.DonationReceiptListFragment__recurring))
InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION -> context.getString(R.string.DonationReceiptListFragment__one_time)
InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT -> context.getString(R.string.DonationReceiptListFragment__donation_for_a_friend)
}
val datePaid: String = DateUtils.formatDate(Locale.getDefault(), record.timestamp)
lifecycleOwner.lifecycleScope.launch {
val bitmapUri: Uri = withContext(Dispatchers.Default) {
val outputStream = ByteArrayOutputStream()
val view = LayoutInflater
.from(context)
.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()
}
withContext(Dispatchers.Main) {
callback.onBitmapRendered()
openShareSheet(context, bitmapUri, callback)
}
}
}
private fun openShareSheet(context: Context, uri: Uri, callback: Callback) {
val mimeType = Intent.normalizeMimeType("image/png")
val shareIntent = ShareCompat.IntentBuilder(context)
.setStream(uri)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
callback.onStartActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No activity existed to share the media.", e)
Toast.makeText(context, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
}
}
interface Callback {
fun onBitmapRendered()
fun onStartActivity(intent: Intent)
}
}

View File

@@ -1,32 +1,20 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
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.concurrent.SimpleTask
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.SignalProgressDialog
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.receipts.ReceiptImageRenderer
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.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import java.io.ByteArrayOutputStream
import java.util.Locale
class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.donation_receipt_detail_fragment) {
@@ -49,77 +37,35 @@ class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.do
sharePngButton.isEnabled = false
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.donationReceiptRecord != null) {
adapter.submitList(getConfiguration(state.donationReceiptRecord, state.subscriptionName).toMappingModelList())
if (state.inAppPaymentReceiptRecord != null) {
adapter.submitList(getConfiguration(state.inAppPaymentReceiptRecord, state.subscriptionName).toMappingModelList())
}
if (state.donationReceiptRecord != null && state.subscriptionName != null) {
if (state.inAppPaymentReceiptRecord != null && state.subscriptionName != null) {
sharePngButton.isEnabled = true
sharePngButton.setOnClickListener {
renderPng(state.donationReceiptRecord, state.subscriptionName)
progressDialog = SignalProgressDialog.show(requireContext())
ReceiptImageRenderer.renderPng(
context = requireContext(),
lifecycleOwner = viewLifecycleOwner,
record = state.inAppPaymentReceiptRecord,
subscriptionName = state.subscriptionName,
callback = object : ReceiptImageRenderer.Callback {
override fun onBitmapRendered() {
progressDialog.dismiss()
}
override fun onStartActivity(intent: Intent) {
startActivity(intent)
}
}
)
}
}
}
}
private fun renderPng(record: DonationReceiptRecord, subscriptionName: String) {
progressDialog = SignalProgressDialog.show(requireContext())
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_DONATION -> getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, getString(R.string.DonationReceiptListFragment__recurring))
DonationReceiptRecord.Type.ONE_TIME_DONATION -> getString(R.string.DonationReceiptListFragment__one_time)
DonationReceiptRecord.Type.ONE_TIME_GIFT -> getString(R.string.DonationReceiptListFragment__donation_for_a_friend)
DonationReceiptRecord.Type.RECURRING_BACKUP -> error("Not supported in this fragment")
}
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 {
private fun getConfiguration(record: InAppPaymentReceiptRecord, subscriptionName: String?): DSLConfiguration {
return configure {
customPref(
SplashImage.Model(
@@ -141,10 +87,10 @@ class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.do
title = DSLSettingsText.from(R.string.DonationReceiptDetailsFragment__donation_type),
summary = DSLSettingsText.from(
when (record.type) {
DonationReceiptRecord.Type.RECURRING_DONATION -> getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, getString(R.string.DonationReceiptListFragment__recurring))
DonationReceiptRecord.Type.ONE_TIME_DONATION -> getString(R.string.DonationReceiptListFragment__one_time)
DonationReceiptRecord.Type.ONE_TIME_GIFT -> getString(R.string.DonationReceiptListFragment__donation_for_a_friend)
DonationReceiptRecord.Type.RECURRING_BACKUP -> error("Not supported in this fragment.")
InAppPaymentReceiptRecord.Type.RECURRING_DONATION -> getString(R.string.DonationReceiptDetailsFragment__s_dash_s, subscriptionName, getString(R.string.DonationReceiptListFragment__recurring))
InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION -> getString(R.string.DonationReceiptListFragment__one_time)
InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT -> getString(R.string.DonationReceiptListFragment__donation_for_a_friend)
InAppPaymentReceiptRecord.Type.RECURRING_BACKUP -> error("Not supported in this fragment.")
}
)
)
@@ -155,10 +101,4 @@ class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.do
)
}
}
companion object {
private const val DONATION_RECEIPT_WIDTH = 1916
private val TAG = Log.tag(DonationReceiptDetailFragment::class.java)
}
}

View File

@@ -4,7 +4,7 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.util.Locale
@@ -22,8 +22,8 @@ class DonationReceiptDetailRepository {
.subscribeOn(Schedulers.io())
}
fun getDonationReceiptRecord(id: Long): Single<DonationReceiptRecord> {
return Single.fromCallable<DonationReceiptRecord> {
fun getDonationReceiptRecord(id: Long): Single<InAppPaymentReceiptRecord> {
return Single.fromCallable<InAppPaymentReceiptRecord> {
SignalDatabase.donationReceipts.getReceipt(id)!!
}.subscribeOn(Schedulers.io())
}

View File

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

View File

@@ -7,7 +7,7 @@ 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.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.livedata.Store
@@ -16,7 +16,7 @@ class DonationReceiptDetailViewModel(id: Long, private val repository: DonationR
private val store = Store(DonationReceiptDetailState())
private val disposables = CompositeDisposable()
private var networkDisposable: Disposable
private val cachedRecord: Single<DonationReceiptRecord> = repository.getDonationReceiptRecord(id).cache()
private val cachedRecord: Single<InAppPaymentReceiptRecord> = repository.getDonationReceiptRecord(id).cache()
val state: LiveData<DonationReceiptDetailState> = store.stateLiveData
@@ -43,7 +43,7 @@ class DonationReceiptDetailViewModel(id: Long, private val repository: DonationR
disposables.clear()
disposables += cachedRecord.subscribe { record ->
store.update { it.copy(donationReceiptRecord = record) }
store.update { it.copy(inAppPaymentReceiptRecord = record) }
}
disposables += cachedRecord.flatMap {

View File

@@ -1,10 +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
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
data class DonationReceiptBadge(
val type: DonationReceiptRecord.Type,
val type: InAppPaymentReceiptRecord.Type,
val level: Int,
val badge: Badge
)

View File

@@ -5,7 +5,7 @@ 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.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
@@ -21,7 +21,7 @@ object DonationReceiptListItem {
}
class Model(
val record: DonationReceiptRecord,
val record: InAppPaymentReceiptRecord,
val badge: Badge?
) : MappingModel<Model> {
override fun areContentsTheSame(newItem: Model): Boolean = record == newItem.record && badge == newItem.badge
@@ -42,10 +42,10 @@ object DonationReceiptListItem {
dateView.text = DateUtils.formatDate(Locale.getDefault(), model.record.timestamp)
typeView.setText(
when (model.record.type) {
DonationReceiptRecord.Type.RECURRING_DONATION -> R.string.DonationReceiptListFragment__recurring
DonationReceiptRecord.Type.ONE_TIME_DONATION -> R.string.DonationReceiptListFragment__one_time
DonationReceiptRecord.Type.ONE_TIME_GIFT -> R.string.DonationReceiptListFragment__donation_for_a_friend
DonationReceiptRecord.Type.RECURRING_BACKUP -> error("Not supported in this fragment")
InAppPaymentReceiptRecord.Type.RECURRING_DONATION -> R.string.DonationReceiptListFragment__recurring
InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION -> R.string.DonationReceiptListFragment__one_time
InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT -> R.string.DonationReceiptListFragment__donation_for_a_friend
InAppPaymentReceiptRecord.Type.RECURRING_BACKUP -> error("Not supported in this fragment")
}
)
moneyView.text = FiatMoneyUtil.format(context.resources, model.record.amount)

View File

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

View File

@@ -11,7 +11,7 @@ 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.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -30,8 +30,8 @@ class DonationReceiptListPageFragment : Fragment(R.layout.donation_receipt_list_
}
)
private val type: DonationReceiptRecord.Type?
get() = requireArguments().getString(ARG_TYPE)?.let { DonationReceiptRecord.Type.fromCode(it) }
private val type: InAppPaymentReceiptRecord.Type?
get() = requireArguments().getString(ARG_TYPE)?.let { InAppPaymentReceiptRecord.Type.fromCode(it) }
private lateinit var emptyStateGroup: Group
@@ -71,10 +71,10 @@ class DonationReceiptListPageFragment : Fragment(R.layout.donation_receipt_list_
}
}
private fun getBadgeForRecord(record: DonationReceiptRecord, badges: List<DonationReceiptBadge>): Badge? {
private fun getBadgeForRecord(record: InAppPaymentReceiptRecord, badges: List<DonationReceiptBadge>): Badge? {
return when (record.type) {
DonationReceiptRecord.Type.ONE_TIME_DONATION -> badges.firstOrNull { it.type == DonationReceiptRecord.Type.ONE_TIME_DONATION }?.badge
DonationReceiptRecord.Type.ONE_TIME_GIFT -> badges.firstOrNull { it.type == DonationReceiptRecord.Type.ONE_TIME_GIFT }?.badge
InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION -> badges.firstOrNull { it.type == InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION }?.badge
InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT -> badges.firstOrNull { it.type == InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT }?.badge
else -> badges.firstOrNull { it.level == record.subscriptionLevel }?.badge
}
}
@@ -83,7 +83,7 @@ class DonationReceiptListPageFragment : Fragment(R.layout.donation_receipt_list_
private const val ARG_TYPE = "arg_type"
fun create(type: DonationReceiptRecord.Type?): Fragment {
fun create(type: InAppPaymentReceiptRecord.Type?): Fragment {
return DonationReceiptListPageFragment().apply {
arguments = Bundle().apply {
putString(ARG_TYPE, type?.code)

View File

@@ -3,10 +3,10 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.receipts
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.database.model.InAppPaymentReceiptRecord
class DonationReceiptListPageRepository {
fun getRecords(type: DonationReceiptRecord.Type?): Single<List<DonationReceiptRecord>> {
fun getRecords(type: InAppPaymentReceiptRecord.Type?): Single<List<InAppPaymentReceiptRecord>> {
return Single.fromCallable {
SignalDatabase.donationReceipts.getReceipts(type)
}.subscribeOn(Schedulers.io())

View File

@@ -1,8 +1,8 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
data class DonationReceiptListPageState(
val records: List<DonationReceiptRecord> = emptyList(),
val records: List<InAppPaymentReceiptRecord> = emptyList(),
val isLoaded: Boolean = false
)

View File

@@ -5,10 +5,10 @@ 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
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.util.livedata.Store
class DonationReceiptListPageViewModel(type: DonationReceiptRecord.Type?, repository: DonationReceiptListPageRepository) : ViewModel() {
class DonationReceiptListPageViewModel(type: InAppPaymentReceiptRecord.Type?, repository: DonationReceiptListPageRepository) : ViewModel() {
private val disposables = CompositeDisposable()
private val store = Store(DonationReceiptListPageState())
@@ -31,7 +31,7 @@ class DonationReceiptListPageViewModel(type: DonationReceiptRecord.Type?, reposi
disposables.clear()
}
class Factory(private val type: DonationReceiptRecord.Type?, private val repository: DonationReceiptListPageRepository) : ViewModelProvider.Factory {
class Factory(private val type: InAppPaymentReceiptRecord.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

@@ -5,7 +5,7 @@ import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.components.settings.app.subscription.getBoostBadges
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.util.Locale
@@ -17,13 +17,13 @@ class DonationReceiptListRepository {
}.map { response ->
if (response.result.isPresent) {
val config = response.result.get()
val boostBadge = DonationReceiptBadge(DonationReceiptRecord.Type.ONE_TIME_DONATION, -1, config.getBoostBadges().first())
val giftBadge = DonationReceiptBadge(DonationReceiptRecord.Type.ONE_TIME_GIFT, -1, config.getGiftBadges().first())
val boostBadge = DonationReceiptBadge(InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION, -1, config.getBoostBadges().first())
val giftBadge = DonationReceiptBadge(InAppPaymentReceiptRecord.Type.ONE_TIME_GIFT, -1, config.getGiftBadges().first())
val subBadges = config.getSubscriptionLevels().map {
DonationReceiptBadge(
level = it.key,
badge = Badges.fromServiceBadge(it.value.badge),
type = DonationReceiptRecord.Type.RECURRING_DONATION
type = InAppPaymentReceiptRecord.Type.RECURRING_DONATION
)
}
subBadges + boostBadge + giftBadge