Add base and subclassed upgrade sheets.

This commit is contained in:
Alex Hart
2024-10-09 16:40:32 -03:00
committed by Greyson Parrelli
parent 7abe76f76a
commit 7cc425fa7b
10 changed files with 412 additions and 78 deletions

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.billing.upgrade
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeIconColors
import org.thoughtcrime.securesms.backup.v2.ui.subscription.testBackupTypes
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
/**
* Sheet describing how users must upgrade to enable optimized storage.
*/
class UpgradeToEnableOptimizedStorageSheet : UpgradeToPaidTierBottomSheet() {
@Composable
override fun UpgradeSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit
) {
UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType = paidBackupType,
isSubscribeEnabled = isSubscribeEnabled,
onSubscribeClick = onSubscribeClick,
onCancelClick = {
dismissAllowingStateLoss()
}
)
}
}
@Composable
private fun UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType: MessageBackupsType.Paid,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
BottomSheets.Handle()
Image(
painter = painterResource(id = R.drawable.image_signal_backups),
contentDescription = null,
modifier = Modifier
.padding(top = 8.dp, bottom = 16.dp)
.size(80.dp)
)
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_to_enable_this_feature),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
)
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__storage_optimization_can_only_be_used),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, bottom = 28.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
isCurrent = false,
isSelected = false,
onSelected = {},
enabled = false,
iconColors = MessageBackupsTypeIconColors.default().let {
it.copy(iconColorNormal = it.iconColorSelected)
},
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 50.dp)
)
Buttons.LargePrimary(
enabled = isSubscribeEnabled,
onClick = onSubscribeClick,
modifier = Modifier
.defaultMinSize(minWidth = 256.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 8.dp)
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__subscribe_for_s_month, formattedPrice)
)
}
TextButton(
enabled = isSubscribeEnabled,
onClick = onCancelClick,
modifier = Modifier
.defaultMinSize(minWidth = 256.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 16.dp)
) {
Text(
text = stringResource(id = android.R.string.cancel)
)
}
}
}
@SignalPreview
@Composable
private fun UpgradeToEnableOptimizedStorageSheetContentPreview() {
Previews.BottomSheetPreview {
UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType = testBackupTypes()[1] as MessageBackupsType.Paid,
isSubscribeEnabled = true
)
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.billing.upgrade
import android.os.Bundle
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import kotlinx.coroutines.rx3.asFlowable
import org.signal.core.ui.Dialogs
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowViewModel
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsStage
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.viewModel
/**
* BottomSheet that encapsulates the common logic for updating someone to paid tier.
*/
abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment(), InAppPaymentCheckoutDelegate.ErrorHandlerCallback {
companion object {
const val RESULT_KEY = "UpgradeToPaidTierBottomSheet.RESULT_KEY"
}
private val viewModel: MessageBackupsFlowViewModel by viewModel {
MessageBackupsFlowViewModel(
initialTierSelection = MessageBackupTier.PAID,
startScreen = MessageBackupsStage.TYPE_SELECTION
)
}
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
override val peekHeightPercentage: Float = 1f
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
errorHandler.attach(
fragment = this,
errorHandlerCallback = this,
inAppPaymentIdSource = viewModel.stateFlow.asFlowable()
.filter { it.inAppPayment != null }
.map { it.inAppPayment!!.id }
)
}
@Composable
override fun SheetContent() {
val state by viewModel.stateFlow.collectAsState()
val paidBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.PAID } as? MessageBackupsType.Paid
val freeBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.FREE } as? MessageBackupsType.Free
if (paidBackupType != null && freeBackupType != null) {
UpgradeSheetContent(
paidBackupType = paidBackupType,
freeBackupType = freeBackupType,
isSubscribeEnabled = state.stage == MessageBackupsStage.TYPE_SELECTION,
onSubscribeClick = viewModel::goToNextStage
)
}
when (state.stage) {
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> Dialogs.IndeterminateProgressDialog()
MessageBackupsStage.PROCESS_PAYMENT -> Dialogs.IndeterminateProgressDialog()
MessageBackupsStage.PROCESS_FREE -> Dialogs.IndeterminateProgressDialog()
else -> Unit
}
LaunchedEffect(state.stage) {
if (state.stage == MessageBackupsStage.CHECKOUT_SHEET) {
AppDependencies.billingApi.launchBillingFlow(requireActivity())
}
if (state.stage == MessageBackupsStage.COMPLETED) {
dismissAllowingStateLoss()
setFragmentResult(RESULT_KEY, bundleOf(RESULT_KEY to true))
}
}
}
/**
* This is responsible for displaying the normal upgrade sheet content.
*/
@Composable
abstract fun UpgradeSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit
)
override fun onUserLaunchedAnExternalApplication() = error("Unsupported.")
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Unsupported.")
override fun exitCheckoutFlow() {
dismissAllowingStateLoss()
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.billing.upgrade
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeIconColors
import org.thoughtcrime.securesms.backup.v2.ui.subscription.testBackupTypes
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
/**
* Bottom sheet notifying user that the media they selected is no longer available. This
* can occur when a user had a paid tier in the past and had storage optimization enabled,
* but did not download their media within 30 days of canceling their subscription.
*/
class UpgradeToStartMediaBackupSheet : UpgradeToPaidTierBottomSheet() {
@Composable
override fun UpgradeSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit
) {
UpgradeToStartMediaBackupSheetContent(
paidBackupType = paidBackupType,
freeBackupType = freeBackupType,
isSubscribeEnabled = isSubscribeEnabled,
onSubscribeClick = onSubscribeClick,
onCancelClick = {
dismissAllowingStateLoss()
}
)
}
}
@Composable
private fun UpgradeToStartMediaBackupSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
) {
BottomSheets.Handle()
Image(
painter = painterResource(R.drawable.image_signal_backups_media),
contentDescription = null,
modifier = Modifier
.size(80.dp)
)
Text(
text = stringResource(R.string.UpgradeToStartMediaBackupSheet__this_media_is_no_longer_available),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(
text = pluralStringResource(R.plurals.UpgradeToStartMediaBackupSheet__your_current_signal_backup_plan_includes, freeBackupType.mediaRetentionDays, freeBackupType.mediaRetentionDays),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
MessageBackupsTypeBlock(
messageBackupsType = paidBackupType,
isCurrent = false,
isSelected = false,
onSelected = {},
enabled = false,
modifier = Modifier.padding(top = 24.dp, bottom = 32.dp),
iconColors = MessageBackupsTypeIconColors.default().let {
it.copy(iconColorNormal = it.iconColorSelected)
}
)
Buttons.LargePrimary(
enabled = isSubscribeEnabled,
onClick = onSubscribeClick,
modifier = Modifier
.defaultMinSize(minWidth = 256.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 8.dp)
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(paidBackupType.pricePerMonth) {
FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = stringResource(id = R.string.UpgradeToStartMediaBackupSheet__subscribe_for_s_month, formattedPrice)
)
}
TextButton(
enabled = isSubscribeEnabled,
onClick = onCancelClick,
modifier = Modifier
.defaultMinSize(minWidth = 256.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 16.dp)
) {
Text(
text = stringResource(id = android.R.string.cancel)
)
}
}
}
@SignalPreview
@Composable
private fun UpgradeToStartMediaBackupSheetContentPreview() {
Previews.Preview {
UpgradeToStartMediaBackupSheetContent(
paidBackupType = testBackupTypes()[1] as MessageBackupsType.Paid,
freeBackupType = testBackupTypes()[0] as MessageBackupsType.Free,
isSubscribeEnabled = true
)
}
}