Implement donation receipts.

fork-5.53.8
Alex Hart 2022-02-22 13:41:36 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 63dab3f4b0
commit 7b499f96be
35 zmienionych plików z 1286 dodań i 11 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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())
}
)
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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())))
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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")
}
}
}

Wyświetl plik

@ -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)
}
}
}
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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)
)
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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
)
}
}
}

Wyświetl plik

@ -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())

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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

Wyświetl plik

@ -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 &amp; 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 youre 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>

Wyświetl plik

@ -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>