Skip to content

Commit 6b50be7

Browse files
alex-signalgreyson-signal
authored andcommitted
Implement start of backups payment integration work.
1 parent 680223c commit 6b50be7

81 files changed

Lines changed: 1492 additions & 1141 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -748,13 +748,6 @@
748748
android:windowSoftInputMode="stateAlwaysHidden"
749749
android:exported="false"/>
750750

751-
<activity
752-
android:name=".backup.v2.ui.subscription.MessageBackupsFlowActivity"
753-
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
754-
android:exported="false"
755-
android:theme="@style/Signal.DayNight.NoActionBar"
756-
android:windowSoftInputMode="adjustResize" />
757-
758751
<activity
759752
android:name=".stories.settings.StorySettingsActivity"
760753
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -803,13 +796,6 @@
803796
android:windowSoftInputMode="stateAlwaysHidden"
804797
android:exported="false"/>
805798

806-
<activity
807-
android:name=".badges.gifts.flow.GiftFlowActivity"
808-
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
809-
android:theme="@style/Signal.DayNight.NoActionBar"
810-
android:windowSoftInputMode="stateAlwaysHidden"
811-
android:exported="false"/>
812-
813799
<activity
814800
android:name=".wallpaper.ChatWallpaperActivity"
815801
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -1120,10 +1106,10 @@
11201106
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
11211107
android:exported="false"/>
11221108

1123-
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
1124-
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
1125-
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
1126-
android:exported="false"/>
1109+
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
1110+
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
1111+
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
1112+
android:exported="false"/>
11271113

11281114
<service
11291115
android:enabled="true"

app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55

66
package org.thoughtcrime.securesms.backup.v2
77

8+
import kotlinx.collections.immutable.persistentListOf
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.withContext
811
import org.greenrobot.eventbus.EventBus
912
import org.signal.core.util.Base64
1013
import org.signal.core.util.EventTimer
11-
import org.signal.core.util.LongSerializer
1214
import org.signal.core.util.logging.Log
15+
import org.signal.core.util.money.FiatMoney
1316
import org.signal.core.util.withinTransaction
1417
import org.signal.libsignal.messagebackup.MessageBackup
1518
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
1619
import org.signal.libsignal.messagebackup.MessageBackupKey
1720
import org.signal.libsignal.protocol.ServiceId.Aci
1821
import org.signal.libsignal.zkgroup.backups.BackupLevel
1922
import org.signal.libsignal.zkgroup.profiles.ProfileKey
23+
import org.thoughtcrime.securesms.R
2024
import org.thoughtcrime.securesms.attachments.Attachment
2125
import org.thoughtcrime.securesms.attachments.AttachmentId
2226
import org.thoughtcrime.securesms.attachments.Cdn
@@ -35,8 +39,11 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
3539
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
3640
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
3741
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
42+
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
43+
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
3844
import org.thoughtcrime.securesms.database.DistributionListTables
3945
import org.thoughtcrime.securesms.database.SignalDatabase
46+
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
4047
import org.thoughtcrime.securesms.dependencies.AppDependencies
4148
import org.thoughtcrime.securesms.groups.GroupId
4249
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
@@ -58,13 +65,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
5865
import org.whispersystems.signalservice.api.push.ServiceId.ACI
5966
import org.whispersystems.signalservice.api.push.ServiceId.PNI
6067
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
68+
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
6169
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
6270
import java.io.ByteArrayOutputStream
6371
import java.io.File
6472
import java.io.InputStream
6573
import java.io.OutputStream
66-
import java.lang.Exception
74+
import java.math.BigDecimal
6775
import java.time.ZonedDateTime
76+
import java.util.Currency
77+
import java.util.Locale
6878
import kotlin.time.Duration.Companion.milliseconds
6979

7080
object BackupRepository {
@@ -673,6 +683,82 @@ object BackupRepository {
673683
}
674684
}
675685

