Read and use backups data to structure tier feature sets.
This commit is contained in:
parent
478e3a7233
commit
fd31bc60b2
12 changed files with 258 additions and 130 deletions
|
@ -6,7 +6,6 @@
|
|||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
@ -25,7 +24,6 @@ import org.signal.libsignal.messagebackup.MessageBackupKey
|
|||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
|
@ -46,7 +44,6 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
|||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||
|
@ -83,7 +80,6 @@ import java.io.File
|
|||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigDecimal
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
@ -896,30 +892,29 @@ object BackupRepository {
|
|||
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
|
||||
val backupCurrency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
return when (tier) {
|
||||
MessageBackupTier.FREE -> getFreeType(backupCurrency)
|
||||
MessageBackupTier.FREE -> getFreeType()
|
||||
MessageBackupTier.PAID -> getPaidType(backupCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFreeType(currency: Currency): MessageBackupsType {
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
|
||||
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
private suspend fun getFreeType(): MessageBackupsType {
|
||||
val config = getSubscriptionsConfiguration()
|
||||
|
||||
return MessageBackupsType.Free(
|
||||
mediaRetentionDays = config.backupConfiguration.freeTierMediaDays
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
|
||||
val config = getSubscriptionsConfiguration()
|
||||
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
|
||||
storageAllowanceBytes = config.backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL]!!.storageAllowanceBytes
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getSubscriptionsConfiguration(): SubscriptionsConfiguration {
|
||||
val serviceResponse = withContext(Dispatchers.IO) {
|
||||
AppDependencies
|
||||
.donationsService
|
||||
|
@ -938,31 +933,7 @@ object BackupRepository {
|
|||
error("Unhandled error occurred while downloading configuration.")
|
||||
}
|
||||
|
||||
val config = serviceResponse.result.get()
|
||||
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
|
||||
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
)
|
||||
return serviceResponse.result.get()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,13 +32,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
|
@ -49,7 +47,7 @@ import java.util.Currency
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageBackupsCheckoutSheet(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
messageBackupsType: MessageBackupsType.Paid,
|
||||
availablePaymentMethods: List<InAppPaymentData.PaymentMethodType>,
|
||||
sheetState: SheetState,
|
||||
onDismissRequest: () -> Unit,
|
||||
|
@ -78,7 +76,7 @@ fun MessageBackupsCheckoutSheet(
|
|||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
messageBackupsType: MessageBackupsType.Paid,
|
||||
availablePaymentGateways: List<InAppPaymentData.PaymentMethodType>,
|
||||
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
|
||||
) {
|
||||
|
@ -244,11 +242,9 @@ private fun MessageBackupsCheckoutSheetPreview() {
|
|||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
title = "Free",
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
features = persistentListOf()
|
||||
storageAllowanceBytes = 107374182400
|
||||
),
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = {}
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.navigation.compose.rememberNavController
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
@ -112,7 +113,15 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||
currentBackupTier = state.currentMessageBackupTier,
|
||||
selectedBackupTier = state.selectedMessageBackupTier,
|
||||
availableBackupTypes = state.availableBackupTypes,
|
||||
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||
onMessageBackupsTierSelected = { tier ->
|
||||
val type = state.availableBackupTypes.first { it.tier == tier }
|
||||
val label = when (type) {
|
||||
is MessageBackupsType.Free -> requireContext().resources.getQuantityString(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, type.mediaRetentionDays, type.mediaRetentionDays)
|
||||
is MessageBackupsType.Paid -> requireContext().getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
|
||||
}
|
||||
|
||||
viewModel.onMessageBackupTierUpdated(tier, label)
|
||||
},
|
||||
onNavigationClick = viewModel::goToPreviousScreen,
|
||||
onReadMoreClicked = {},
|
||||
onCancelSubscriptionClicked = viewModel::displayCancellationDialog,
|
||||
|
@ -121,7 +130,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||
|
||||
if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
|
||||
MessageBackupsCheckoutSheet(
|
||||
messageBackupsType = state.availableBackupTypes.first { it.tier == state.selectedMessageBackupTier!! },
|
||||
messageBackupsType = state.availableBackupTypes.filterIsInstance<MessageBackupsType.Paid>().first { it.tier == state.selectedMessageBackupTier!! },
|
||||
availablePaymentMethods = state.availablePaymentMethods,
|
||||
sheetState = checkoutSheetState,
|
||||
onDismissRequest = {
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupTierLabel: String? = null,
|
||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val currentMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
|
|
|
@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
|
@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayOrderStrategy
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.math.BigDecimal
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private val internalStateFlow = MutableStateFlow(
|
||||
|
@ -125,8 +128,13 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
|||
internalStateFlow.update { it.copy(selectedPaymentMethod = paymentMethod) }
|
||||
}
|
||||
|
||||
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
|
||||
internalStateFlow.update { it.copy(selectedMessageBackupTier = messageBackupTier) }
|
||||
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier, messageBackupTierLabel: String) {
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
selectedMessageBackupTier = messageBackupTier,
|
||||
selectedMessageBackupTierLabel = messageBackupTierLabel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCancellationComplete() {
|
||||
|
@ -187,6 +195,8 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
|||
internalStateFlow.update { it.copy(inAppPayment = null) }
|
||||
}
|
||||
|
||||
val currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
|
||||
SignalDatabase.inAppPayments.clearCreated()
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_BACKUP,
|
||||
|
@ -195,8 +205,8 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
|||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
badge = null,
|
||||
label = backupsType.title,
|
||||
amount = backupsType.pricePerMonth.toFiatValue(),
|
||||
label = state.selectedMessageBackupTierLabel!!,
|
||||
amount = if (backupsType is MessageBackupsType.Paid) backupsType.pricePerMonth.toFiatValue() else FiatMoney(BigDecimal.ZERO, currency).toFiatValue(),
|
||||
level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
|
||||
recipientId = Recipient.self().id.serialize(),
|
||||
paymentMethodType = state.selectedPaymentMethod!!,
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
|
||||
|
@ -14,9 +13,20 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
|||
* Represents a type of backup a user can select.
|
||||
*/
|
||||
@Stable
|
||||
data class MessageBackupsType(
|
||||
val tier: MessageBackupTier,
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
sealed interface MessageBackupsType {
|
||||
|
||||
val tier: MessageBackupTier
|
||||
|
||||
data class Paid(
|
||||
val pricePerMonth: FiatMoney,
|
||||
val storageAllowanceBytes: Long
|
||||
) : MessageBackupsType {
|
||||
override val tier: MessageBackupTier = MessageBackupTier.PAID
|
||||
}
|
||||
|
||||
data class Free(
|
||||
val mediaRetentionDays: Int
|
||||
) : MessageBackupsType {
|
||||
override val tier: MessageBackupTier = MessageBackupTier.FREE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
|||
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.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
|
@ -49,10 +50,12 @@ import org.signal.core.ui.Previews
|
|||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ByteUnit
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
|
@ -252,12 +255,15 @@ fun MessageBackupsTypeBlock(
|
|||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
|
||||
text = getFormattedPricePerMonth(messageBackupsType),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsType.title,
|
||||
text = when (messageBackupsType) {
|
||||
is MessageBackupsType.Free -> pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
|
||||
is MessageBackupsType.Paid -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
|
@ -273,7 +279,7 @@ fun MessageBackupsTypeBlock(
|
|||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
messageBackupsType.features.forEach {
|
||||
getFeatures(messageBackupsType = messageBackupsType).forEach {
|
||||
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it, iconTint = featureIconTint)
|
||||
}
|
||||
}
|
||||
|
@ -290,53 +296,73 @@ fun MessageBackupsTypeBlock(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
return if (pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
"Free"
|
||||
} else {
|
||||
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
|
||||
private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): String {
|
||||
return when (messageBackupsType) {
|
||||
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
|
||||
is MessageBackupsType.Paid -> {
|
||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getFeatures(messageBackupsType: MessageBackupsType): List<MessageBackupsTypeFeature> {
|
||||
return when (messageBackupsType) {
|
||||
is MessageBackupsType.Free -> persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_text_message_backup)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = pluralStringResource(
|
||||
id = R.plurals.MessageBackupsTypeSelectionScreen__last_d_days_of_media,
|
||||
count = messageBackupsType.mediaRetentionDays,
|
||||
messageBackupsType.mediaRetentionDays
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageBackupsType.Paid -> {
|
||||
val photoCount = messageBackupsType.storageAllowanceBytes / ByteUnit.MEGABYTES.toBytes(2)
|
||||
val photoCountThousands = photoCount / 1000
|
||||
val (count, size) = messageBackupsType.storageAllowanceBytes.bytes.getLargestNonZeroValue()
|
||||
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_text_message_backup)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_media_backup)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(
|
||||
id = R.string.MessageBackupsTypeSelectionScreen__s_of_storage_s_photos,
|
||||
"${count}${size.label}",
|
||||
"~${photoCountThousands}K"
|
||||
)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__thanks_for_supporting_signal)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun testBackupTypes(): List<MessageBackupsType> {
|
||||
return listOf(
|
||||
MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
title = "Text + 30 days of media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media"
|
||||
)
|
||||
)
|
||||
MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
title = "Text + All your media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!"
|
||||
)
|
||||
)
|
||||
storageAllowanceBytes = 107374182400
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -42,13 +42,13 @@ import androidx.compose.ui.graphics.Color
|
|||
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.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
@ -66,19 +66,19 @@ import org.signal.donations.InAppPaymentType
|
|||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
|
@ -418,14 +418,29 @@ private fun BackupTypeRow(
|
|||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
} else if (messageBackupsType is MessageBackupsType.Paid) {
|
||||
val localResources = LocalContext.current.resources
|
||||
val formattedCurrency = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(localResources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, messageBackupsType.title, formattedCurrency)
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media), formattedCurrency)
|
||||
)
|
||||
} else {
|
||||
val retentionDays = (messageBackupsType as MessageBackupsType.Free).mediaRetentionDays
|
||||
val localResources = LocalContext.current.resources
|
||||
val formattedCurrency = remember {
|
||||
val currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
FiatMoneyUtil.format(localResources, FiatMoney(BigDecimal.ZERO, currency), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month,
|
||||
pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, retentionDays, retentionDays),
|
||||
formattedCurrency
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -680,11 +695,8 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||
private fun BackupTypeRowPreview() {
|
||||
Previews.Preview {
|
||||
BackupTypeRow(
|
||||
messageBackupsType = MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
title = "Free",
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
features = persistentListOf()
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
onChangeBackupsTypeClick = {},
|
||||
onEnableBackupsClick = {}
|
||||
|
|
|
@ -20,11 +20,11 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
|
@ -33,16 +33,16 @@ import org.signal.core.util.money.FiatMoney
|
|||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
|
@ -161,8 +161,18 @@ private fun BackupsTypeRow(
|
|||
nextRenewalTimestamp: Long
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val formattedAmount = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
val formattedAmount = remember(messageBackupsType) {
|
||||
val amount = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> messageBackupsType.pricePerMonth
|
||||
else -> FiatMoney(BigDecimal.ZERO, SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP))
|
||||
}
|
||||
|
||||
FiatMoneyUtil.format(resources, amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
val title = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
|
||||
is MessageBackupsType.Free -> pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, count = messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
|
||||
}
|
||||
|
||||
val renewal = remember(nextRenewalTimestamp) {
|
||||
|
@ -171,7 +181,7 @@ private fun BackupsTypeRow(
|
|||
|
||||
Rows.TextRow(text = {
|
||||
Column {
|
||||
Text(text = messageBackupsType.title)
|
||||
Text(text = title)
|
||||
Text(
|
||||
text = stringResource(id = R.string.BackupsTypeSettingsFragment__s_month_renews_s, formattedAmount, renewal),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
@ -212,11 +222,8 @@ private fun BackupsTypeSettingsContentPreview() {
|
|||
Previews.Preview {
|
||||
BackupsTypeSettingsContent(
|
||||
state = BackupsTypeSettingsState(
|
||||
messageBackupsType = MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
title = "Free",
|
||||
features = persistentListOf()
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
)
|
||||
),
|
||||
contentCallbacks = object : ContentCallbacks {}
|
||||
|
|
|
@ -7458,6 +7458,30 @@
|
|||
<string name="MessageBackupsTypeSelectionScreen__change_backup_type">Change backup type</string>
|
||||
<!-- Secondary action button label when selecting a backup tier with a current selection -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
|
||||
<!-- MessageBackupsType block amount for free tier -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__free">Free</string>
|
||||
<!-- MessageBackupsType block amount for paid tier. Placeholder is formatted currency amount. -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__s_month">%1$s/month</string>
|
||||
<!-- Title for free tier -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__text_plus_all_your_media">Text + all your media</string>
|
||||
<!-- Title for paid tier. Placeholder is days of media retention. -->
|
||||
<plurals name="MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media">
|
||||
<item quantity="one">Text + %1$d day of media</item>
|
||||
<item quantity="other">Text + %1$d days of media</item>
|
||||
</plurals>
|
||||
<!-- Description text for text history feature -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__full_text_message_backup">Full text message backup</string>
|
||||
<!-- Description text for paid tier media retention -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__full_media_backup">Full media backup</string>
|
||||
<!-- Description text for storage space for paid tier media. Placeholder 1 is for byte amount, placeholder 2 is for a photo count estimate -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__s_of_storage_s_photos">%1$s of storage (%2$s photos)</string>
|
||||
<!-- Description text for thanks -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__thanks_for_supporting_signal">Thanks for supporting Signal!</string>
|
||||
<!-- Description text for free tier media retention. Placeholder is retention day count. -->
|
||||
<plurals name="MessageBackupsTypeSelectionScreen__last_d_days_of_media">
|
||||
<item quantity="one">Last %1$d day of media</item>
|
||||
<item quantity="other">Last %1$d days of media</item>
|
||||
</plurals>
|
||||
|
||||
<!-- ConfirmBackupCancellationDialog -->
|
||||
<!-- Dialog title -->
|
||||
|
|
|
@ -29,6 +29,12 @@ inline val Long.gibiBytes: ByteSize
|
|||
inline val Int.gibiBytes: ByteSize
|
||||
get() = (this * 1024).mebiBytes
|
||||
|
||||
inline val Long.tebiBytes: ByteSize
|
||||
get() = (this * 1024).gibiBytes
|
||||
|
||||
inline val Int.tebiBytes: ByteSize
|
||||
get() = (this * 1024).gibiBytes
|
||||
|
||||
class ByteSize(val bytes: Long) {
|
||||
val inWholeBytes: Long
|
||||
get() = bytes
|
||||
|
@ -42,6 +48,9 @@ class ByteSize(val bytes: Long) {
|
|||
val inWholeGibiBytes: Long
|
||||
get() = inWholeMebiBytes / 1024
|
||||
|
||||
val inWholeTebiBytes: Long
|
||||
get() = inWholeGibiBytes / 1024
|
||||
|
||||
val inKibiBytes: Float
|
||||
get() = bytes / 1024f
|
||||
|
||||
|
@ -50,4 +59,25 @@ class ByteSize(val bytes: Long) {
|
|||
|
||||
val inGibiBytes: Float
|
||||
get() = inMebiBytes / 1024f
|
||||
|
||||
val inTebiBytes: Float
|
||||
get() = inGibiBytes / 1024f
|
||||
|
||||
fun getLargestNonZeroValue(): Pair<Long, Size> {
|
||||
return when {
|
||||
inWholeTebiBytes > 0L -> inWholeTebiBytes to Size.TEBIBYTE
|
||||
inWholeGibiBytes > 0L -> inWholeGibiBytes to Size.GIBIBYTE
|
||||
inWholeMebiBytes > 0L -> inWholeMebiBytes to Size.MEBIBYTE
|
||||
inWholeKibiBytes > 0L -> inWholeKibiBytes to Size.KIBIBYTE
|
||||
else -> inWholeBytes to Size.BYTE
|
||||
}
|
||||
}
|
||||
|
||||
enum class Size(val label: String) {
|
||||
BYTE("B"),
|
||||
KIBIBYTE("KB"),
|
||||
MEBIBYTE("MB"),
|
||||
GIBIBYTE("GB"),
|
||||
TEBIBYTE("TB")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ public class SubscriptionsConfiguration {
|
|||
@JsonProperty("sepaMaximumEuros")
|
||||
private BigDecimal sepaMaximumEuros;
|
||||
|
||||
@JsonProperty("backup")
|
||||
private BackupConfiguration backupConfiguration;
|
||||
|
||||
public static class CurrencyConfiguration {
|
||||
@JsonProperty("minimum")
|
||||
private BigDecimal minimum;
|
||||
|
@ -88,6 +91,31 @@ public class SubscriptionsConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
public static class BackupConfiguration {
|
||||
@JsonProperty("levels")
|
||||
private Map<Integer, BackupLevelConfiguration> backupLevelConfigurationMap;
|
||||
|
||||
@JsonProperty("backupFreeTierMediaDays")
|
||||
private int freeTierMediaDays;
|
||||
|
||||
public Map<Integer, BackupLevelConfiguration> getBackupLevelConfigurationMap() {
|
||||
return backupLevelConfigurationMap;
|
||||
}
|
||||
|
||||
public int getFreeTierMediaDays() {
|
||||
return freeTierMediaDays;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupLevelConfiguration {
|
||||
@JsonProperty("storageAllowanceBytes")
|
||||
private long storageAllowanceBytes;
|
||||
|
||||
public long getStorageAllowanceBytes() {
|
||||
return storageAllowanceBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, CurrencyConfiguration> getCurrencies() {
|
||||
return currencies;
|
||||
}
|
||||
|
@ -99,4 +127,8 @@ public class SubscriptionsConfiguration {
|
|||
public BigDecimal getSepaMaximumEuros() {
|
||||
return sepaMaximumEuros;
|
||||
}
|
||||
|
||||
public BackupConfiguration getBackupConfiguration() {
|
||||
return backupConfiguration;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue