Implement new top-level backups settings.
This commit is contained in:
parent
ea33fa2af1
commit
5bdc7c2740
25 changed files with 870 additions and 618 deletions
|
@ -105,6 +105,7 @@ object BackupRepository {
|
|||
403 -> {
|
||||
Log.w(TAG, "Received status 403. The user is not in the media tier. Updating local state.", error.exception)
|
||||
SignalStore.backup.backupTier = MessageBackupTier.FREE
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
// TODO [backup] If the user thought they were in media tier but aren't, feels like we should have a special UX flow for this?
|
||||
}
|
||||
}
|
||||
|
@ -846,6 +847,11 @@ object BackupRepository {
|
|||
Log.i(TAG, "Could not retrieve backup tier.", e)
|
||||
null
|
||||
}
|
||||
|
||||
if (SignalStore.backup.backupTier != null) {
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
}
|
||||
|
||||
return SignalStore.backup.backupTier
|
||||
}
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
|||
MessageBackupTier.FREE -> {
|
||||
SignalStore.backup.areBackupsEnabled = true
|
||||
SignalStore.backup.backupTier = MessageBackupTier.FREE
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
|
||||
state.copy(stage = MessageBackupsStage.COMPLETED)
|
||||
}
|
||||
|
|
|
@ -201,6 +201,17 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
if (RemoteConfig.messageBackups) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__backups),
|
||||
// TODO [message-backups] -- icon
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
|
||||
},
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_data_24),
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Top-level backups settings screen.
|
||||
*/
|
||||
class BackupsSettingsFragment : ComposeFragment() {
|
||||
|
||||
private lateinit var checkoutLauncher: ActivityResultLauncher<Unit>
|
||||
|
||||
private val viewModel: BackupsSettingsViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
checkoutLauncher = createBackupsCheckoutLauncher {
|
||||
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = state,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onBackupsRowClick = {
|
||||
when (state.enabledState) {
|
||||
is BackupsSettingsState.EnabledState.Active, BackupsSettingsState.EnabledState.Inactive -> {
|
||||
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Never -> {
|
||||
checkoutLauncher.launch(Unit)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupsSettingsContent(
|
||||
backupsSettingsState: BackupsSettingsState,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onBackupsRowClick: () -> Unit = {},
|
||||
onOnDeviceBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.preferences_chats__backups),
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__back_up_your_message_history),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter), vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
when (backupsSettingsState.enabledState) {
|
||||
BackupsSettingsState.EnabledState.Loading -> {
|
||||
LoadingBackupsRow()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Inactive -> {
|
||||
InactiveBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
is BackupsSettingsState.EnabledState.Active -> {
|
||||
ActiveBackupsRow(
|
||||
enabledState = backupsSettingsState.enabledState,
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Never -> {
|
||||
NeverEnabledBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Failed -> {
|
||||
Text(text = "TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__other_ways_to_backup)
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups),
|
||||
label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to),
|
||||
onClick = onOnDeviceBackupsRowClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NeverEnabledBackupsRow(
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_backup_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupsSettingsFragment_automatic_backups_with_signals),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Buttons.MediumTonal(
|
||||
onClick = onBackupsRowClick,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.BackupsSettingsFragment_set_up)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InactiveBackupsRow(
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
label = stringResource(R.string.preferences_off),
|
||||
icon = painterResource(R.drawable.symbol_backup_24),
|
||||
onClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveBackupsRow(
|
||||
enabledState: BackupsSettingsState.EnabledState.Active,
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
icon = {
|
||||
Box(
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_backup_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
|
||||
)
|
||||
|
||||
when (enabledState.type) {
|
||||
is MessageBackupsType.Paid -> {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.BackupsSettingsFragment_s_month_renews_s,
|
||||
FiatMoneyUtil.format(LocalContext.current.resources, enabledState.type.pricePerMonth),
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), enabledState.expiresAt.inWholeMilliseconds)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
is MessageBackupsType.Free -> {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.BackupsSettingsFragment_last_backup_s,
|
||||
DateUtils.getDatelessRelativeTimeSpanFormattedDate(
|
||||
LocalContext.current,
|
||||
Locale.getDefault(),
|
||||
enabledState.lastBackupAt.inWholeMilliseconds
|
||||
).value
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingBackupsRow() {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupsSettingsContentPreview() {
|
||||
Previews.Preview {
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = BackupsSettingsState(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 1_000_000
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = 0.seconds
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun InactiveBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
InactiveBackupsRow()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ActivePaidBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
ActiveBackupsRow(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 1_000_000
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = 0.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ActiveFreeBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
ActiveBackupsRow(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = 0.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun LoadingBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
LoadingBackupsRow()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NeverEnabledBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
NeverEnabledBackupsRow()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Screen state for top-level backups settings screen.
|
||||
*/
|
||||
data class BackupsSettingsState(
|
||||
val enabledState: EnabledState = EnabledState.Loading
|
||||
) {
|
||||
/**
|
||||
* Describes the 'enabled' state of backups.
|
||||
*/
|
||||
sealed interface EnabledState {
|
||||
/**
|
||||
* Loading data for this row
|
||||
*/
|
||||
data object Loading : EnabledState
|
||||
|
||||
/**
|
||||
* Backups have never been enabled.
|
||||
*/
|
||||
data object Never : EnabledState
|
||||
|
||||
/**
|
||||
* Backups were active at one point, but have been turned off.
|
||||
*/
|
||||
data object Inactive : EnabledState
|
||||
|
||||
/**
|
||||
* Backup state couldn't be retrieved from the server for some reason
|
||||
*/
|
||||
data object Failed : EnabledState
|
||||
|
||||
/**
|
||||
* Backups are currently active.
|
||||
*/
|
||||
data class Active(val type: MessageBackupsType, val expiresAt: Duration, val lastBackupAt: Duration) : EnabledState
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BackupsSettingsViewModel::class)
|
||||
}
|
||||
|
||||
private val internalStateFlow = MutableStateFlow(BackupsSettingsState())
|
||||
|
||||
val stateFlow: StateFlow<BackupsSettingsState> = internalStateFlow
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
InternetConnectionObserver.observe().asFlow()
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.drop(1)
|
||||
.collect {
|
||||
refreshState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
Log.d(TAG, "Refreshing state.")
|
||||
loadEnabledState()
|
||||
}
|
||||
|
||||
private fun loadEnabledState() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val enabledState = when (SignalStore.backup.backupTier) {
|
||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
||||
null -> getEnabledStateForNoTier()
|
||||
}
|
||||
|
||||
internalStateFlow.update { it.copy(enabledState = enabledState) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
type = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to build enabled state.", e)
|
||||
BackupsSettingsState.EnabledState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getEnabledStateForPaidTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
val backupType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as MessageBackupsType.Paid
|
||||
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrThrow()
|
||||
if (activeSubscription.isActive) {
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
type = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney.fromSignalNetworkAmount(
|
||||
activeSubscription.activeSubscription.amount,
|
||||
Currency.getInstance(activeSubscription.activeSubscription.currency)
|
||||
),
|
||||
storageAllowanceBytes = backupType.storageAllowanceBytes
|
||||
)
|
||||
)
|
||||
} else {
|
||||
BackupsSettingsState.EnabledState.Inactive
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to build enabled state.", e)
|
||||
BackupsSettingsState.EnabledState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnabledStateForNoTier(): BackupsSettingsState.EnabledState {
|
||||
return if (SignalStore.uiHints.hasEverEnabledRemoteBackups) {
|
||||
BackupsSettingsState.EnabledState.Never
|
||||
} else {
|
||||
BackupsSettingsState.EnabledState.Inactive
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
|
@ -44,6 +46,7 @@ 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.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
|
@ -58,16 +61,18 @@ import org.signal.core.ui.Scaffolds
|
|||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Snackbars
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
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.backups.type.BackupsTypeSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
|
@ -75,7 +80,10 @@ 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
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Remote backups settings fragment.
|
||||
|
@ -105,7 +113,8 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
requestedSnackbar = state.snackbar,
|
||||
contentCallbacks = callbacks,
|
||||
backupProgress = backupProgress,
|
||||
backupSize = state.backupSize
|
||||
backupSize = state.backupSize,
|
||||
renewalTime = state.renewalTime
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -115,18 +124,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
override fun onEnableBackupsClick() {
|
||||
checkoutLauncher.launch(Unit)
|
||||
override fun onBackupTypeActionClick(tier: MessageBackupTier) {
|
||||
// TODO [message-backups]
|
||||
}
|
||||
|
||||
override fun onBackUpUsingCellularClick(canUseCellular: Boolean) {
|
||||
viewModel.setCanBackUpUsingCellular(canUseCellular)
|
||||
}
|
||||
|
||||
override fun onViewPaymentHistory() {
|
||||
findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_remoteBackupsPaymentHistoryFragment)
|
||||
}
|
||||
|
||||
override fun onBackupNowClick() {
|
||||
viewModel.onBackupNowClick()
|
||||
}
|
||||
|
@ -191,10 +196,9 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
*/
|
||||
private interface ContentCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun onEnableBackupsClick() = Unit
|
||||
fun onBackupsTypeClick() = Unit
|
||||
fun onBackupTypeActionClick(tier: MessageBackupTier) = Unit
|
||||
fun onBackUpUsingCellularClick(canUseCellular: Boolean) = Unit
|
||||
fun onViewPaymentHistory() = Unit
|
||||
fun onBackupNowClick() = Unit
|
||||
fun onTurnOffAndDeleteBackupsClick() = Unit
|
||||
fun onChangeBackupFrequencyClick() = Unit
|
||||
|
@ -207,6 +211,7 @@ private interface ContentCallbacks {
|
|||
@Composable
|
||||
private fun RemoteBackupsSettingsContent(
|
||||
messageBackupsType: MessageBackupsType?,
|
||||
renewalTime: Duration,
|
||||
lastBackupTimestamp: Long,
|
||||
canBackUpUsingCellular: Boolean,
|
||||
backupsFrequency: BackupFrequency,
|
||||
|
@ -232,99 +237,88 @@ private fun RemoteBackupsSettingsContent(
|
|||
modifier = Modifier
|
||||
.padding(it)
|
||||
) {
|
||||
item {
|
||||
BackupTypeRow(
|
||||
messageBackupsType = messageBackupsType,
|
||||
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
|
||||
onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick
|
||||
)
|
||||
if (messageBackupsType != null) {
|
||||
item {
|
||||
BackupTypeRow(
|
||||
messageBackupsType = messageBackupsType,
|
||||
renewalTime = renewalTime,
|
||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (messageBackupsType == null) {
|
||||
item {
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__payment_history),
|
||||
onClick = contentCallbacks::onViewPaymentHistory
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Dividers.Default()
|
||||
InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = Util.getPrettyFileSize(backupSize),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = Util.getPrettyFileSize(backupSize),
|
||||
text = getTextForFrequency(backupsFrequency = backupsFrequency),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onClick = contentCallbacks::onChangeBackupFrequencyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = getTextForFrequency(backupsFrequency = backupsFrequency),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = contentCallbacks::onChangeBackupFrequencyClick
|
||||
)
|
||||
}
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
}
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick
|
||||
)
|
||||
}
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -383,63 +377,71 @@ private fun RemoteBackupsSettingsContent(
|
|||
|
||||
@Composable
|
||||
private fun BackupTypeRow(
|
||||
messageBackupsType: MessageBackupsType?,
|
||||
onEnableBackupsClick: () -> Unit,
|
||||
onChangeBackupsTypeClick: () -> Unit
|
||||
messageBackupsType: MessageBackupsType,
|
||||
renewalTime: Duration,
|
||||
onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(top = 16.dp, bottom = 14.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_type),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
if (messageBackupsType == null) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backups_disabled),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else if (messageBackupsType is MessageBackupsType.Paid) {
|
||||
val localResources = LocalContext.current.resources
|
||||
val formattedCurrency = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(localResources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
Row {
|
||||
Column {
|
||||
val title = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
|
||||
is MessageBackupsType.Free -> pluralStringResource(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media), formattedCurrency)
|
||||
text = buildAnnotatedString {
|
||||
SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK)
|
||||
append(" ")
|
||||
append(title)
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
} 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())
|
||||
|
||||
val cost = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth))
|
||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
text = cost,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
|
||||
if (messageBackupsType is MessageBackupsType.Paid) {
|
||||
if (renewalTime > 0.seconds) {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__renews_s, DateUtils.formatDateWithYear(Locale.getDefault(), renewalTime.inWholeMilliseconds))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon
|
||||
}
|
||||
|
||||
if (messageBackupsType == null) {
|
||||
Buttons.Small(onClick = onEnableBackupsClick) {
|
||||
Text(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__enable_backups))
|
||||
}
|
||||
val buttonText = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__manage_or_cancel)
|
||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__upgrade)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) },
|
||||
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
||||
containerColor = SignalTheme.colors.colorTransparent5
|
||||
),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = buttonText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -668,7 +670,7 @@ private fun getTextForFrequency(backupsFrequency: BackupFrequency): String {
|
|||
private fun RemoteBackupsSettingsContentPreview() {
|
||||
Previews.Preview {
|
||||
RemoteBackupsSettingsContent(
|
||||
messageBackupsType = null,
|
||||
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30),
|
||||
lastBackupTimestamp = -1,
|
||||
canBackUpUsingCellular = false,
|
||||
backupsFrequency = BackupFrequency.MANUAL,
|
||||
|
@ -676,6 +678,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||
requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||
contentCallbacks = object : ContentCallbacks {},
|
||||
backupProgress = null,
|
||||
renewalTime = 1727193018.seconds,
|
||||
backupSize = 2300000
|
||||
)
|
||||
}
|
||||
|
@ -685,13 +688,22 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||
@Composable
|
||||
private fun BackupTypeRowPreview() {
|
||||
Previews.Preview {
|
||||
BackupTypeRow(
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
onChangeBackupsTypeClick = {},
|
||||
onEnableBackupsClick = {}
|
||||
)
|
||||
Column {
|
||||
BackupTypeRow(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000
|
||||
),
|
||||
renewalTime = 1727193018.seconds
|
||||
)
|
||||
|
||||
BackupTypeRow(
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
renewalTime = 0.seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class RemoteBackupsSettingsState(
|
||||
val messageBackupsType: MessageBackupsType? = null,
|
||||
|
@ -14,6 +16,7 @@ data class RemoteBackupsSettingsState(
|
|||
val backupSize: Long = 0,
|
||||
val backupsFrequency: BackupFrequency = BackupFrequency.DAILY,
|
||||
val lastBackupTimestamp: Long = 0,
|
||||
val renewalTime: Duration = 0.seconds,
|
||||
val dialog: Dialog = Dialog.NONE,
|
||||
val snackbar: Snackbar = Snackbar.NONE
|
||||
) {
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -17,11 +17,14 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* ViewModel for state management of RemoteBackupsSettingsFragment
|
||||
|
@ -40,6 +43,19 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||
|
||||
init {
|
||||
refresh()
|
||||
|
||||
viewModelScope.launch {
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
if (activeSubscription.isSuccess) {
|
||||
val subscription = activeSubscription.getOrThrow().activeSubscription
|
||||
if (subscription.isActive && subscription != null) {
|
||||
_state.update { it.copy(renewalTime = subscription.endOfCurrentPeriod.seconds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.type
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -39,7 +39,6 @@ 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.Locale
|
||||
|
@ -86,10 +85,6 @@ class BackupsTypeSettingsFragment : ComposeFragment() {
|
|||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
override fun onPaymentHistoryClick() {
|
||||
findNavController().safeNavigate(R.id.action_backupsTypeSettingsFragment_to_remoteBackupsPaymentHistoryFragment)
|
||||
}
|
||||
|
||||
override fun onChangeOrCancelSubscriptionClick() {
|
||||
checkoutLauncher.launch(Unit)
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.type
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import org.signal.donations.PaymentSourceType
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.type
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
|
@ -1,337 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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 androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.navArgument
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.receipts.ReceiptImageRenderer
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.Nav
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.math.BigDecimal
|
||||
import java.util.Calendar
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Displays a list or detail view of in-app-payment receipts related to
|
||||
* backups.
|
||||
*/
|
||||
class RemoteBackupsPaymentHistoryFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: RemoteBackupsPaymentHistoryViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val navController = rememberNavController()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navController.setOnBackPressedDispatcher(requireActivity().onBackPressedDispatcher)
|
||||
navController.enableOnBackPressed(true)
|
||||
}
|
||||
|
||||
val onNavigationClick = remember {
|
||||
{
|
||||
if (!navController.popBackStack()) {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Nav.Host(navController = navController, startDestination = "list") {
|
||||
composable("list") {
|
||||
PaymentHistoryContent(
|
||||
state = state,
|
||||
onNavigationClick = onNavigationClick,
|
||||
onRecordClick = { navController.navigate("detail/${it.id}") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("detail/{recordId}", listOf(navArgument("recordId") { type = NavType.LongType })) { backStackEntry ->
|
||||
val recordId = backStackEntry.arguments?.getLong("recordId")!!
|
||||
val record = state.records[recordId]!!
|
||||
|
||||
PaymentHistoryDetails(
|
||||
record = record,
|
||||
onNavigationClick = onNavigationClick,
|
||||
onShareClick = this@RemoteBackupsPaymentHistoryFragment::onShareClick
|
||||
)
|
||||
|
||||
if (state.displayProgressDialog) {
|
||||
Dialogs.IndeterminateProgressDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShareClick(record: InAppPaymentReceiptRecord) {
|
||||
viewModel.onStartRenderingBitmap()
|
||||
ReceiptImageRenderer.renderPng(
|
||||
requireContext(),
|
||||
viewLifecycleOwner,
|
||||
record,
|
||||
getString(R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup),
|
||||
object : ReceiptImageRenderer.Callback {
|
||||
override fun onBitmapRendered() {
|
||||
viewModel.onEndRenderingBitmap()
|
||||
}
|
||||
|
||||
override fun onStartActivity(intent: Intent) {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentHistoryContent(
|
||||
state: RemoteBackupsPaymentHistoryState,
|
||||
onNavigationClick: () -> Unit,
|
||||
onRecordClick: (InAppPaymentReceiptRecord) -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_history),
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
) {
|
||||
val itemList = remember(state.records) { state.records.values.toPersistentList() }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = itemList,
|
||||
key = { _, item -> item.id }
|
||||
) { idx, item ->
|
||||
val previous = itemList.getOrNull(idx - 1)
|
||||
val previousYear = rememberYear(timestamp = previous?.timestamp ?: 0)
|
||||
val ourYear = rememberYear(timestamp = item.timestamp)
|
||||
|
||||
if (previousYear != ourYear) {
|
||||
Texts.SectionHeader(text = "$ourYear")
|
||||
}
|
||||
|
||||
PaymentHistoryRow(item, onRecordClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberYear(timestamp: Long): Int {
|
||||
if (timestamp == 0L) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val calendar = remember {
|
||||
Calendar.getInstance()
|
||||
}
|
||||
|
||||
return remember(timestamp) {
|
||||
calendar.timeInMillis = timestamp
|
||||
calendar.get(Calendar.YEAR)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentHistoryRow(
|
||||
record: InAppPaymentReceiptRecord,
|
||||
onRecordClick: (InAppPaymentReceiptRecord) -> Unit
|
||||
) {
|
||||
val date = remember(record.timestamp) {
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), record.timestamp)
|
||||
}
|
||||
|
||||
val onClick = remember(record) {
|
||||
{ onRecordClick(record) }
|
||||
}
|
||||
|
||||
Rows.TextRow(text = {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = date,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
val resources = LocalContext.current.resources
|
||||
val fiat = remember(record.amount) {
|
||||
FiatMoneyUtil.format(resources, record.amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(text = fiat)
|
||||
}, onClick = onClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentHistoryDetails(
|
||||
record: InAppPaymentReceiptRecord,
|
||||
onNavigationClick: () -> Unit,
|
||||
onShareClick: (InAppPaymentReceiptRecord) -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_details),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val formattedAmount = remember(record.amount) {
|
||||
FiatMoneyUtil.format(resources, record.amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_type),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(top = 24.dp, bottom = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formattedAmount,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__backup_type),
|
||||
label = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup)
|
||||
)
|
||||
|
||||
val formattedDate = remember(record.timestamp) {
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), record.timestamp)
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__date_paid),
|
||||
label = formattedDate
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = { onShareClick(record) },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(bottom = 24.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__share))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun PaymentHistoryContentPreview() {
|
||||
Previews.Preview {
|
||||
PaymentHistoryContent(
|
||||
state = RemoteBackupsPaymentHistoryState(
|
||||
records = persistentMapOf(
|
||||
1L to testRecord()
|
||||
)
|
||||
),
|
||||
onNavigationClick = {},
|
||||
onRecordClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun PaymentHistoryRowPreview() {
|
||||
Previews.Preview {
|
||||
PaymentHistoryRow(
|
||||
record = testRecord(),
|
||||
onRecordClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun PaymentDetailsContentPreview() {
|
||||
Previews.Preview {
|
||||
PaymentHistoryDetails(
|
||||
record = testRecord(),
|
||||
onNavigationClick = {},
|
||||
onShareClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun testRecord(): InAppPaymentReceiptRecord {
|
||||
return InAppPaymentReceiptRecord(
|
||||
id = 1,
|
||||
amount = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
timestamp = 1718739691000,
|
||||
type = InAppPaymentReceiptRecord.Type.RECURRING_BACKUP,
|
||||
subscriptionLevel = 201
|
||||
)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||
|
||||
object RemoteBackupsPaymentHistoryRepository {
|
||||
|
||||
fun getReceipts(): List<InAppPaymentReceiptRecord> {
|
||||
return SignalDatabase.donationReceipts.getReceipts(InAppPaymentReceiptRecord.Type.RECURRING_BACKUP)
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||
|
||||
@Stable
|
||||
data class RemoteBackupsPaymentHistoryState(
|
||||
val records: PersistentMap<Long, InAppPaymentReceiptRecord> = persistentMapOf(),
|
||||
val displayProgressDialog: Boolean = false
|
||||
)
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups.history
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RemoteBackupsPaymentHistoryViewModel : ViewModel() {
|
||||
|
||||
private val internalStateFlow = MutableStateFlow(RemoteBackupsPaymentHistoryState())
|
||||
val state: StateFlow<RemoteBackupsPaymentHistoryState> = internalStateFlow
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val receipts = withContext(Dispatchers.IO) {
|
||||
RemoteBackupsPaymentHistoryRepository.getReceipts()
|
||||
}
|
||||
|
||||
internalStateFlow.update { state -> state.copy(records = receipts.associateBy { it.id }.toPersistentMap()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStartRenderingBitmap() {
|
||||
internalStateFlow.update { it.copy(displayProgressDialog = true) }
|
||||
}
|
||||
|
||||
fun onEndRenderingBitmap() {
|
||||
internalStateFlow.update { it.copy(displayProgressDialog = false) }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
@ -46,18 +47,26 @@ object RecurringInAppPaymentRepository {
|
|||
private val donationsService = AppDependencies.donationsService
|
||||
|
||||
fun getActiveSubscription(type: InAppPaymentSubscriberRecord.Type): Single<ActiveSubscription> {
|
||||
val localSubscription = InAppPaymentsRepository.getSubscriber(type)
|
||||
return if (localSubscription != null) {
|
||||
Single.fromCallable { donationsService.getSubscription(localSubscription.subscriberId) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<ActiveSubscription>::flattenResult)
|
||||
.doOnSuccess { activeSubscription ->
|
||||
if (activeSubscription.isActive && activeSubscription.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) {
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Single.just(ActiveSubscription.EMPTY)
|
||||
return Single.fromCallable {
|
||||
getActiveSubscriptionSync(type).getOrThrow()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): Result<ActiveSubscription> {
|
||||
val response = InAppPaymentsRepository.getSubscriber(type)?.let {
|
||||
donationsService.getSubscription(it.subscriberId)
|
||||
} ?: return Result.success(ActiveSubscription.EMPTY)
|
||||
|
||||
return try {
|
||||
val result = response.resultOrThrow
|
||||
if (result.isActive && result.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) {
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
}
|
||||
|
||||
Result.success(result)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,12 @@ import android.graphics.Typeface
|
|||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextPaint
|
||||
import android.text.style.MetricAffectingSpan
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.withStyle
|
||||
|
||||
/**
|
||||
* Helper object for working with the SignalSymbols font
|
||||
|
@ -17,6 +23,7 @@ import android.text.style.MetricAffectingSpan
|
|||
object SignalSymbols {
|
||||
|
||||
enum class Glyph(val unicode: Char) {
|
||||
CHECKMARK('\u2713'),
|
||||
CHEVRON_RIGHT('\uE025'),
|
||||
PERSON_CIRCLE('\uE05E')
|
||||
}
|
||||
|
@ -43,6 +50,17 @@ object SignalSymbols {
|
|||
return text
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnnotatedString.Builder.SignalSymbol(weight: Weight, glyph: Glyph) {
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontFamily = FontFamily(getTypeface(LocalContext.current, weight))
|
||||
)
|
||||
) {
|
||||
append(glyph.unicode.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTypeface(context: Context, weight: Weight): Typeface {
|
||||
return when (weight) {
|
||||
Weight.BOLD -> getBoldWeightedFont(context)
|
||||
|
|
|
@ -258,6 +258,7 @@ class InAppPaymentRedemptionJob private constructor(
|
|||
Log.i(TAG, "Enabling backups and setting backup tier to PAID", true)
|
||||
SignalStore.backup.areBackupsEnabled = true
|
||||
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -516,6 +516,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
|
||||
SignalStore.backup.areBackupsEnabled = true
|
||||
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
}
|
||||
|
||||
val subscriber = InAppPaymentsRepository.requireSubscriber(subscriberType)
|
||||
|
|
|
@ -27,6 +27,7 @@ public class UiHintValues extends SignalStoreValues {
|
|||
private static final String DISMISSED_CONTACTS_PERMISSION_BANNER = "uihints.dismissed_contacts_permission_banner";
|
||||
private static final String HAS_SEEN_DELETE_SYNC_EDUCATION_SHEET = "uihints.has_seen_delete_sync_education_sheet";
|
||||
private static final String LAST_SUPPORT_VERSION_SEEN = "uihints.last_support_version_seen";
|
||||
private static final String HAS_EVER_ENABLED_REMOTE_BACKUPS = "uihints.has_ever_enabled_remote_backups";
|
||||
|
||||
UiHintValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
|
@ -39,7 +40,7 @@ public class UiHintValues extends SignalStoreValues {
|
|||
|
||||
@Override
|
||||
@NonNull List<String> getKeysToIncludeInBackup() {
|
||||
return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_COMPLETED_USERNAME_ONBOARDING, HAS_SEEN_TEXT_FORMATTING_ALERT);
|
||||
return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_COMPLETED_USERNAME_ONBOARDING, HAS_SEEN_TEXT_FORMATTING_ALERT, HAS_EVER_ENABLED_REMOTE_BACKUPS);
|
||||
}
|
||||
|
||||
public void markHasSeenGroupSettingsMenuToast() {
|
||||
|
@ -200,4 +201,12 @@ public class UiHintValues extends SignalStoreValues {
|
|||
public void setLastSupportVersionSeen(int version) {
|
||||
putInteger(LAST_SUPPORT_VERSION_SEEN, version);
|
||||
}
|
||||
|
||||
public void markHasEverEnabledRemoteBackups() {
|
||||
putBoolean(HAS_EVER_ENABLED_REMOTE_BACKUPS, true);
|
||||
}
|
||||
|
||||
public boolean getHasEverEnabledRemoteBackups() {
|
||||
return getBoolean(HAS_EVER_ENABLED_REMOTE_BACKUPS, false);
|
||||
}
|
||||
}
|
||||
|
|
12
app/src/main/res/drawable/symbol_backup_24.xml
Normal file
12
app/src/main/res/drawable/symbol_backup_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<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="M12 2.88c-5.04 0-9.13 4.08-9.13 9.12 0 2.34 0.88 4.47 2.33 6.09l0.48-0.48c0.41-0.4 1.11-0.21 1.26 0.35l0.76 2.96c0.14 0.56-0.37 1.06-0.92 0.92l-2.96-0.76c-0.57-0.15-0.76-0.85-0.35-1.26l0.5-0.5C2.2 17.4 1.11 14.83 1.11 12 1.13 6 6 1.12 12 1.12 18 1.13 22.88 6 22.88 12c0 6-4.87 10.88-10.88 10.88-0.48 0-0.88-0.4-0.88-0.88s0.4-0.88 0.88-0.88c5.04 0 9.13-4.08 9.13-9.12 0-5.04-4.09-9.13-9.13-9.13Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.74 5.23c0.02-0.4 0.35-0.73 0.76-0.73 0.4 0 0.74 0.32 0.76 0.73l0.2 6.31 4.31 0.2c0.41 0.02 0.73 0.35 0.73 0.76 0 0.4-0.32 0.74-0.73 0.76l-5.2 0.24H11.5c-0.55 0-1-0.45-1-1v-0.06l0.24-7.2Z"/>
|
||||
</vector>
|
|
@ -29,6 +29,13 @@
|
|||
android:defaultValue="true"
|
||||
app:argType="boolean" />
|
||||
</action>
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_backupsSettingsFragment"
|
||||
app:destination="@id/backupsSettingsFragment"
|
||||
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
|
||||
android:id="@+id/action_appSettingsFragment_to_accountSettingsFragment"
|
||||
app:destination="@id/accountSettingsFragment"
|
||||
|
@ -960,20 +967,26 @@
|
|||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/remoteBackupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.RemoteBackupsSettingsFragment">
|
||||
android:id="@+id/backupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.BackupsSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
|
||||
app:destination="@id/backupsTypeSettingsFragment"
|
||||
android:id="@+id/action_backupsSettingsFragment_to_backupsPreferenceFragment"
|
||||
app:destination="@id/backupsPreferenceFragment"
|
||||
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/remoteBackupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_remoteBackupsPaymentHistoryFragment"
|
||||
app:destination="@id/remoteBackupsPaymentHistoryFragment"
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
|
||||
app:destination="@id/backupsTypeSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
|
@ -987,21 +1000,10 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/backupsTypeSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment">
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_backupsTypeSettingsFragment_to_remoteBackupsPaymentHistoryFragment"
|
||||
app:destination="@id/remoteBackupsPaymentHistoryFragment"
|
||||
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/remoteBackupsPaymentHistoryFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.history.RemoteBackupsPaymentHistoryFragment" />
|
||||
|
||||
<include app:graph="@navigation/username_link_settings" />
|
||||
<include app:graph="@navigation/story_privacy_settings" />
|
||||
|
||||
|
|
|
@ -10,6 +10,13 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.AppSettingsFragment"
|
||||
android:label="app_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_backupsSettingsFragment"
|
||||
app:destination="@id/backupsSettingsFragment"
|
||||
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
|
||||
android:id="@+id/action_appSettingsFragment_to_usernameLinkSettingsFragment"
|
||||
app:destination="@id/username_link_settings"
|
||||
|
@ -966,20 +973,34 @@
|
|||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/remoteBackupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.RemoteBackupsSettingsFragment">
|
||||
android:id="@+id/backupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.BackupsSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
|
||||
app:destination="@id/backupsTypeSettingsFragment"
|
||||
android:id="@+id/action_backupsSettingsFragment_to_backupsPreferenceFragment"
|
||||
app:destination="@id/backupsPreferenceFragment"
|
||||
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
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_remoteBackupsPaymentHistoryFragment"
|
||||
app:destination="@id/remoteBackupsPaymentHistoryFragment"
|
||||
android:id="@+id/action_backupsSettingsFragment_to_remoteBackupsSettingsFragment"
|
||||
app:destination="@id/remoteBackupsSettingsFragment"
|
||||
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/remoteBackupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.RemoteBackupsSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
|
||||
app:destination="@id/backupsTypeSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
|
@ -991,21 +1012,10 @@
|
|||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/remoteBackupsPaymentHistoryFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.history.RemoteBackupsPaymentHistoryFragment"></fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/backupsTypeSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment">
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_backupsTypeSettingsFragment_to_remoteBackupsPaymentHistoryFragment"
|
||||
app:destination="@id/remoteBackupsPaymentHistoryFragment"
|
||||
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>
|
||||
|
||||
<include app:graph="@navigation/username_link_settings" />
|
||||
|
|
|
@ -7336,19 +7336,15 @@
|
|||
<!-- Educational bottom sheet confirm/dismiss button text shown to notify about delete syncs causing deletes to happen across all devices -->
|
||||
<string name="DeleteSyncEducation_acknowledge_button">OK</string>
|
||||
|
||||
<!-- RemoteBackupsPaymentHistoryFragment -->
|
||||
<!-- Title of the screen for payment history -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__payment_history">Payment history</string>
|
||||
<!-- Description for backup rows -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup">Text and all media backup</string>
|
||||
<!-- Title of the screen for payment details -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__payment_details">Payment details</string>
|
||||
<!-- Title of row specifying the type of backup -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__backup_type">Backup type</string>
|
||||
<!-- Title of row specifying the date the backup was paid on -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__date_paid">Date paid</string>
|
||||
<!-- Button label to share the receipt -->
|
||||
<string name="RemoteBackupsPaymentHistoryFragment__share">Share</string>
|
||||
<!-- BackupsSettingsFragment -->
|
||||
<!-- Subtitle for row for active backup, first placeholder is formatted amount, second is renewal date -->
|
||||
<string name="BackupsSettingsFragment_s_month_renews_s">%1$s/month, renews %2$s</string>
|
||||
<!-- Subtitle for row for active backup, placeholder is last date of backup -->
|
||||
<string name="BackupsSettingsFragment_last_backup_s">Last backup %1$s</string>
|
||||
<!-- Subtitle for row for no backup ever created -->
|
||||
<string name="BackupsSettingsFragment_automatic_backups_with_signals">Automatic backups with Signal\'s secure end-to-end encrypted storage service.</string>
|
||||
<!-- Action button label to set up backups -->
|
||||
<string name="BackupsSettingsFragment_set_up">Set up</string>
|
||||
|
||||
<!-- RemoteBackupsSettingsFragment -->
|
||||
<!-- Displayed on the title bar -->
|
||||
|
@ -7376,13 +7372,27 @@
|
|||
<!-- Snackbar text displayed when backup will be created overnight -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_will_be_created_overnight">Backup will be created overnight.</string>
|
||||
<!-- Title text in row detailing selected backup type -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_type">Backup type</string>
|
||||
<string name="RemoteBackupsSettingsFragment__backup_plan">Backup plan</string>
|
||||
<!-- Subtitle text in row detailing selected backup type displayed when backups are disabled -->
|
||||
<string name="RemoteBackupsSettingsFragment__backups_disabled">Backups disabled</string>
|
||||
<!-- Format string for backup and cost. First placeholder is backup title, second is cost per month. -->
|
||||
<string name="RemoteBackupsSettingsFragment__s_dot_s_per_month">%1$s · %2$s/month</string>
|
||||
<!-- Button label to enable backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__enable_backups">Enable backups</string>
|
||||
<!-- Format string for backup and cost. First placeholder is cost per month. -->
|
||||
<string name="RemoteBackupsSettingsFragment__s_per_month">%1$s/month</string>
|
||||
<!-- String for free backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__your_backup_plan_is_free">Your backup plan is free</string>
|
||||
<!-- Displays the date the subscription will renew -->
|
||||
<string name="RemoteBackupsSettingsFragment__renews_s">Renews %1$s</string>
|
||||
<!-- Displayed at the top of the screen when no backup is currently enabled -->
|
||||
<string name="RemoteBackupsSettingsFragment__back_up_your_message_history">Back up your message history so you never lose data when you get a new phone or reinstall Signal.</string>
|
||||
<!-- Section header for other ways to back up -->
|
||||
<string name="RemoteBackupsSettingsFragment__other_ways_to_backup">Other ways to backup</string>
|
||||
<!-- Row title for performing on-device backup -->
|
||||
<string name="RemoteBackupsSettingsFragment__on_device_backups">On-device backups</string>
|
||||
<!-- Row label for performing on-device backup -->
|
||||
<string name="RemoteBackupsSettingsFragment__save_your_backups_to">Save your backups to a folder on this device</string>
|
||||
<!-- Button label to manage or cancel backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__manage_or_cancel">Manage or cancel</string>
|
||||
<!-- button label to upgrade backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__upgrade">Upgrade</string>
|
||||
<!-- Progress indicator subtext displayed when a backup is in progress -->
|
||||
<string name="RemoteBackupsSettingsFragment__d_slash_d">%1$d/%2$d</string>
|
||||
<!-- Title text in row detailing last backup -->
|
||||
|
|
Loading…
Add table
Reference in a new issue