kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement donation receipts.
rodzic
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())
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.878,2.555L5.122,2.555A2.123,2.123 0,0 0,3 4.678L3,19.555L3,19.555l2.205,2.2a1.127,1.127 0,0 0,1.591 0l1.412,-1.41a1.126,1.126 0,0 1,1.592 0l1.4,1.4a1.126,1.126 0,0 0,1.592 0l1.411,-1.408a1.125,1.125 0,0 1,1.591 0l1.4,1.4a1.126,1.126 0,0 0,1.592 0L21,19.555h0L21,4.678A2.123,2.123 0,0 0,18.878 2.555ZM7,7.5L17,7.5L17,9L7,9ZM7,10.5L17,10.5L17,12L7,12ZM7,13.5h8L15,15L7,15Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="100dp"
|
||||
android:height="30.4dp"
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="152">
|
||||
<path
|
||||
android:pathData="m60.6,13.9 l1.4,5.8c-5.6,1.4 -11,3.6 -16,6.6l-3.1,-5.1c5.5,-3.3 11.5,-5.8 17.7,-7.3zM91.4,13.9 L90,19.7c5.7,1.4 11.1,3.6 16.1,6.6l3.1,-5.1c-5.6,-3.3 -11.6,-5.8 -17.8,-7.3zM21.2,42.9c-3.3,5.5 -5.8,11.5 -7.3,17.7l5.8,1.4c1.4,-5.7 3.6,-11.1 6.6,-16.1zM18,76c0,-2.9 0.2,-5.8 0.6,-8.7l-5.9,-0.9c-1,6.4 -1,12.8 0,19.2l5.9,-0.9c-0.4,-2.9 -0.6,-5.8 -0.6,-8.7zM109.1,130.8 L106,125.7c-5,3 -10.4,5.3 -16.1,6.6l1.4,5.8c6.3,-1.5 12.3,-4 17.8,-7.3zM134,76c0,2.9 -0.2,5.8 -0.6,8.7l5.9,0.9c1,-6.4 1,-12.8 0,-19.2l-5.9,0.9c0.4,2.9 0.6,5.8 0.6,8.7zM138.1,91.4 L132.3,90c-1.4,5.7 -3.6,11.1 -6.6,16.1l5.1,3.1c3.3,-5.6 5.8,-11.6 7.3,-17.8zM84.7,133.4c-5.8,0.9 -11.6,0.9 -17.4,0l-0.9,5.9c6.4,1 12.8,1 19.2,0zM122.7,110.4c-3.5,4.7 -7.6,8.8 -12.3,12.3l3.6,4.8c5.2,-3.8 9.7,-8.4 13.6,-13.5zM110.4,29.3c4.7,3.5 8.8,7.6 12.3,12.3l4.8,-3.6c-3.8,-5.2 -8.4,-9.7 -13.5,-13.5zM29.3,41.6c3.5,-4.7 7.6,-8.8 12.3,-12.3l-3.6,-4.8c-5.2,3.8 -9.7,8.4 -13.5,13.5zM130.8,42.9 L125.7,46c3,5 5.3,10.4 6.6,16.1l5.8,-1.4c-1.5,-6.3 -4,-12.3 -7.3,-17.8zM67.3,18.6c5.8,-0.9 11.6,-0.9 17.4,0l0.9,-5.9c-6.4,-1 -12.8,-1 -19.2,0zM32.4,129.1 L20,132 22.9,119.6 17.1,118.2 14.2,130.6c-0.8,3.2 1.2,6.5 4.5,7.2 0.9,0.2 1.8,0.2 2.7,0l12.4,-2.8zM18.3,112.9 L24.1,114.3 26.1,105.7c-2.9,-4.9 -5.1,-10.2 -6.5,-15.7l-5.8,1.4c1.3,5.3 3.3,10.4 5.9,15.2zM46.3,125.9 L37.7,127.9 39.1,133.7 45.4,132.2c4.8,2.6 9.9,4.6 15.2,5.9l1.4,-5.8c-5.5,-1.3 -10.8,-3.5 -15.7,-6.4zM76,24c-28.7,0 -52,23.3 -52,52 0,9.8 2.8,19.4 8,27.6l-5,21.4 21.3,-5c24.3,15.3 56.4,8 71.7,-16.3s8,-56.4 -16.3,-71.7c-8.3,-5.2 -17.9,-8 -27.7,-8z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m194.2,49.1c-8.3,0 -12.9,3.8 -12.9,8.9 -0.1,5.7 5.7,8.3 12.6,9.9l7.2,1.7c13.9,3.1 24,10.2 24,23.5 0,14.7 -11.5,24 -31.1,24s-31.8,-8.9 -32.2,-26.2h16.4c0.6,8 6.9,12.1 15.7,12.1 8.6,0 14.1,-4 14.2,-9.8 0,-5.4 -4.9,-7.9 -13.6,-10l-8.7,-2.2c-13.5,-3.3 -21.8,-10 -21.8,-21.8 -0.1,-14.5 12.8,-24.2 30.3,-24.2 17.8,0 29.5,9.8 29.8,24.1h-16.2c-0.6,-6.4 -5.6,-10 -13.7,-10z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m233.2,39.8c0,-4.6 4.1,-8.4 9,-8.4s9,3.8 9,8.4 -4.1,8.4 -9,8.4 -9,-3.7 -9,-8.4zM233.8,56h16.6v60h-16.6z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m260.9,123.1 l15.4,-2.1c1.4,3.2 5.1,6.4 12.4,6.4s12.5,-3.2 12.5,-11v-11h-0.7c-2.2,5 -7.5,9.8 -17.1,9.8 -13.5,0 -24.3,-9.3 -24.3,-29.3 0,-20.4 11.1,-30.7 24.3,-30.7 10,0 14.9,6 17.1,10.9h0.6v-10.1h16.5v60.6c0,15 -12.2,22.8 -29.3,22.8 -16.1,0 -25.4,-7.3 -27.4,-16.3zM301.3,85.8c0,-10.5 -4.5,-17.3 -12.6,-17.3 -8.2,0 -12.6,7.2 -12.6,17.3 0,10.3 4.5,16.8 12.6,16.8 8,0.1 12.6,-6.2 12.6,-16.8z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m345.2,116h-16.6v-60h15.9v10.6h0.7c2.7,-7 9.1,-11.4 18,-11.4 12.5,0 20.7,8.6 20.7,22.6v38.2h-16.6v-35.2c0,-7.3 -4,-11.7 -10.7,-11.7s-11.3,4.5 -11.4,12.3z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m391.7,99.3c0,-13.4 10.7,-17.4 22.5,-18.5 10.4,-1 14.5,-1.5 14.5,-5.4v-0.2c0,-4.9 -3.2,-7.8 -8.9,-7.8 -6,0 -9.5,2.9 -10.7,6.9l-15.4,-1.2c2.3,-10.9 11.8,-17.8 26.1,-17.8 13.4,0 25.5,6 25.5,20.3v40.4h-15.8v-8.3h-0.5c-2.9,5.6 -8.7,9.4 -17.5,9.4 -11.4,0.1 -19.8,-6 -19.8,-17.8zM428.9,94.6v-6.4c-2,1.3 -7.9,2.2 -11.6,2.7 -5.9,0.8 -9.7,3.1 -9.7,7.8s3.7,7 8.8,7c7.3,0 12.5,-4.8 12.5,-11.1z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="m472.5,116h-16.6v-80h16.6z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,9L7,9L7,7.5L17,7.5ZM17,10.5L7,10.5L7,12L17,12ZM15,13.5L7,13.5L7,15h8ZM21,19.613 L19.323,21.287A1.862,1.862 0,0 1,18 21.836h0a1.858,1.858 0,0 1,-1.326 -0.552l-1.4,-1.4a0.377,0.377 0,0 0,-0.531 0l-1.412,1.409a1.876,1.876 0,0 1,-2.652 0l-1.4,-1.4a0.378,0.378 0,0 0,-0.532 0L7.327,21.289a1.877,1.877 0,0 1,-2.649 0L3,19.613L3,4.122A2.122,2.122 0,0 1,5.122 2L18.878,2A2.122,2.122 0,0 1,21 4.122L21,19.613ZM19.5,4.122a0.623,0.623 0,0 0,-0.622 -0.622L5.122,3.5a0.623,0.623 0,0 0,-0.622 0.622L4.5,18.991l1.237,1.237a0.38,0.38 0,0 0,0.532 0l1.412,-1.41a1.879,1.879 0,0 1,2.651 0l1.4,1.407a0.378,0.378 0,0 0,0.532 0l1.411,-1.409a1.878,1.878 0,0 1,2.651 0l1.4,1.407a0.378,0.378 0,0 0,0.532 0L19.5,18.991Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="100dp"
|
||||
android:height="30.4dp"
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="152">
|
||||
<path
|
||||
android:pathData="M60.64,13.87l1.44,5.82A57.84,57.84 0,0 0,46 26.34l-3.1,-5.14A63.76,63.76 0,0 1,60.64 13.87ZM91.36,13.87 L89.92,19.69A57.84,57.84 0,0 1,106 26.34l3.1,-5.14A63.76,63.76 0,0 0,91.36 13.87ZM21.2,42.92a63.76,63.76 0,0 0,-7.33 17.72l5.82,1.44A57.84,57.84 0,0 1,26.34 46ZM18,76a57.9,57.9 0,0 1,0.65 -8.69l-5.93,-0.9a64.23,64.23 0,0 0,0 19.18l5.93,-0.9A57.9,57.9 0,0 1,18 76ZM109.08,130.8 L105.98,125.66a57.84,57.84 0,0 1,-16.06 6.65l1.44,5.82A63.76,63.76 0,0 0,109.08 130.8ZM134,76a57.9,57.9 0,0 1,-0.65 8.69l5.93,0.9a64.23,64.23 0,0 0,0 -19.18l-5.93,0.9A57.9,57.9 0,0 1,134 76ZM138.13,91.36 L132.31,89.92A57.84,57.84 0,0 1,125.66 106l5.14,3.1A63.76,63.76 0,0 0,138.13 91.36ZM84.69,133.36a58.41,58.41 0,0 1,-17.38 0l-0.9,5.93a64.23,64.23 0,0 0,19.18 0ZM122.69,110.41a58.21,58.21 0,0 1,-12.29 12.29l3.56,4.83A64.1,64.1 0,0 0,127.52 114ZM110.4,29.31A58.21,58.21 0,0 1,122.69 41.6L127.52,38A64.1,64.1 0,0 0,114 24.48ZM29.31,41.6A58.21,58.21 0,0 1,41.6 29.31L38,24.48A64.1,64.1 0,0 0,24.48 38ZM130.8,42.92 L125.66,46a57.84,57.84 0,0 1,6.65 16.06l5.82,-1.44A63.76,63.76 0,0 0,130.8 42.92ZM67.31,18.65a58.41,58.41 0,0 1,17.38 0l0.9,-5.93a64.23,64.23 0,0 0,-19.18 0ZM32.39,129.11 L20,132l2.89,-12.39 -5.84,-1.37 -2.89,12.39a6,6 0,0 0,7.21 7.21L33.75,135ZM18.3,112.89l5.84,1.36 2,-8.59a57.75,57.75 0,0 1,-6.46 -15.74l-5.82,1.44a63.52,63.52 0,0 0,5.9 15.21ZM46.3,125.89 L37.71,127.89 39.07,133.73 45.39,132.26a63.52,63.52 0,0 0,15.21 5.9l1.44,-5.82A57.75,57.75 0,0 1,46.34 125.85ZM76,24a52,52 0,0 0,-44 79.67L27,125l21.33,-5A52,52 0,1 0,76 24Z"
|
||||
android:fillColor="#3a76f0"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M194.24,49.07c-8.28,0 -12.85,3.78 -12.85,8.94 -0.12,5.74 5.7,8.32 12.65,9.92l7.19,1.72c13.91,3.13 24,10.2 24,23.52 0,14.65 -11.53,24 -31.06,24s-31.8,-8.94 -32.23,-26.25H178.3c0.55,8 6.88,12.07 15.67,12.07 8.59,0 14.14,-4 14.18,-9.84 0,-5.39 -4.89,-7.89 -13.6,-10l-8.71,-2.19c-13.51,-3.24 -21.83,-10 -21.8,-21.8 -0.07,-14.53 12.78,-24.22 30.32,-24.22 17.81,0 29.53,9.85 29.76,24.11H207.91C207.29,52.74 202.33,49.07 194.24,49.07Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M233.18,39.85c0,-4.61 4.06,-8.4 9,-8.4s9,3.79 9,8.4 -4.06,8.43 -9,8.43S233.18,44.5 233.18,39.85ZM233.8,56h16.64v60H233.8Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M260.91,123.05 L276.3,121c1.37,3.24 5.08,6.44 12.42,6.44s12.5,-3.2 12.5,-11v-11h-0.7c-2.19,5 -7.54,9.77 -17.11,9.77 -13.52,0 -24.34,-9.3 -24.34,-29.26 0,-20.43 11.14,-30.66 24.3,-30.66 10,0 14.92,6 17.15,10.86h0.62V56h16.53V116.6c0,15 -12.19,22.78 -29.34,22.78C272.16,139.38 262.9,132.11 260.91,123.05ZM301.3,85.82c0,-10.5 -4.53,-17.34 -12.62,-17.34 -8.24,0 -12.61,7.15 -12.61,17.34 0,10.35 4.45,16.84 12.61,16.84C296.69,102.66 301.3,96.41 301.3,85.82Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M345.16,116H328.52V56h15.86V66.6h0.7c2.7,-7 9.14,-11.36 18.05,-11.36 12.5,0 20.7,8.59 20.7,22.58V116H367.19V80.78c0,-7.34 -4,-11.71 -10.66,-11.71S345.2,73.6 345.16,81.33Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M391.68,99.26c0,-13.4 10.66,-17.38 22.54,-18.48 10.42,-1 14.53,-1.52 14.53,-5.39v-0.23c0,-4.92 -3.25,-7.77 -8.91,-7.77 -6,0 -9.53,2.93 -10.66,6.91l-15.39,-1.25c2.3,-10.94 11.75,-17.81 26.13,-17.81 13.36,0 25.47,6 25.47,20.31L445.39,116L429.61,116L429.61,107.7h-0.47c-2.93,5.58 -8.67,9.45 -17.54,9.45C400.15,117.15 391.68,111.14 391.68,99.26ZM428.86,94.57L428.86,88.21c-2,1.32 -7.93,2.18 -11.56,2.69 -5.86,0.82 -9.73,3.13 -9.73,7.81s3.68,7 8.79,7C423.67,105.67 428.86,100.86 428.86,94.57Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M472.53,116H455.89V36h16.64Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="100dp"
|
||||
android:height="30.4dp"
|
||||
android:viewportWidth="500"
|
||||
android:viewportHeight="152">
|
||||
<path
|
||||
android:pathData="M60.64,13.87l1.44,5.82A57.84,57.84 0,0 0,46 26.34l-3.1,-5.14A63.76,63.76 0,0 1,60.64 13.87ZM91.36,13.87 L89.92,19.69A57.84,57.84 0,0 1,106 26.34l3.1,-5.14A63.76,63.76 0,0 0,91.36 13.87ZM21.2,42.92a63.76,63.76 0,0 0,-7.33 17.72l5.82,1.44A57.84,57.84 0,0 1,26.34 46ZM18,76a57.9,57.9 0,0 1,0.65 -8.69l-5.93,-0.9a64.23,64.23 0,0 0,0 19.18l5.93,-0.9A57.9,57.9 0,0 1,18 76ZM109.08,130.8 L105.98,125.66a57.84,57.84 0,0 1,-16.06 6.65l1.44,5.82A63.76,63.76 0,0 0,109.08 130.8ZM134,76a57.9,57.9 0,0 1,-0.65 8.69l5.93,0.9a64.23,64.23 0,0 0,0 -19.18l-5.93,0.9A57.9,57.9 0,0 1,134 76ZM138.13,91.36 L132.31,89.92A57.84,57.84 0,0 1,125.66 106l5.14,3.1A63.76,63.76 0,0 0,138.13 91.36ZM84.69,133.36a58.41,58.41 0,0 1,-17.38 0l-0.9,5.93a64.23,64.23 0,0 0,19.18 0ZM122.69,110.41a58.21,58.21 0,0 1,-12.29 12.29l3.56,4.83A64.1,64.1 0,0 0,127.52 114ZM110.4,29.31A58.21,58.21 0,0 1,122.69 41.6L127.52,38A64.1,64.1 0,0 0,114 24.48ZM29.31,41.6A58.21,58.21 0,0 1,41.6 29.31L38,24.48A64.1,64.1 0,0 0,24.48 38ZM130.8,42.92 L125.66,46a57.84,57.84 0,0 1,6.65 16.06l5.82,-1.44A63.76,63.76 0,0 0,130.8 42.92ZM67.31,18.65a58.41,58.41 0,0 1,17.38 0l0.9,-5.93a64.23,64.23 0,0 0,-19.18 0ZM32.39,129.11 L20,132l2.89,-12.39 -5.84,-1.37 -2.89,12.39a6,6 0,0 0,7.21 7.21L33.75,135ZM18.3,112.89l5.84,1.36 2,-8.59a57.75,57.75 0,0 1,-6.46 -15.74l-5.82,1.44a63.52,63.52 0,0 0,5.9 15.21ZM46.3,125.89 L37.71,127.89 39.07,133.73 45.39,132.26a63.52,63.52 0,0 0,15.21 5.9l1.44,-5.82A57.75,57.75 0,0 1,46.34 125.85ZM76,24a52,52 0,0 0,-44 79.67L27,125l21.33,-5A52,52 0,1 0,76 24Z"
|
||||
android:fillColor="#3a76f0"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M194.24,49.07c-8.28,0 -12.85,3.78 -12.85,8.94 -0.12,5.74 5.7,8.32 12.65,9.92l7.19,1.72c13.91,3.13 24,10.2 24,23.52 0,14.65 -11.53,24 -31.06,24s-31.8,-8.94 -32.23,-26.25H178.3c0.55,8 6.88,12.07 15.67,12.07 8.59,0 14.14,-4 14.18,-9.84 0,-5.39 -4.89,-7.89 -13.6,-10l-8.71,-2.19c-13.51,-3.24 -21.83,-10 -21.8,-21.8 -0.07,-14.53 12.78,-24.22 30.32,-24.22 17.81,0 29.53,9.85 29.76,24.11H207.91C207.29,52.74 202.33,49.07 194.24,49.07Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M233.18,39.85c0,-4.61 4.06,-8.4 9,-8.4s9,3.79 9,8.4 -4.06,8.43 -9,8.43S233.18,44.5 233.18,39.85ZM233.8,56h16.64v60H233.8Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M260.91,123.05 L276.3,121c1.37,3.24 5.08,6.44 12.42,6.44s12.5,-3.2 12.5,-11v-11h-0.7c-2.19,5 -7.54,9.77 -17.11,9.77 -13.52,0 -24.34,-9.3 -24.34,-29.26 0,-20.43 11.14,-30.66 24.3,-30.66 10,0 14.92,6 17.15,10.86h0.62V56h16.53V116.6c0,15 -12.19,22.78 -29.34,22.78C272.16,139.38 262.9,132.11 260.91,123.05ZM301.3,85.82c0,-10.5 -4.53,-17.34 -12.62,-17.34 -8.24,0 -12.61,7.15 -12.61,17.34 0,10.35 4.45,16.84 12.61,16.84C296.69,102.66 301.3,96.41 301.3,85.82Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M345.16,116H328.52V56h15.86V66.6h0.7c2.7,-7 9.14,-11.36 18.05,-11.36 12.5,0 20.7,8.59 20.7,22.58V116H367.19V80.78c0,-7.34 -4,-11.71 -10.66,-11.71S345.2,73.6 345.16,81.33Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M391.68,99.26c0,-13.4 10.66,-17.38 22.54,-18.48 10.42,-1 14.53,-1.52 14.53,-5.39v-0.23c0,-4.92 -3.25,-7.77 -8.91,-7.77 -6,0 -9.53,2.93 -10.66,6.91l-15.39,-1.25c2.3,-10.94 11.75,-17.81 26.13,-17.81 13.36,0 25.47,6 25.47,20.31L445.39,116L429.61,116L429.61,107.7h-0.47c-2.93,5.58 -8.67,9.45 -17.54,9.45C400.15,117.15 391.68,111.14 391.68,99.26ZM428.86,94.57L428.86,88.21c-2,1.32 -7.93,2.18 -11.56,2.69 -5.86,0.82 -9.73,3.13 -9.73,7.81s3.68,7 8.79,7C423.67,105.67 428.86,100.86 428.86,94.57Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M472.53,116H455.89V36h16.64Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@+id/share_png"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/share_png"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48sp"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/DonationReceiptDetailsFragment__share_receipt"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:backgroundTint="@color/signal_background_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_background_primary">
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||
app:title="@string/DonationReceiptListFragment__all_activity" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableTabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabIndicatorColor="@color/signal_inverse_primary"
|
||||
app:tabIndicatorFullWidth="false"
|
||||
app:tabSelectedTextColor="@color/signal_text_primary"
|
||||
app:tabTextAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:tabTextColor="@color/signal_text_secondary" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:paddingStart="18dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/badge"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:badge_size="medium"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintEnd_toStartOf="@id/money"
|
||||
app:layout_constraintStart_toEndOf="@id/badge"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Jan 5, 2022" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toStartOf="@id/money"
|
||||
app:layout_constraintStart_toEndOf="@id/badge"
|
||||
app:layout_constraintTop_toBottomOf="@id/date"
|
||||
tools:text="Recurring" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/money"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="$10" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="1916px"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/core_white"
|
||||
tools:ignore="PxUsage">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logo"
|
||||
android:layout_width="400px"
|
||||
android:layout_height="121.6px"
|
||||
android:layout_marginTop="96px"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_signal_logo_type_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/core_grey_60"
|
||||
android:textSize="52px"
|
||||
app:layout_constraintBottom_toBottomOf="@id/logo"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/logo"
|
||||
tools:text="Tues, Dec 02, 2021" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginTop="48px"
|
||||
android:background="@color/core_grey_15"
|
||||
app:layout_constraintTop_toBottomOf="@id/logo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="148px"
|
||||
android:text="@string/DonationReceiptDetailsFragment__donation_receipt"
|
||||
android:textSize="80px"
|
||||
android:textColor="@color/core_grey_90"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/amount_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="96px"
|
||||
android:text="@string/DonationReceiptDetailsFragment__amount"
|
||||
android:textSize="68px"
|
||||
android:textColor="@color/core_grey_90"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="96px"
|
||||
android:textSize="68px"
|
||||
android:textColor="@color/core_grey_90"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
tools:text="$10.00" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginTop="80px"
|
||||
android:background="@color/core_grey_90"
|
||||
app:layout_constraintTop_toBottomOf="@id/amount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/donation_type_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="88px"
|
||||
android:text="@string/DonationReceiptDetailsFragment__donation_type"
|
||||
android:textSize="68px"
|
||||
android:textColor="@color/core_grey_90"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/donation_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/core_grey_45"
|
||||
android:textSize="52px"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/donation_type_label"
|
||||
tools:text="Sustainer 2 - Recurring" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginTop="40px"
|
||||
android:background="@color/core_grey_20"
|
||||
app:layout_constraintTop_toBottomOf="@id/donation_type" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_paid_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40px"
|
||||
android:text="@string/DonationReceiptDetailsFragment__date_paid"
|
||||
android:textSize="68px"
|
||||
android:textColor="@color/core_grey_90"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider_3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_paid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/core_grey_45"
|
||||
android:textSize="52px"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_paid_label"
|
||||
tools:text="Nov 10, 2021" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/thanks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="88px"
|
||||
android:text="@string/DonationReceiptDetailsFragment__thank_you_for_supporting"
|
||||
android:textColor="@color/core_grey_60"
|
||||
android:textSize="48px"
|
||||
app:layout_constraintTop_toBottomOf="@id/date_paid" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -3,4 +3,4 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:srcCompat="@drawable/ic_card_process" />
|
||||
tools:srcCompat="@drawable/ic_card_process" />
|
||||
|
|
|
@ -100,14 +100,14 @@
|
|||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"></action>
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_subscribeFragment"
|
||||
app:destination="@id/subscribeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"></action>
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_boostsFragment"
|
||||
app:destination="@id/boosts"
|
||||
|
@ -547,6 +547,38 @@
|
|||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_donationReceiptListFragment"
|
||||
app:destination="@id/donationReceiptListFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donationReceiptListFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list.DonationReceiptListFragment"
|
||||
android:label="donation_receipt_list_fragment"
|
||||
tools:layout="@layout/donation_receipt_list_fragment">
|
||||
<action
|
||||
android:id="@+id/action_donationReceiptListFragment_to_donationReceiptDetailFragment"
|
||||
app:destination="@id/donationReceiptDetailFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donationReceiptDetailFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail.DonationReceiptDetailFragment"
|
||||
android:label="donation_receipt_detail_fragment"
|
||||
tools:layout="@layout/donation_receipt_detail_fragment">
|
||||
|
||||
<argument
|
||||
android:name="id"
|
||||
app:argType="long" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -4079,6 +4079,8 @@
|
|||
|
||||
<string name="ManageDonationsFragment__my_support">My support</string>
|
||||
<string name="ManageDonationsFragment__manage_subscription">Manage subscription</string>
|
||||
<!-- Label for Tax Receipts button -->
|
||||
<string name="ManageDonationsFragment__tax_receipts">Tax Receipts</string>
|
||||
<string name="ManageDonationsFragment__badges">Badges</string>
|
||||
<string name="ManageDonationsFragment__subscription_faq">Subscription FAQ</string>
|
||||
<string name="ManageDonationsFragment__error_getting_subscription">Error getting subscription.</string>
|
||||
|
@ -4324,6 +4326,36 @@
|
|||
<!-- Description shown for the Signal Release Notes channel -->
|
||||
<string name="ReleaseNotes__signal_release_notes_and_news">Signal Release Notes & News</string>
|
||||
|
||||
<!-- Donation receipts activity title -->
|
||||
<string name="DonationReceiptListFragment__all_activity">All activity</string>
|
||||
<!-- Donation receipts all tab label -->
|
||||
<string name="DonationReceiptListFragment__all">All</string>
|
||||
<!-- Donation receipts recurring tab label -->
|
||||
<string name="DonationReceiptListFragment__recurring">Recurring</string>
|
||||
<!-- Donation receipts one time tab label -->
|
||||
<string name="DonationReceiptListFragment__one_time">One time</string>
|
||||
<!-- Donation receipts boost row label -->
|
||||
<string name="DonationReceiptListFragment__boost">Boost</string>
|
||||
<!-- Donation receipts details title -->
|
||||
<string name="DonationReceiptDetailsFragment__details">Details</string>
|
||||
<!-- Donation receipts donation type heading -->
|
||||
<string name="DonationReceiptDetailsFragment__donation_type">Donation type</string>
|
||||
<!-- Donation receipts date paid heading -->
|
||||
<string name="DonationReceiptDetailsFragment__date_paid">Date paid</string>
|
||||
<!-- Donation receipts share PNG -->
|
||||
<string name="DonationReceiptDetailsFragment__share_receipt">Share receipt</string>
|
||||
<!-- Donation receipts list end note -->
|
||||
<string name="DonationReceiptListFragment__if_you_have">If you have reinstalled Signal, receipts from previous donations will not be available.</string>
|
||||
<!-- Donation receipts document title -->
|
||||
<string name="DonationReceiptDetailsFragment__donation_receipt">Donation receipt</string>
|
||||
<!-- Donation receipts amount title -->
|
||||
<string name="DonationReceiptDetailsFragment__amount">Amount</string>
|
||||
<!-- Donation receipts thanks -->
|
||||
<string name="DonationReceiptDetailsFragment__thank_you_for_supporting">Thank you for supporting Signal. Your contribution helps fuel the mission of developing open source privacy technology that protects free expression and enables secure global communication for millions around the world. If you’re a resident of the United States, please retain this receipt for your tax records. Signal Technology Foundation is a tax–exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82–4506840.</string>
|
||||
<!-- Donation receipt type -->
|
||||
<string name="DonationReceiptDetailsFragment__s_dash_s">%1$s - %2$s</string>
|
||||
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -65,6 +65,12 @@
|
|||
<item name="android:textColor">@color/core_white</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Text.Giant" parent="@style/TextAppearance.AppCompat.Display1">
|
||||
<item name="android:textSize">48sp</item>
|
||||
<item name="android:textColor">@color/signal_text_primary</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Title1" parent="@style/TextAppearance.AppCompat.Title">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">28sp</item>
|
||||
|
|
Ładowanie…
Reference in New Issue