mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 10:46:50 +00:00
Convert InternalDonorErrorConfigurationFragment to Compose.
This commit is contained in:
@@ -1,65 +1,238 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
class InternalDonorErrorConfigurationFragment : DSLSettingsFragment() {
|
||||
/**
|
||||
* Internal tool for configuring donor error states for testing.
|
||||
*/
|
||||
class InternalDonorErrorConfigurationFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: InternalDonorErrorConfigurationViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
InternalDonorErrorConfigurationScreen(
|
||||
state = state,
|
||||
callback = remember { DefaultInternalDonorErrorConfigurationCallback() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: InternalDonorErrorConfigurationState): DSLConfiguration {
|
||||
return configure {
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from("Expired Badge"),
|
||||
selected = state.badges.indexOf(state.selectedBadge),
|
||||
listItems = state.badges.map { it.name }.toTypedArray(),
|
||||
onSelected = { viewModel.setSelectedBadge(it) }
|
||||
)
|
||||
/**
|
||||
* Default callback that bridges UI interactions to ViewModel updates.
|
||||
*/
|
||||
inner class DefaultInternalDonorErrorConfigurationCallback : InternalDonorErrorConfigurationScreenCallback {
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from("Cancellation Reason"),
|
||||
selected = UnexpectedSubscriptionCancellation.entries.indexOf(state.selectedUnexpectedSubscriptionCancellation),
|
||||
listItems = UnexpectedSubscriptionCancellation.entries.map { it.status }.toTypedArray(),
|
||||
onSelected = { viewModel.setSelectedUnexpectedSubscriptionCancellation(it) },
|
||||
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
|
||||
)
|
||||
override fun onSelectedBadgeChanged(badgeId: String) {
|
||||
val index = viewModel.state.value.badges.indexOfFirst { it.id == badgeId }
|
||||
if (index >= 0) {
|
||||
viewModel.setSelectedBadge(index)
|
||||
}
|
||||
}
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from("Charge Failure"),
|
||||
selected = StripeDeclineCode.Code.entries.indexOf(state.selectedStripeDeclineCode),
|
||||
listItems = StripeDeclineCode.Code.entries.map { it.code }.toTypedArray(),
|
||||
onSelected = { viewModel.setStripeDeclineCode(it) },
|
||||
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
|
||||
)
|
||||
override fun onSelectedCancellationChanged(status: String) {
|
||||
val index = UnexpectedSubscriptionCancellation.entries.indexOfFirst { it.status == status }
|
||||
if (index >= 0) {
|
||||
viewModel.setSelectedUnexpectedSubscriptionCancellation(index)
|
||||
}
|
||||
}
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from("Save and Finish"),
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.save().subscribe { requireActivity().finish() }
|
||||
}
|
||||
)
|
||||
override fun onSelectedDeclineCodeChanged(code: String) {
|
||||
val index = StripeDeclineCode.Code.entries.indexOfFirst { it.code == code }
|
||||
if (index >= 0) {
|
||||
viewModel.setStripeDeclineCode(index)
|
||||
}
|
||||
}
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from("Clear"),
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.clearErrorState().subscribe()
|
||||
}
|
||||
)
|
||||
override fun onSaveClick() {
|
||||
viewModel.save().subscribe { requireActivity().finish() }
|
||||
}
|
||||
|
||||
override fun onClearClick() {
|
||||
viewModel.clearErrorState().subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen for configuring donor error states.
|
||||
*/
|
||||
@Composable
|
||||
fun InternalDonorErrorConfigurationScreen(
|
||||
state: InternalDonorErrorConfigurationState,
|
||||
callback: InternalDonorErrorConfigurationScreenCallback
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "Donor Error Configuration",
|
||||
onNavigationClick = callback::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val badgeLabels = state.badges.map { it.name }.toTypedArray()
|
||||
val badgeValues = state.badges.map { it.id }.toTypedArray()
|
||||
val selectedBadgeValue = state.selectedBadge?.id ?: ""
|
||||
|
||||
Rows.RadioListRow(
|
||||
text = "Expired Badge",
|
||||
labels = badgeLabels,
|
||||
values = badgeValues,
|
||||
selectedValue = selectedBadgeValue,
|
||||
onSelected = callback::onSelectedBadgeChanged
|
||||
)
|
||||
|
||||
val cancellationLabels = UnexpectedSubscriptionCancellation.entries.map { it.status }.toTypedArray()
|
||||
val cancellationValues = UnexpectedSubscriptionCancellation.entries.map { it.status }.toTypedArray()
|
||||
val selectedCancellationValue = state.selectedUnexpectedSubscriptionCancellation?.status ?: ""
|
||||
val cancellationEnabled = state.selectedBadge?.isSubscription() == true
|
||||
|
||||
Rows.RadioListRow(
|
||||
text = "Cancellation Reason",
|
||||
labels = cancellationLabels,
|
||||
values = cancellationValues,
|
||||
selectedValue = selectedCancellationValue,
|
||||
onSelected = callback::onSelectedCancellationChanged,
|
||||
enabled = cancellationEnabled
|
||||
)
|
||||
|
||||
val declineCodeLabels = StripeDeclineCode.Code.entries.map { it.code }.toTypedArray()
|
||||
val declineCodeValues = StripeDeclineCode.Code.entries.map { it.code }.toTypedArray()
|
||||
val selectedDeclineCodeValue = state.selectedStripeDeclineCode?.code ?: ""
|
||||
val declineCodeEnabled = state.selectedBadge?.isSubscription() == true
|
||||
|
||||
Rows.RadioListRow(
|
||||
text = "Charge Failure",
|
||||
labels = declineCodeLabels,
|
||||
values = declineCodeValues,
|
||||
selectedValue = selectedDeclineCodeValue,
|
||||
onSelected = callback::onSelectedDeclineCodeChanged,
|
||||
enabled = declineCodeEnabled
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = callback::onSaveClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Save and Finish")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextButton(
|
||||
onClick = callback::onClearClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Clear")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface for [InternalDonorErrorConfigurationScreen] interactions.
|
||||
*/
|
||||
interface InternalDonorErrorConfigurationScreenCallback {
|
||||
fun onNavigationClick()
|
||||
fun onSelectedBadgeChanged(badgeId: String)
|
||||
fun onSelectedCancellationChanged(status: String)
|
||||
fun onSelectedDeclineCodeChanged(code: String)
|
||||
fun onSaveClick()
|
||||
fun onClearClick()
|
||||
|
||||
object Empty : InternalDonorErrorConfigurationScreenCallback {
|
||||
override fun onNavigationClick() = Unit
|
||||
override fun onSelectedBadgeChanged(badgeId: String) = Unit
|
||||
override fun onSelectedCancellationChanged(status: String) = Unit
|
||||
override fun onSelectedDeclineCodeChanged(code: String) = Unit
|
||||
override fun onSaveClick() = Unit
|
||||
override fun onClearClick() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun InternalDonorErrorConfigurationScreenPreview() {
|
||||
Previews.Preview {
|
||||
InternalDonorErrorConfigurationScreen(
|
||||
state = InternalDonorErrorConfigurationState(
|
||||
badges = listOf(
|
||||
Badge(
|
||||
id = "test1",
|
||||
category = Badge.Category.Testing,
|
||||
name = "Test Badge 1",
|
||||
description = "Test description 1",
|
||||
imageUrl = android.net.Uri.EMPTY,
|
||||
imageDensity = "xxxhdpi",
|
||||
expirationTimestamp = 0L,
|
||||
visible = true,
|
||||
duration = 0L
|
||||
),
|
||||
Badge(
|
||||
id = "test2",
|
||||
category = Badge.Category.Testing,
|
||||
name = "Test Badge 2",
|
||||
description = "Test description 2",
|
||||
imageUrl = android.net.Uri.EMPTY,
|
||||
imageDensity = "xxxhdpi",
|
||||
expirationTimestamp = 0L,
|
||||
visible = true,
|
||||
duration = 0L
|
||||
)
|
||||
),
|
||||
selectedBadge = null,
|
||||
selectedUnexpectedSubscriptionCancellation = null,
|
||||
selectedStripeDeclineCode = null
|
||||
),
|
||||
callback = InternalDonorErrorConfigurationScreenCallback.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getBoostBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
|
||||
@@ -17,58 +18,27 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscr
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class InternalDonorErrorConfigurationViewModel : ViewModel() {
|
||||
|
||||
private val store = RxStore(InternalDonorErrorConfigurationState())
|
||||
private val disposables = CompositeDisposable()
|
||||
private val store = MutableStateFlow(InternalDonorErrorConfigurationState())
|
||||
|
||||
val state: Flowable<InternalDonorErrorConfigurationState> = store.stateFlowable
|
||||
val state: StateFlow<InternalDonorErrorConfigurationState> = store
|
||||
|
||||
init {
|
||||
val giftBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
AppDependencies.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.getGiftBadges() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
val configuration = AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault()).toNetworkResult().successOrNull() ?: return@launch
|
||||
val giftBadges = configuration.getGiftBadges()
|
||||
val boostBadges = configuration.getBoostBadges()
|
||||
val subscriptionBadges = configuration.getSubscriptionLevels().values.map { Badges.fromServiceBadge(it.badge) }
|
||||
|
||||
val boostBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
AppDependencies.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.getBoostBadges() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
val subscriptionBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
AppDependencies.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { config -> config.getSubscriptionLevels().values.map { Badges.fromServiceBadge(it.badge) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
disposables += Single.zip(giftBadges, boostBadges, subscriptionBadges) { g, b, s ->
|
||||
g + b + s
|
||||
}.subscribe { badges ->
|
||||
store.update { it.copy(badges = badges) }
|
||||
store.update { it.copy(badges = giftBadges + boostBadges + subscriptionBadges) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
fun setSelectedBadge(badgeIndex: Int) {
|
||||
store.update {
|
||||
it.copy(selectedBadge = if (badgeIndex in it.badges.indices) it.badges[badgeIndex] else null)
|
||||
@@ -100,7 +70,7 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun save(): Completable {
|
||||
val snapshot = store.state
|
||||
val snapshot = store.value
|
||||
val saveState = Completable.fromAction {
|
||||
InAppPaymentSubscriberRecord.Type.DONATION.lock.withLock {
|
||||
when {
|
||||
|
||||
Reference in New Issue
Block a user