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

View File

@@ -6,7 +6,7 @@ import androidx.core.content.contentValuesOf
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import java.math.BigDecimal
import java.util.Currency
@@ -46,7 +46,7 @@ class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : D
}
}
fun addReceipt(record: DonationReceiptRecord) {
fun addReceipt(record: InAppPaymentReceiptRecord) {
require(record.id == -1L)
val values = contentValuesOf(
@@ -60,7 +60,7 @@ class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : D
writableDatabase.insert(TABLE_NAME, null, values)
}
fun getReceipt(id: Long): DonationReceiptRecord? {
fun getReceipt(id: Long): InAppPaymentReceiptRecord? {
readableDatabase.query(TABLE_NAME, null, ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
return if (cursor.moveToNext()) {
readRecord(cursor)
@@ -70,15 +70,15 @@ class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : D
}
}
fun getReceipts(type: DonationReceiptRecord.Type?): List<DonationReceiptRecord> {
fun getReceipts(type: InAppPaymentReceiptRecord.Type?): List<InAppPaymentReceiptRecord> {
val (where, whereArgs) = if (type != null) {
"$TYPE = ?" to SqlUtil.buildArgs(type.code)
} else {
"$TYPE != ?" to SqlUtil.buildArgs(DonationReceiptRecord.Type.RECURRING_DONATION)
"$TYPE != ?" to SqlUtil.buildArgs(InAppPaymentReceiptRecord.Type.RECURRING_DONATION)
}
readableDatabase.query(TABLE_NAME, null, where, whereArgs, null, null, "$DATE DESC").use { cursor ->
val results = ArrayList<DonationReceiptRecord>(cursor.count)
val results = ArrayList<InAppPaymentReceiptRecord>(cursor.count)
while (cursor.moveToNext()) {
results.add(readRecord(cursor))
}
@@ -87,10 +87,10 @@ class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : D
}
}
private fun readRecord(cursor: Cursor): DonationReceiptRecord {
return DonationReceiptRecord(
private fun readRecord(cursor: Cursor): InAppPaymentReceiptRecord {
return InAppPaymentReceiptRecord(
id = CursorUtil.requireLong(cursor, ID),
type = DonationReceiptRecord.Type.fromCode(CursorUtil.requireString(cursor, TYPE)),
type = InAppPaymentReceiptRecord.Type.fromCode(CursorUtil.requireString(cursor, TYPE)),
amount = FiatMoney(
BigDecimal(CursorUtil.requireString(cursor, AMOUNT)),
Currency.getInstance(CursorUtil.requireString(cursor, CURRENCY))

View File

@@ -5,7 +5,7 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Currency
data class DonationReceiptRecord(
data class InAppPaymentReceiptRecord(
val id: Long = -1L,
val amount: FiatMoney,
val timestamp: Long,
@@ -20,18 +20,18 @@ data class DonationReceiptRecord(
companion object {
fun fromCode(code: String): Type {
return values().first { it.code == code }
return entries.first { it.code == code }
}
}
}
companion object {
@JvmStatic
fun createForSubscription(subscription: ActiveSubscription.Subscription): DonationReceiptRecord {
fun createForSubscription(subscription: ActiveSubscription.Subscription): InAppPaymentReceiptRecord {
val activeCurrency = Currency.getInstance(subscription.currency)
val activeAmount = subscription.amount.movePointLeft(activeCurrency.defaultFractionDigits)
return DonationReceiptRecord(
return InAppPaymentReceiptRecord(
id = -1L,
amount = FiatMoney(activeAmount, activeCurrency),
timestamp = System.currentTimeMillis(),
@@ -40,8 +40,8 @@ data class DonationReceiptRecord(
)
}
fun createForBoost(amount: FiatMoney): DonationReceiptRecord {
return DonationReceiptRecord(
fun createForBoost(amount: FiatMoney): InAppPaymentReceiptRecord {
return InAppPaymentReceiptRecord(
id = -1L,
amount = amount,
timestamp = System.currentTimeMillis(),
@@ -50,8 +50,8 @@ data class DonationReceiptRecord(
)
}
fun createForGift(amount: FiatMoney): DonationReceiptRecord {
return DonationReceiptRecord(
fun createForGift(amount: FiatMoney): InAppPaymentReceiptRecord {
return InAppPaymentReceiptRecord(
id = -1L,
amount = amount,
timestamp = System.currentTimeMillis(),

View File

@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.st
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.Companion.toDonationErrorValue
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.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
@@ -114,13 +114,13 @@ class ExternalLaunchDonationJob private constructor(
checkIntentStatus(stripePaymentIntent.status)
Log.i(TAG, "Creating and inserting donation receipt record.", true)
val donationReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentType.ONE_TIME_DONATION) {
DonationReceiptRecord.createForBoost(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
val inAppPaymentReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentType.ONE_TIME_DONATION) {
InAppPaymentReceiptRecord.createForBoost(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
} else {
DonationReceiptRecord.createForGift(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
InAppPaymentReceiptRecord.createForGift(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
}
SignalDatabase.donationReceipts.addReceipt(donationReceiptRecord)
SignalDatabase.donationReceipts.addReceipt(inAppPaymentReceiptRecord)
Log.i(TAG, "Creating and inserting one-time pending donation.", true)
SignalStore.inAppPayments.setPendingOneTimeDonation(

View File

@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.database.InAppPaymentTable
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.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -141,8 +141,8 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
Log.i(TAG, "Creating and inserting receipt.", true)
val receipt = when (inAppPayment.type) {
InAppPaymentType.ONE_TIME_DONATION -> DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
InAppPaymentType.ONE_TIME_GIFT -> DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
InAppPaymentType.ONE_TIME_DONATION -> InAppPaymentReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
InAppPaymentType.ONE_TIME_GIFT -> InAppPaymentReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
else -> {
Log.e(TAG, "Unexpected type ${inAppPayment.type}", true)
return CheckResult.Failure()

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
import org.thoughtcrime.securesms.database.InAppPaymentTable
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.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -456,7 +456,7 @@ class InAppPaymentRecurringContextJob private constructor(
}
info("Validated credential. Recording receipt and handing off to redemption job.")
SignalDatabase.donationReceipts.addReceipt(DonationReceiptRecord.createForSubscription(subscription))
SignalDatabase.donationReceipts.addReceipt(InAppPaymentReceiptRecord.createForSubscription(subscription))
SignalDatabase.inAppPayments.update(
inAppPayment = inAppPayment.copy(
data = inAppPayment.data.copy(

View File

@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource;
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode;
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.InAppPaymentSubscriberRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue;
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue;
@@ -233,7 +233,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential);
Log.d(TAG, "Validated credential. Recording receipt and handing off to redemption job.", true);
SignalDatabase.donationReceipts().addReceipt(DonationReceiptRecord.createForSubscription(subscription));
SignalDatabase.donationReceipts().addReceipt(InAppPaymentReceiptRecord.createForSubscription(subscription));
SignalStore.inAppPayments().clearSubscriptionRequestCredential();
SignalStore.inAppPayments().setSubscriptionReceiptCredential(receiptCredentialPresentation);