686+
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
687+
return availableBackupTiers.map { getBackupsType(it) }
688+
}
689+
690+
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
691+
val backupCurrency = SignalStore.donationsValues().getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
692+
return when (tier) {
693+
MessageBackupTier.FREE -> getFreeType(backupCurrency)
694+
MessageBackupTier.PAID -> getPaidType(backupCurrency)
695+
}
696+
}
697+
698+
private fun getFreeType(currency: Currency): MessageBackupsType {
699+
return MessageBackupsType(
700+
tier = MessageBackupTier.FREE,
701+
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
702+
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
703+
features = persistentListOf(
704+
MessageBackupsTypeFeature(
705+
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
706+
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
707+
),
708+
MessageBackupsTypeFeature(
709+
iconResourceId = R.drawable.symbol_album_compact_bold_16,
710+
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
711+
)
712+
)
713+
)
714+
}
715+
716+
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
717+
val serviceResponse = withContext(Dispatchers.IO) {
718+
AppDependencies
719+
.donationsService
720+
.getDonationsConfiguration(Locale.getDefault())
721+
}
722+
723+
if (serviceResponse.result.isEmpty) {
724+
if (serviceResponse.applicationError.isPresent) {
725+
throw serviceResponse.applicationError.get()
726+
}
727+
728+
if (serviceResponse.executionError.isPresent) {
729+
throw serviceResponse.executionError.get()
730+
}
731+
732+
error("Unhandled error occurred while downloading configuration.")
733+
}
734+
735+
val config = serviceResponse.result.get()
736+
737+
return MessageBackupsType(
738+
tier = MessageBackupTier.PAID,
739+
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
740+
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
741+
features = persistentListOf(
742+
MessageBackupsTypeFeature(
743+
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
744+
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
745+
),
746+
MessageBackupsTypeFeature(
747+
iconResourceId = R.drawable.symbol_album_compact_bold_16,
748+
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
749+
),
750+
MessageBackupsTypeFeature(
751+
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
752+
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
753+
),
754+
MessageBackupsTypeFeature(
755+
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
756+
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
757+
)
758+
)
759+
)
760+
}
761+
676762
/**
677763
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
678764
* Should be the basis of all backup operations.
@@ -765,18 +851,3 @@ class BackupMetadata(
765851
val usedSpace: Long,
766852
val mediaCount: Long
767853
)
768-
769-
enum class MessageBackupTier(val value: Int) {
770-
FREE(0),
771-
PAID(1);
772-
773-
companion object Serializer : LongSerializer<MessageBackupTier?> {
774-
override fun serialize(data: MessageBackupTier?): Long {
775-
return data?.value?.toLong() ?: -1
776-
}
777-
778-
override fun deserialize(data: Long): MessageBackupTier? {
779-
return values().firstOrNull { it.value == data.toInt() }
780-
}
781-
}
782-
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.thoughtcrime.securesms.backup.v2
7+
8+
import org.signal.core.util.LongSerializer
9+
10+
/**
11+
* Serializable enum value for what we think a user's current backup tier is.
12+
*
13+
* We should not trust the stored value on its own, we should also verify it
14+
* against what the server knows, but it is a useful flag that helps avoid a
15+
* network call in some cases.
16+
*/
17+
enum class MessageBackupTier(val value: Int) {
18+
FREE(0),
19+
PAID(1);
20+
21+
companion object Serializer : LongSerializer<MessageBackupTier?> {
22+
override fun serialize(data: MessageBackupTier?): Long {
23+
return data?.value?.toLong() ?: -1
24+
}
25+
26+
override fun deserialize(data: Long): MessageBackupTier? {
27+
return entries.firstOrNull { it.value == data.toInt() }
28+
}
29+
}
30+
}

