diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt index 1c5edbb589..725d497a36 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt @@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.backup.v2.ui import android.os.Parcelable import androidx.annotation.StringRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize @@ -20,10 +22,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -39,12 +46,13 @@ import org.signal.core.ui.Buttons import org.signal.core.ui.Previews import org.signal.core.ui.SignalPreview import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment -import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.BackupMessagesJob import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob +import org.thoughtcrime.securesms.payments.FiatMoneyUtil /** * Notifies the user of an issue with their backup. @@ -67,6 +75,14 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { @Composable override fun SheetContent() { + var pricePerMonth by remember { mutableStateOf("-") } + val resources = LocalContext.current.resources + + LaunchedEffect(Unit) { + val price = AppDependencies.billingApi.queryProduct()?.price ?: return@LaunchedEffect + pricePerMonth = FiatMoneyUtil.format(resources, price, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) + } + BackupAlertSheetContent( backupAlert = backupAlert, onPrimaryActionClick = this::performPrimaryAction, @@ -81,10 +97,12 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { BackupMessagesJob.enqueue() startActivity(AppSettingsActivity.remoteBackups(requireContext())) } - BackupAlert.PAYMENT_PROCESSING -> Unit + + BackupAlert.PAYMENT_PROCESSING -> launchManageBackupsSubscription() BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { performFullMediaDownload() } + BackupAlert.DISK_FULL -> Unit } @@ -97,13 +115,16 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { BackupAlert.COULD_NOT_COMPLETE_BACKUP -> { // TODO [message-backups] - Dismiss and notify later } - BackupAlert.PAYMENT_PROCESSING -> error("PAYMENT_PROCESSING state does not support a secondary action.") + + BackupAlert.PAYMENT_PROCESSING -> Unit BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { // TODO [message-backups] - Silence and remind on last day } + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { displayLastChanceDialog() } + BackupAlert.DISK_FULL -> Unit } @@ -130,8 +151,9 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { @Composable private fun BackupAlertSheetContent( backupAlert: BackupAlert, - onPrimaryActionClick: () -> Unit, - onSecondaryActionClick: () -> Unit + pricePerMonth: String = "", + onPrimaryActionClick: () -> Unit = {}, + onSecondaryActionClick: () -> Unit = {} ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -143,20 +165,43 @@ private fun BackupAlertSheetContent( Spacer(modifier = Modifier.size(26.dp)) - val iconColors = rememberBackupsIconColors(backupAlert = backupAlert) - Icon( - painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] final asset - contentDescription = null, - tint = iconColors.foreground, - modifier = Modifier - .size(88.dp) - .background(color = iconColors.background, shape = CircleShape) - .padding(20.dp) - ) + when (backupAlert) { + BackupAlert.PAYMENT_PROCESSING, BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { + Box { + Image( + painter = painterResource(id = R.drawable.image_signal_backups), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .padding(2.dp) + ) + Icon( + painter = painterResource(R.drawable.symbol_error_circle_fill_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.align(Alignment.TopEnd) + ) + } + } + + else -> { + val iconColors = rememberBackupsIconColors(backupAlert = backupAlert) + Icon( + painter = painterResource(id = R.drawable.symbol_backup_light), + contentDescription = null, + tint = iconColors.foreground, + modifier = Modifier + .size(80.dp) + .background(color = iconColors.background, shape = CircleShape) + .padding(20.dp) + ) + } + } Text( text = stringResource(id = rememberTitleResource(backupAlert = backupAlert)), style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) ) @@ -164,9 +209,8 @@ private fun BackupAlertSheetContent( BackupAlert.COULD_NOT_COMPLETE_BACKUP -> CouldNotCompleteBackup( daysSinceLastBackup = 7 // TODO [message-backups] ) - BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody( - paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PAY // TODO [message-backups] -- Get this data from elsewhere... The active subscription object? - ) + + BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody() BackupAlert.MEDIA_BACKUPS_ARE_OFF -> MediaBackupsAreOffBody(30) // TODO [message-backups] -- Get this value from backend BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> MediaWillBeDeletedTodayBody() BackupAlert.DISK_FULL -> DiskFullBody( @@ -184,7 +228,7 @@ private fun BackupAlertSheetContent( .defaultMinSize(minWidth = 220.dp) .padding(bottom = padBottom) ) { - Text(text = stringResource(id = rememberPrimaryActionResource(backupAlert = backupAlert))) + Text(text = primaryActionString(backupAlert = backupAlert, pricePerMonth = pricePerMonth)) } if (secondaryActionResource > 0) { @@ -201,15 +245,18 @@ private fun CouldNotCompleteBackup( ) { Text( text = stringResource(id = R.string.BackupAlertBottomSheet__your_device_hasnt, daysSinceLastBackup), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 60.dp) ) } @Composable -private fun PaymentProcessingBody(paymentMethodType: InAppPaymentData.PaymentMethodType) { +private fun PaymentProcessingBody() { Text( - text = stringResource(id = R.string.BackupAlertBottomSheet__were_having_trouble_collecting__google_pay), + text = stringResource(id = R.string.BackupAlertBottomSheet__check_to_make_sure_your_payment_method), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 60.dp) ) } @@ -219,14 +266,16 @@ private fun MediaBackupsAreOffBody( daysUntilDeletion: Long ) { Text( - text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__your_signal_media_backup_plan, daysUntilDeletion.toInt(), daysUntilDeletion), + text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__your_backup_plan_has_expired, daysUntilDeletion.toInt(), daysUntilDeletion), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 24.dp) ) Text( text = stringResource(id = R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 36.dp) ) } @@ -236,12 +285,14 @@ private fun MediaWillBeDeletedTodayBody() { Text( text = stringResource(id = R.string.BackupAlertBottomSheet__your_signal_media_backup_plan_has_been), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 24.dp) ) Text( text = stringResource(id = R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 36.dp) ) } @@ -254,12 +305,14 @@ private fun DiskFullBody( Text( text = stringResource(id = R.string.BackupAlertBottomSheet__your_device_does_not_have_enough_free_space, requiredSpace), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 24.dp) ) Text( text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__if_you_choose_skip, daysUntilDeletion.toInt(), daysUntilDeletion), // TODO [message-backups] Learn More link textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 36.dp) ) } @@ -268,8 +321,9 @@ private fun DiskFullBody( private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors { return remember(backupAlert) { when (backupAlert) { - BackupAlert.COULD_NOT_COMPLETE_BACKUP, BackupAlert.PAYMENT_PROCESSING, BackupAlert.DISK_FULL -> BackupsIconColors.Warning - BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error + BackupAlert.PAYMENT_PROCESSING, BackupAlert.MEDIA_BACKUPS_ARE_OFF -> error("Not icon-based options.") + BackupAlert.COULD_NOT_COMPLETE_BACKUP, BackupAlert.DISK_FULL -> BackupsIconColors.Warning + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error } } } @@ -280,8 +334,8 @@ private fun rememberTitleResource(backupAlert: BackupAlert): Int { return remember(backupAlert) { when (backupAlert) { BackupAlert.COULD_NOT_COMPLETE_BACKUP -> R.string.BackupAlertBottomSheet__couldnt_complete_backup - BackupAlert.PAYMENT_PROCESSING -> R.string.BackupAlertBottomSheet__cant_process_backup_payment - BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__media_backups_are_off + BackupAlert.PAYMENT_PROCESSING -> R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__your_backups_subscription_expired BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today BackupAlert.DISK_FULL -> R.string.BackupAlertBottomSheet__cant_complete_download } @@ -289,15 +343,16 @@ private fun rememberTitleResource(backupAlert: BackupAlert): Int { } @Composable -private fun rememberPrimaryActionResource(backupAlert: BackupAlert): Int { - return remember(backupAlert) { - when (backupAlert) { - BackupAlert.COULD_NOT_COMPLETE_BACKUP -> android.R.string.ok // TODO [message-backups] -- Finalized copy - BackupAlert.PAYMENT_PROCESSING -> android.R.string.ok - BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__download_media_now - BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.BackupAlertBottomSheet__download_media_now - BackupAlert.DISK_FULL -> android.R.string.ok - } +private fun primaryActionString( + backupAlert: BackupAlert, + pricePerMonth: String +): String { + return when (backupAlert) { + BackupAlert.COULD_NOT_COMPLETE_BACKUP -> stringResource(android.R.string.ok) // TODO [message-backups] -- Finalized copy + BackupAlert.PAYMENT_PROCESSING -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription) + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth) + BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> stringResource(R.string.BackupAlertBottomSheet__download_media_now) + BackupAlert.DISK_FULL -> stringResource(android.R.string.ok) } } @@ -306,8 +361,8 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int { return remember(backupAlert) { when (backupAlert) { BackupAlert.COULD_NOT_COMPLETE_BACKUP -> android.R.string.cancel // TODO [message-backups] -- Finalized copy - BackupAlert.PAYMENT_PROCESSING -> -1 - BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__download_later + BackupAlert.PAYMENT_PROCESSING -> R.string.BackupAlertBottomSheet__not_now + BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__not_now BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.BackupAlertBottomSheet__dont_download_media BackupAlert.DISK_FULL -> R.string.BackupAlertBottomSheet__skip } @@ -319,9 +374,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int { private fun BackupAlertSheetContentPreviewGeneric() { Previews.BottomSheetPreview { BackupAlertSheetContent( - backupAlert = BackupAlert.COULD_NOT_COMPLETE_BACKUP, - onPrimaryActionClick = {}, - onSecondaryActionClick = {} + backupAlert = BackupAlert.COULD_NOT_COMPLETE_BACKUP ) } } @@ -331,9 +384,7 @@ private fun BackupAlertSheetContentPreviewGeneric() { private fun BackupAlertSheetContentPreviewPayment() { Previews.BottomSheetPreview { BackupAlertSheetContent( - backupAlert = BackupAlert.PAYMENT_PROCESSING, - onPrimaryActionClick = {}, - onSecondaryActionClick = {} + backupAlert = BackupAlert.PAYMENT_PROCESSING ) } } @@ -344,8 +395,7 @@ private fun BackupAlertSheetContentPreviewMedia() { Previews.BottomSheetPreview { BackupAlertSheetContent( backupAlert = BackupAlert.MEDIA_BACKUPS_ARE_OFF, - onPrimaryActionClick = {}, - onSecondaryActionClick = {} + pricePerMonth = "$2.99" ) } } @@ -355,9 +405,7 @@ private fun BackupAlertSheetContentPreviewMedia() { private fun BackupAlertSheetContentPreviewDelete() { Previews.BottomSheetPreview { BackupAlertSheetContent( - backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY, - onPrimaryActionClick = {}, - onSecondaryActionClick = {} + backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY ) } } @@ -367,9 +415,7 @@ private fun BackupAlertSheetContentPreviewDelete() { private fun BackupAlertSheetContentPreviewDiskFull() { Previews.BottomSheetPreview { BackupAlertSheetContent( - backupAlert = BackupAlert.DISK_FULL, - onPrimaryActionClick = {}, - onSecondaryActionClick = {} + backupAlert = BackupAlert.DISK_FULL ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt index 059a12a16c..cf311dff57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupsIconColors.kt @@ -16,6 +16,11 @@ sealed interface BackupsIconColors { @get:Composable val background: Color + data object None : BackupsIconColors { + override val foreground: Color @Composable get() = error("No coloring should be applied.") + override val background: Color @Composable get() = error("No coloring should be applied.") + } + data object Normal : BackupsIconColors { override val foreground: Color @Composable get() = MaterialTheme.colorScheme.onSurface override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/CreateBackupBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/CreateBackupBottomSheet.kt index ac7607d35a..ffebd3920d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/CreateBackupBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/CreateBackupBottomSheet.kt @@ -6,15 +6,12 @@ package org.thoughtcrime.securesms.backup.v2.ui import android.content.DialogInterface -import androidx.compose.foundation.background +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -87,26 +84,24 @@ private fun CreateBackupBottomSheetContent( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() + .padding(horizontal = dimensionResource(R.dimen.core_ui__gutter)) + .padding(bottom = 24.dp) ) { BottomSheets.Handle() - Icon( - painter = painterResource(id = R.drawable.symbol_backup_light), - tint = BackupsIconColors.Normal.foreground, + Image( + painter = painterResource(id = R.drawable.image_signal_backups), contentDescription = null, modifier = Modifier .padding(top = 18.dp, bottom = 11.dp) - .size(88.dp) - .background( - color = BackupsIconColors.Normal.background, - shape = CircleShape - ) - .padding(20.dp) + .size(80.dp) + .padding(4.dp) ) Text( - text = stringResource(id = R.string.CreateBackupBottomSheet__create_backup), - style = MaterialTheme.typography.titleLarge + text = stringResource(id = R.string.CreateBackupBottomSheet__you_are_all_set), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center ) Text( @@ -116,33 +111,24 @@ private fun CreateBackupBottomSheetContent( textAlign = TextAlign.Center, modifier = Modifier .padding(top = 8.dp, bottom = 64.dp) - .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 31.dp) + Buttons.LargeTonal( + onClick = onBackupNowClick, + modifier = Modifier.widthIn(min = 220.dp) ) { - TextButton( - onClick = onBackupLaterClick, - modifier = Modifier.padding(start = dimensionResource(id = R.dimen.core_ui__gutter)) - ) { - Text( - text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_later) - ) - } + Text( + text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_now) + ) + } - Spacer(modifier = Modifier.weight(1f)) - - Buttons.LargeTonal( - onClick = onBackupNowClick, - modifier = Modifier.padding(end = dimensionResource(id = R.dimen.core_ui__gutter)) - ) { - Text( - text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_now) - ) - } + TextButton( + onClick = onBackupLaterClick, + modifier = Modifier.widthIn(min = 220.dp).padding(top = 16.dp) + ) { + Text( + text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_later) + ) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt index c8d3603fd2..bcf1e958d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -15,7 +14,6 @@ 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.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -33,7 +31,6 @@ import androidx.compose.ui.unit.dp import org.signal.core.ui.Buttons import org.signal.core.ui.Previews import org.signal.core.ui.Scaffolds -import org.signal.core.ui.theme.SignalTheme import org.thoughtcrime.securesms.R /** @@ -65,11 +62,11 @@ fun MessageBackupsEducationScreen( ) { item { Image( - painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Final image asset + painter = painterResource(id = R.drawable.image_signal_backups), // TODO [message-backups] Final image asset contentDescription = null, modifier = Modifier - .padding(top = 48.dp) - .size(88.dp) + .padding(top = 24.dp) + .size(80.dp) ) } @@ -175,7 +172,6 @@ private fun NotableFeatureRow( modifier = Modifier .padding(end = 8.dp) .size(32.dp) - .background(color = SignalTheme.colors.colorSurface2, shape = CircleShape) .padding(6.dp) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyEducationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyEducationScreen.kt index 72024c0ac7..580c90ef8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyEducationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyEducationScreen.kt @@ -5,15 +5,13 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription -import androidx.compose.foundation.background +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -50,18 +48,12 @@ fun MessageBackupsKeyEducationScreen( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - Icon( - painter = painterResource(R.drawable.symbol_key_24), + Image( + painter = painterResource(R.drawable.image_signal_backups_key), contentDescription = null, - tint = MaterialTheme.colorScheme.primary, modifier = Modifier .padding(top = 24.dp) .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ) - .padding(16.dp) ) Text( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index b59ba7d878..39244f3ebc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,11 +14,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -80,18 +79,12 @@ fun MessageBackupsKeyRecordScreen( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - Icon( - painter = painterResource(R.drawable.symbol_lock_24), + Image( + painter = painterResource(R.drawable.image_signal_backups_lock), contentDescription = null, - tint = MaterialTheme.colorScheme.primary, modifier = Modifier .padding(top = 24.dp) .size(80.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ) - .padding(16.dp) ) Text( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt index fc872e7bdb..a2ec724f98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt @@ -93,7 +93,7 @@ fun MessageBackupsTypeSelectionScreen( ) { item { Image( - painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized art asset + painter = painterResource(id = R.drawable.image_signal_backups_plans), contentDescription = null, modifier = Modifier.size(88.dp) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingExtensions.kt new file mode 100644 index 0000000000..cdb5702303 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/billing/GooglePlayBillingExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.billing + +import android.content.Intent +import android.net.Uri +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies + +/** + * Launches user to the Google Play backups management screen. + */ +fun Fragment.launchManageBackupsSubscription() { + lifecycleScope.launch(Dispatchers.Main) { + val uri = withContext(Dispatchers.Default) { + Uri.parse( + getString( + R.string.backup_subscription_management_url, + GooglePlayBillingDependencies.getProductId(), + requireContext().applicationInfo.packageName + ) + ) + } + + val intent = Intent(Intent.ACTION_VIEW, uri) + startActivity(intent) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index f7417a5d71..668d46ac7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -5,19 +5,19 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -54,11 +55,8 @@ 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.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import org.signal.core.ui.Buttons import org.signal.core.ui.Dialogs import org.signal.core.ui.Dividers @@ -77,9 +75,9 @@ 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.billing.launchManageBackupsSubscription import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.ComposeFragment -import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies import org.thoughtcrime.securesms.fonts.SignalSymbols import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState @@ -143,18 +141,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { override fun onBackupTypeActionClick(tier: MessageBackupTier) { when (tier) { MessageBackupTier.FREE -> checkoutLauncher.launch(MessageBackupTier.PAID) - MessageBackupTier.PAID -> lifecycleScope.launch(Dispatchers.Main) { - val uri = Uri.parse( - getString( - R.string.backup_subscription_management_url, - GooglePlayBillingDependencies.getProductId(), - requireContext().applicationInfo.packageName - ) - ) - - val intent = Intent(Intent.ACTION_VIEW, uri) - startActivity(intent) - } + MessageBackupTier.PAID -> launchManageBackupsSubscription() } } @@ -457,7 +444,7 @@ private fun BackupTypeRow( .background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp)) .padding(24.dp) ) { - Row { + Row(modifier = Modifier.fillMaxWidth()) { Column { val title = when (messageBackupsType) { is MessageBackupsType.Paid -> stringResource(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media) @@ -493,7 +480,13 @@ private fun BackupTypeRow( } } - // Icon + Spacer(modifier = Modifier.weight(1f)) + + Image( + painter = painterResource(R.drawable.image_signal_backups), + contentDescription = null, + modifier = Modifier.size(64.dp) + ) } val buttonText = when (messageBackupsType) { @@ -504,7 +497,8 @@ private fun BackupTypeRow( Buttons.LargeTonal( onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }, colors = ButtonDefaults.filledTonalButtonColors().copy( - containerColor = SignalTheme.colors.colorTransparent5 + containerColor = SignalTheme.colors.colorTransparent5, + contentColor = colorResource(R.color.signal_light_colorOnSurface) ), modifier = Modifier.padding(top = 12.dp) ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt index 45d7048f94..1c72698639 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt @@ -8,20 +8,20 @@ package org.thoughtcrime.securesms.components.settings.app.storage import android.os.Bundle import android.view.View import androidx.activity.result.ActivityResultLauncher -import androidx.compose.foundation.background +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -34,12 +34,12 @@ import org.signal.core.ui.Previews import org.signal.core.ui.SignalPreview import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.MessageBackupTier -import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock import org.thoughtcrime.securesms.backup.v2.ui.subscription.testBackupTypes import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.payments.FiatMoneyUtil /** * Sheet describing how users must upgrade to enable optimized storage. @@ -75,7 +75,7 @@ class UpgradeToEnableOptimizedStorageSheet : ComposeBottomSheetDialogFragment() @Composable private fun UpgradeToEnableOptimizedStorageSheetContent( - messageBackupsType: MessageBackupsType?, + messageBackupsType: MessageBackupsType.Paid?, onUpgradeNowClick: () -> Unit = {}, onCancelClick: () -> Unit = {} ) { @@ -90,18 +90,12 @@ private fun UpgradeToEnableOptimizedStorageSheetContent( ) { BottomSheets.Handle() - Icon( - painter = painterResource(id = R.drawable.symbol_backup_light), + Image( + painter = painterResource(id = R.drawable.image_signal_backups), contentDescription = null, - tint = BackupsIconColors.Normal.foreground, modifier = Modifier - .padding(top = 8.dp, bottom = 12.dp) - .size(88.dp) - .background( - color = BackupsIconColors.Normal.background, - shape = CircleShape - ) - .padding(20.dp) + .padding(top = 8.dp, bottom = 24.dp) + .size(80.dp) ) Text( @@ -140,8 +134,13 @@ private fun UpgradeToEnableOptimizedStorageSheetContent( .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) .padding(bottom = 8.dp) ) { + val resources = LocalContext.current.resources + val formattedPrice = remember(messageBackupsType.pricePerMonth) { + FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) + } + Text( - text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_now) + text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__subscribe_for_s_month, formattedPrice) ) } @@ -164,7 +163,7 @@ private fun UpgradeToEnableOptimizedStorageSheetContent( private fun UpgradeToEnableOptimizedStorageSheetContentPreview() { Previews.BottomSheetPreview { UpgradeToEnableOptimizedStorageSheetContent( - messageBackupsType = testBackupTypes()[1] + messageBackupsType = testBackupTypes()[1] as MessageBackupsType.Paid? ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt index 8bce64105f..6f5ca3f21b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt @@ -16,13 +16,13 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType class UpgradeToEnableOptimizedStorageViewModel : ViewModel() { - private val internalMessageBackupsType = mutableStateOf(null) - val messageBackupsType: State = internalMessageBackupsType + private val internalMessageBackupsType = mutableStateOf(null) + val messageBackupsType: State = internalMessageBackupsType init { viewModelScope.launch { val backupsType = withContext(Dispatchers.IO) { - BackupRepository.getBackupsType(MessageBackupTier.PAID) + BackupRepository.getBackupsType(MessageBackupTier.PAID) as? MessageBackupsType.Paid } withContext(Dispatchers.Main) { diff --git a/app/src/main/res/drawable-night/image_signal_backups.xml b/app/src/main/res/drawable-night/image_signal_backups.xml new file mode 100644 index 0000000000..fef161c8a5 --- /dev/null +++ b/app/src/main/res/drawable-night/image_signal_backups.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-night/image_signal_backups_key.xml b/app/src/main/res/drawable-night/image_signal_backups_key.xml new file mode 100644 index 0000000000..ce806ab2bc --- /dev/null +++ b/app/src/main/res/drawable-night/image_signal_backups_key.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable-night/image_signal_backups_lock.xml b/app/src/main/res/drawable-night/image_signal_backups_lock.xml new file mode 100644 index 0000000000..1d80d9bdbc --- /dev/null +++ b/app/src/main/res/drawable-night/image_signal_backups_lock.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable-night/image_signal_backups_plans.xml b/app/src/main/res/drawable-night/image_signal_backups_plans.xml new file mode 100644 index 0000000000..e95ef99b3e --- /dev/null +++ b/app/src/main/res/drawable-night/image_signal_backups_plans.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/image_signal_backups_subscribed.xml b/app/src/main/res/drawable-night/image_signal_backups_subscribed.xml new file mode 100644 index 0000000000..8f2c2b080a --- /dev/null +++ b/app/src/main/res/drawable-night/image_signal_backups_subscribed.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/image_signal_backups.xml b/app/src/main/res/drawable/image_signal_backups.xml new file mode 100644 index 0000000000..4890c6c807 --- /dev/null +++ b/app/src/main/res/drawable/image_signal_backups.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/image_signal_backups_key.xml b/app/src/main/res/drawable/image_signal_backups_key.xml new file mode 100644 index 0000000000..94d8a43aa2 --- /dev/null +++ b/app/src/main/res/drawable/image_signal_backups_key.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/image_signal_backups_lock.xml b/app/src/main/res/drawable/image_signal_backups_lock.xml new file mode 100644 index 0000000000..4ae3c4425a --- /dev/null +++ b/app/src/main/res/drawable/image_signal_backups_lock.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/image_signal_backups_plans.xml b/app/src/main/res/drawable/image_signal_backups_plans.xml new file mode 100644 index 0000000000..9e73d6c801 --- /dev/null +++ b/app/src/main/res/drawable/image_signal_backups_plans.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/image_signal_backups_subscribed.xml b/app/src/main/res/drawable/image_signal_backups_subscribed.xml new file mode 100644 index 0000000000..57a416971b --- /dev/null +++ b/app/src/main/res/drawable/image_signal_backups_subscribed.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af191a5cc0..a515f60f63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -817,7 +817,7 @@ - Create backup + You\'re all set. Start your backup now. Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the backup takes place. @@ -7239,11 +7239,11 @@ - Media backups are off + Your backups subscription expired - - Your Signal media backup plan has been canceled because we couldn\'t process your payment. You have %1$d day to download any media stored in your backup. After %1$d day, the media in your backup will be deleted. - Your Signal media backup plan has been canceled because we couldn\'t process your payment. You have %1$d days to download any media stored in your backup. After %1$d days, the media in your backup will be deleted. + + Your backup plan has expired because it couldn\'t be renewed with your current payment method. After %1$d day, the media in your backup will be deleted. + Your backup plan has expired because it couldn\'t be renewed with your current payment method. After %1$d days, the media in your backup will be deleted. You can begin paying for backups again at any time to continue backing up all your media. @@ -7261,9 +7261,9 @@ If you choose \"Skip\" the media in your backup will be deleted in %1$d days. - Can\'t process backup payment + Your backups subscription failed to renew - We\'re having trouble collecting your recurring monthly backup payment. Make sure your payment method is up to date. If it isn\'t, update it in Google Pay. Signal will try to process the payment again in a few days. + Check to make sure your payment method is up to date. Tap Manage subscription and under Payment methods tap Update. Couldn\'t complete backup @@ -7272,12 +7272,16 @@ Learn more Skip + + Manage subscription + + Subscribe for %1$s/month Download media now - - Download later Don\'t download media + + Not now Media will be deleted @@ -7358,8 +7362,8 @@ Upgrade to enable this feature Storage optimization can only be used with the paid tier of Signal Backups. Upgrade your backup plan to start using this feature. - - Upgrade now + + Subscribe for %1$s/month Deleting is now synced across all of your devices