app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import androidx.compose.foundation.Image
1111
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
1212
import androidx.compose.foundation.layout.Column
1313
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.navigationBarsPadding
1415
import androidx.compose.foundation.layout.padding
1516
import androidx.compose.foundation.layout.size
1617
import androidx.compose.material3.ExperimentalMaterial3Api
1718
import androidx.compose.material3.Icon
1819
import androidx.compose.material3.MaterialTheme
1920
import androidx.compose.material3.ModalBottomSheet
21+
import androidx.compose.material3.SheetState
2022
import androidx.compose.material3.Text
2123
import androidx.compose.runtime.Composable
2224
import androidx.compose.runtime.remember
@@ -30,49 +32,59 @@ import androidx.compose.ui.tooling.preview.Preview
3032
import androidx.compose.ui.unit.dp
3133
import androidx.compose.ui.viewinterop.AndroidView
3234
import androidx.core.view.updateLayoutParams
35+
import kotlinx.collections.immutable.persistentListOf
3336
import org.signal.core.ui.BottomSheets
3437
import org.signal.core.ui.Buttons
3538
import org.signal.core.ui.Previews
39+
import org.signal.core.util.money.FiatMoney
3640
import org.thoughtcrime.securesms.R
3741
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
3842
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
3943
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
4044
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
4145
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
46+
import java.math.BigDecimal
47+
import java.util.Currency
4248

4349
@OptIn(ExperimentalMaterial3Api::class)
4450
@Composable
4551
fun MessageBackupsCheckoutSheet(
46-
messageBackupTier: MessageBackupTier,
52+
messageBackupsType: MessageBackupsType,
4753
availablePaymentMethods: List<InAppPaymentData.PaymentMethodType>,
54+
sheetState: SheetState,
4855
onDismissRequest: () -> Unit,
4956
onPaymentMethodSelected: (InAppPaymentData.PaymentMethodType) -> Unit
5057
) {
5158
ModalBottomSheet(
5259
onDismissRequest = onDismissRequest,
60+
sheetState = sheetState,
5361
dragHandle = { BottomSheets.Handle() },
54-
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
62+
modifier = Modifier.padding()
5563
) {
56-
SheetContent(
57-
messageBackupTier = messageBackupTier,
58-
availablePaymentGateways = availablePaymentMethods,
59-
onPaymentGatewaySelected = onPaymentMethodSelected
60-
)
64+
Column(
65+
horizontalAlignment = Alignment.CenterHorizontally,
66+
modifier = Modifier
67+
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
68+
.navigationBarsPadding()
69+
) {
70+
SheetContent(
71+
messageBackupsType = messageBackupsType,
72+
availablePaymentGateways = availablePaymentMethods,
73+
onPaymentGatewaySelected = onPaymentMethodSelected
74+
)
75+
}
6176
}
6277
}
6378

6479
@Composable
6580
private fun SheetContent(
66-
messageBackupTier: MessageBackupTier,
81+
messageBackupsType: MessageBackupsType,
6782
availablePaymentGateways: List<InAppPaymentData.PaymentMethodType>,
6883
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
6984
) {
7085
val resources = LocalContext.current.resources
71-
val backupTypeDetails = remember(messageBackupTier) {
72-
getTierDetails(messageBackupTier)
73-
}
74-
val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
75-
FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
86+
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
87+
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
7688
}
7789

7890
Text(
@@ -88,7 +100,7 @@ private fun SheetContent(
88100
)
89101

90102
MessageBackupsTypeBlock(
91-
messageBackupsType = backupTypeDetails,
103+
messageBackupsType = messageBackupsType,
92104
isSelected = false,
93105
onSelected = {},
94106
enabled = false,
@@ -231,7 +243,12 @@ private fun MessageBackupsCheckoutSheetPreview() {
231243
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
232244
) {
233245
SheetContent(
234-
messageBackupTier = MessageBackupTier.PAID,
246+
messageBackupsType = MessageBackupsType(
247+
tier = MessageBackupTier.FREE,
248+
title = "Free",
249+
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
250+
features = persistentListOf()
251+
),
235252
availablePaymentGateways = availablePaymentGateways,
236253
onPaymentGatewaySelected = {}
237254
)

0 commit comments

Comments
 (0)