diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f6f54a1260..1cbb242aa8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -748,13 +748,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
-
-
-
-
-
+
): List {
+ return availableBackupTiers.map { getBackupsType(it) }
+ }
+
+ suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
+ val backupCurrency = SignalStore.donationsValues().getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
+ return when (tier) {
+ MessageBackupTier.FREE -> getFreeType(backupCurrency)
+ MessageBackupTier.PAID -> getPaidType(backupCurrency)
+ }
+ }
+
+ private fun getFreeType(currency: Currency): MessageBackupsType {
+ return MessageBackupsType(
+ tier = MessageBackupTier.FREE,
+ pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
+ title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
+ features = persistentListOf(
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_thread_compact_bold_16,
+ label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
+ ),
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_album_compact_bold_16,
+ label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
+ )
+ )
+ )
+ }
+
+ private suspend fun getPaidType(currency: Currency): MessageBackupsType {
+ val serviceResponse = withContext(Dispatchers.IO) {
+ AppDependencies
+ .donationsService
+ .getDonationsConfiguration(Locale.getDefault())
+ }
+
+ if (serviceResponse.result.isEmpty) {
+ if (serviceResponse.applicationError.isPresent) {
+ throw serviceResponse.applicationError.get()
+ }
+
+ if (serviceResponse.executionError.isPresent) {
+ throw serviceResponse.executionError.get()
+ }
+
+ error("Unhandled error occurred while downloading configuration.")
+ }
+
+ val config = serviceResponse.result.get()
+
+ return MessageBackupsType(
+ tier = MessageBackupTier.PAID,
+ pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
+ title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
+ features = persistentListOf(
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_thread_compact_bold_16,
+ label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
+ ),
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_album_compact_bold_16,
+ label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
+ ),
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_thread_compact_bold_16,
+ label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
+ ),
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_heart_compact_bold_16,
+ label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
+ )
+ )
+ )
+ }
+
/**
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
* Should be the basis of all backup operations.
@@ -765,18 +851,3 @@ class BackupMetadata(
val usedSpace: Long,
val mediaCount: Long
)
-
-enum class MessageBackupTier(val value: Int) {
- FREE(0),
- PAID(1);
-
- companion object Serializer : LongSerializer {
- override fun serialize(data: MessageBackupTier?): Long {
- return data?.value?.toLong() ?: -1
- }
-
- override fun deserialize(data: Long): MessageBackupTier? {
- return values().firstOrNull { it.value == data.toInt() }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/MessageBackupTier.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/MessageBackupTier.kt
new file mode 100644
index 0000000000..9111820986
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/MessageBackupTier.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2
+
+import org.signal.core.util.LongSerializer
+
+/**
+ * Serializable enum value for what we think a user's current backup tier is.
+ *
+ * We should not trust the stored value on its own, we should also verify it
+ * against what the server knows, but it is a useful flag that helps avoid a
+ * network call in some cases.
+ */
+enum class MessageBackupTier(val value: Int) {
+ FREE(0),
+ PAID(1);
+
+ companion object Serializer : LongSerializer {
+ override fun serialize(data: MessageBackupTier?): Long {
+ return data?.value?.toLong() ?: -1
+ }
+
+ override fun deserialize(data: Long): MessageBackupTier? {
+ return entries.firstOrNull { it.value == data.toInt() }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt
index c386600aea..56ae367873 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt
@@ -11,12 +11,14 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -30,49 +32,59 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
+import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
+import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
+import java.math.BigDecimal
+import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
- messageBackupTier: MessageBackupTier,
+ messageBackupsType: MessageBackupsType,
availablePaymentMethods: List,
+ sheetState: SheetState,
onDismissRequest: () -> Unit,
onPaymentMethodSelected: (InAppPaymentData.PaymentMethodType) -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
+ sheetState = sheetState,
dragHandle = { BottomSheets.Handle() },
- modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
+ modifier = Modifier.padding()
) {
- SheetContent(
- messageBackupTier = messageBackupTier,
- availablePaymentGateways = availablePaymentMethods,
- onPaymentGatewaySelected = onPaymentMethodSelected
- )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
+ .navigationBarsPadding()
+ ) {
+ SheetContent(
+ messageBackupsType = messageBackupsType,
+ availablePaymentGateways = availablePaymentMethods,
+ onPaymentGatewaySelected = onPaymentMethodSelected
+ )
+ }
}
}
@Composable
private fun SheetContent(
- messageBackupTier: MessageBackupTier,
+ messageBackupsType: MessageBackupsType,
availablePaymentGateways: List,
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
) {
val resources = LocalContext.current.resources
- val backupTypeDetails = remember(messageBackupTier) {
- getTierDetails(messageBackupTier)
- }
- val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
- FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
+ val formattedPrice = remember(messageBackupsType.pricePerMonth) {
+ FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
@@ -88,7 +100,7 @@ private fun SheetContent(
)
MessageBackupsTypeBlock(
- messageBackupsType = backupTypeDetails,
+ messageBackupsType = messageBackupsType,
isSelected = false,
onSelected = {},
enabled = false,
@@ -231,7 +243,12 @@ private fun MessageBackupsCheckoutSheetPreview() {
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
- messageBackupTier = MessageBackupTier.PAID,
+ messageBackupsType = MessageBackupsType(
+ tier = MessageBackupTier.FREE,
+ title = "Free",
+ pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
+ features = persistentListOf()
+ ),
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowActivity.kt
deleted file mode 100644
index 608a9b48e0..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowActivity.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2024 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.thoughtcrime.securesms.backup.v2.ui.subscription
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.navigation.NavController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.dialog
-import androidx.navigation.compose.rememberNavController
-import org.signal.core.ui.theme.SignalTheme
-import org.thoughtcrime.securesms.PassphraseRequiredActivity
-import org.thoughtcrime.securesms.util.viewModel
-
-class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
-
- private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
-
- override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
- setContent {
- SignalTheme {
- val state by viewModel.state
- val navController = rememberNavController()
-
- fun MessageBackupsScreen.next() {
- val nextScreen = viewModel.goToNextScreen(this)
- if (nextScreen == MessageBackupsScreen.COMPLETED) {
- finishAfterTransition()
- return
- }
- if (nextScreen != this) {
- navController.navigate(nextScreen.name)
- }
- }
-
- fun NavController.popOrFinish() {
- if (popBackStack()) {
- return
- }
-
- finishAfterTransition()
- }
-
- LaunchedEffect(Unit) {
- navController.setLifecycleOwner(this@MessageBackupsFlowActivity)
- navController.setOnBackPressedDispatcher(this@MessageBackupsFlowActivity.onBackPressedDispatcher)
- navController.enableOnBackPressed(true)
- }
-
- NavHost(
- navController = navController,
- startDestination = if (state.currentMessageBackupTier == null) MessageBackupsScreen.EDUCATION.name else MessageBackupsScreen.TYPE_SELECTION.name,
- enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
- exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
- popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
- popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
- ) {
- composable(route = MessageBackupsScreen.EDUCATION.name) {
- MessageBackupsEducationScreen(
- onNavigationClick = navController::popOrFinish,
- onEnableBackups = { MessageBackupsScreen.EDUCATION.next() },
- onLearnMore = {}
- )
- }
-
- composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
- MessageBackupsPinEducationScreen(
- onNavigationClick = navController::popOrFinish,
- onGeneratePinClick = {},
- onUseCurrentPinClick = { MessageBackupsScreen.PIN_EDUCATION.next() },
- recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
- )
- }
-
- composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
- MessageBackupsPinConfirmationScreen(
- pin = state.pin,
- onPinChanged = viewModel::onPinEntryUpdated,
- pinKeyboardType = state.pinKeyboardType,
- onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
- onNextClick = { MessageBackupsScreen.PIN_CONFIRMATION.next() }
- )
- }
-
- composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
- MessageBackupsTypeSelectionScreen(
- selectedBackupTier = state.selectedMessageBackupTier,
- availableBackupTiers = state.availableBackupTiers,
- onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
- onNavigationClick = navController::popOrFinish,
- onReadMoreClicked = {},
- onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
- )
- }
-
- dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
- MessageBackupsCheckoutSheet(
- messageBackupTier = state.selectedMessageBackupTier!!,
- availablePaymentMethods = state.availablePaymentMethods,
- onDismissRequest = navController::popOrFinish,
- onPaymentMethodSelected = {
- viewModel.onPaymentMethodUpdated(it)
- MessageBackupsScreen.CHECKOUT_SHEET.next()
- }
- )
- }
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
new file mode 100644
index 0000000000..3c0e2114b1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2.ui.subscription
+
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.fragment.findNavController
+import io.reactivex.rxjava3.processors.PublishProcessor
+import org.signal.donations.InAppPaymentType
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
+import org.thoughtcrime.securesms.compose.ComposeFragment
+import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.thoughtcrime.securesms.util.navigation.safeNavigate
+import org.thoughtcrime.securesms.util.viewModel
+
+/**
+ * Handles the selection, payment, and changing of a user's backup tier.
+ */
+class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.Callback {
+
+ private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
+
+ private val inAppPaymentIdProcessor = PublishProcessor.create()
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun FragmentContent() {
+ val state by viewModel.state
+ val navController = rememberNavController()
+
+ val checkoutDelegate = remember {
+ DonationCheckoutDelegate(this, this, inAppPaymentIdProcessor)
+ }
+
+ LaunchedEffect(state.inAppPayment?.id) {
+ val inAppPaymentId = state.inAppPayment?.id
+ if (inAppPaymentId != null) {
+ inAppPaymentIdProcessor.onNext(inAppPaymentId)
+ }
+ }
+
+ val checkoutSheetState = rememberModalBottomSheetState(
+ skipPartiallyExpanded = true
+ )
+
+ LaunchedEffect(Unit) {
+ navController.setLifecycleOwner(this@MessageBackupsFlowFragment)
+ navController.setOnBackPressedDispatcher(requireActivity().onBackPressedDispatcher)
+ navController.enableOnBackPressed(true)
+ }
+
+ NavHost(
+ navController = navController,
+ startDestination = state.startScreen.name,
+ enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
+ exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
+ popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
+ popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
+ ) {
+ composable(route = MessageBackupsScreen.EDUCATION.name) {
+ MessageBackupsEducationScreen(
+ onNavigationClick = viewModel::goToPreviousScreen,
+ onEnableBackups = viewModel::goToNextScreen,
+ onLearnMore = {}
+ )
+ }
+
+ composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
+ MessageBackupsPinEducationScreen(
+ onNavigationClick = viewModel::goToPreviousScreen,
+ onGeneratePinClick = {},
+ onUseCurrentPinClick = viewModel::goToNextScreen,
+ recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
+ )
+ }
+
+ composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
+ MessageBackupsPinConfirmationScreen(
+ pin = state.pin,
+ onPinChanged = viewModel::onPinEntryUpdated,
+ pinKeyboardType = state.pinKeyboardType,
+ onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
+ onNextClick = viewModel::goToNextScreen
+ )
+ }
+
+ composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
+ MessageBackupsTypeSelectionScreen(
+ selectedBackupTier = state.selectedMessageBackupTier,
+ availableBackupTypes = state.availableBackupTypes,
+ onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
+ onNavigationClick = viewModel::goToPreviousScreen,
+ onReadMoreClicked = {},
+ onNextClicked = viewModel::goToNextScreen
+ )
+
+ if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
+ MessageBackupsCheckoutSheet(
+ messageBackupsType = state.availableBackupTypes.first { it.tier == state.selectedMessageBackupTier!! },
+ availablePaymentMethods = state.availablePaymentMethods,
+ sheetState = checkoutSheetState,
+ onDismissRequest = {
+ viewModel.goToPreviousScreen()
+ },
+ onPaymentMethodSelected = {
+ viewModel.onPaymentMethodUpdated(it)
+ viewModel.goToNextScreen()
+ }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(state.screen) {
+ val route = navController.currentDestination?.route ?: return@LaunchedEffect
+ if (route == state.screen.name) {
+ return@LaunchedEffect
+ }
+
+ if (state.screen == MessageBackupsScreen.COMPLETED) {
+ if (!findNavController().popBackStack()) {
+ requireActivity().finishAfterTransition()
+ }
+ return@LaunchedEffect
+ }
+
+ if (state.screen == MessageBackupsScreen.PROCESS_PAYMENT) {
+ checkoutDelegate.handleGatewaySelectionResponse(state.inAppPayment!!)
+ viewModel.goToPreviousScreen()
+ return@LaunchedEffect
+ }
+
+ if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
+ return@LaunchedEffect
+ }
+
+ val routeScreen = MessageBackupsScreen.valueOf(route)
+ if (routeScreen.isAfter(state.screen)) {
+ navController.popBackStack()
+ } else {
+ navController.navigate(state.screen.name)
+ }
+ }
+ }
+
+ override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
+ findNavController().safeNavigate(
+ MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
+ DonationProcessorAction.PROCESS_NEW_DONATION,
+ inAppPayment,
+ inAppPayment.type
+ )
+ )
+ }
+
+ override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
+ findNavController().safeNavigate(
+ MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
+ DonationProcessorAction.PROCESS_NEW_DONATION,
+ inAppPayment,
+ inAppPayment.type
+ )
+ )
+ }
+
+ override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
+ findNavController().safeNavigate(
+ MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment)
+ )
+ }
+
+ override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
+ findNavController().safeNavigate(
+ MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment)
+ )
+ }
+
+ override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
+ findNavController().safeNavigate(
+ MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment)
+ )
+ }
+
+ override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
+ // TODO [message-backups] What do? probably some kind of success thing?
+ if (!findNavController().popBackStack()) {
+ requireActivity().finishAfterTransition()
+ }
+ }
+
+ override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) = error("This view doesn't support cancellation, that is done elsewhere.")
+
+ override fun onProcessorActionProcessed() = Unit
+
+ override fun onUserLaunchedAnExternalApplication() {
+ // TODO [message-backups] What do? Are we even supporting bank transfers?
+ }
+
+ override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
+ // TODO [message-backups] What do? Are we even supporting bank transfers?
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowRepository.kt
deleted file mode 100644
index 791b352f3f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowRepository.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * Copyright 2024 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.thoughtcrime.securesms.backup.v2.ui.subscription
-
-class MessageBackupsFlowRepository
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
index f61c051f8a..523c03f242 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
@@ -13,9 +14,12 @@ import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup().backupTier,
val currentMessageBackupTier: MessageBackupTier? = SignalStore.backup().backupTier,
- val availableBackupTiers: List = emptyList(),
+ val availableBackupTypes: List = emptyList(),
val selectedPaymentMethod: InAppPaymentData.PaymentMethodType? = null,
val availablePaymentMethods: List = emptyList(),
val pin: String = "",
- val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
+ val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType,
+ val inAppPayment: InAppPaymentTable.InAppPayment? = null,
+ val startScreen: MessageBackupsScreen,
+ val screen: MessageBackupsScreen = startScreen
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt
index c0af9868dc..93f20924db 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt
@@ -9,30 +9,52 @@ import android.text.TextUtils
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.signal.donations.InAppPaymentType
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayOrderStrategy
+import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
+import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
+import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
class MessageBackupsFlowViewModel : ViewModel() {
private val internalState = mutableStateOf(
MessageBackupsFlowState(
- availableBackupTiers = if (!RemoteConfig.messageBackups) {
- emptyList()
- } else {
- listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
- },
- selectedMessageBackupTier = SignalStore.backup().backupTier
+ availableBackupTypes = emptyList(),
+ selectedMessageBackupTier = SignalStore.backup().backupTier,
+ availablePaymentMethods = GatewayOrderStrategy.getStrategy().orderedGateways.filter { InAppDonations.isPaymentSourceAvailable(it.toPaymentSourceType(), InAppPaymentType.RECURRING_BACKUP) },
+ startScreen = if (SignalStore.backup().backupTier == null) MessageBackupsScreen.EDUCATION else MessageBackupsScreen.TYPE_SELECTION
)
)
val state: State = internalState
- fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
- return when (currentScreen) {
+ init {
+ viewModelScope.launch {
+ internalState.value = internalState.value.copy(
+ availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
+ if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
+ )
+ )
+ }
+ }
+
+ fun goToNextScreen() {
+ val nextScreen = when (internalState.value.screen) {
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
@@ -41,6 +63,27 @@ class MessageBackupsFlowViewModel : ViewModel() {
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
}
+
+ internalState.value = state.value.copy(screen = nextScreen)
+ }
+
+ fun goToPreviousScreen() {
+ if (internalState.value.screen == internalState.value.startScreen) {
+ internalState.value = state.value.copy(screen = MessageBackupsScreen.COMPLETED)
+ return
+ }
+
+ val previousScreen = when (internalState.value.screen) {
+ MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.COMPLETED
+ MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.EDUCATION
+ MessageBackupsScreen.PIN_CONFIRMATION -> MessageBackupsScreen.PIN_EDUCATION
+ MessageBackupsScreen.TYPE_SELECTION -> MessageBackupsScreen.PIN_CONFIRMATION
+ MessageBackupsScreen.CHECKOUT_SHEET -> MessageBackupsScreen.TYPE_SELECTION
+ MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.TYPE_SELECTION
+ MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
+ }
+
+ internalState.value = state.value.copy(screen = previousScreen)
}
fun onPinEntryUpdated(pin: String) {
@@ -74,11 +117,48 @@ class MessageBackupsFlowViewModel : ViewModel() {
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
SignalStore.backup().areBackupsEnabled = true
SignalStore.backup().backupTier = state.value.selectedMessageBackupTier!!
- return MessageBackupsScreen.COMPLETED
- // return MessageBackupsScreen.CHECKOUT_SHEET TODO [message-backups] Switch back to payment flow
+
+ // TODO [message-backups] - Does anything need to be kicked off?
+
+ return when (state.value.selectedMessageBackupTier!!) {
+ MessageBackupTier.FREE -> MessageBackupsScreen.COMPLETED
+ MessageBackupTier.PAID -> MessageBackupsScreen.CHECKOUT_SHEET
+ }
}
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
+ val stateSnapshot = state.value
+ val backupsType = stateSnapshot.availableBackupTypes.first { it.tier == stateSnapshot.selectedMessageBackupTier }
+
+ internalState.value = state.value.copy(inAppPayment = null)
+
+ viewModelScope.launch(Dispatchers.IO) {
+ SignalDatabase.inAppPayments.clearCreated()
+ val id = SignalDatabase.inAppPayments.insert(
+ type = InAppPaymentType.RECURRING_BACKUP,
+ state = InAppPaymentTable.State.CREATED,
+ subscriberId = null,
+ endOfPeriod = null,
+ inAppPaymentData = InAppPaymentData(
+ badge = null,
+ label = backupsType.title,
+ amount = backupsType.pricePerMonth.toFiatValue(),
+ level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
+ recipientId = Recipient.self().id.serialize(),
+ paymentMethodType = stateSnapshot.selectedPaymentMethod!!,
+ redemption = InAppPaymentData.RedemptionState(
+ stage = InAppPaymentData.RedemptionState.Stage.INIT
+ )
+ )
+ )
+
+ val inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
+
+ withContext(Dispatchers.Main) {
+ internalState.value = state.value.copy(inAppPayment = inAppPayment)
+ }
+ }
+
return MessageBackupsScreen.PROCESS_PAYMENT
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsScreen.kt
index 994f180ffd..66b1525ed0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsScreen.kt
@@ -12,5 +12,7 @@ enum class MessageBackupsScreen {
TYPE_SELECTION,
CHECKOUT_SHEET,
PROCESS_PAYMENT,
- COMPLETED
+ COMPLETED;
+
+ fun isAfter(other: MessageBackupsScreen): Boolean = ordinal > other.ordinal
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt
new file mode 100644
index 0000000000..e808a72b4c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2.ui.subscription
+
+import androidx.compose.runtime.Stable
+import kotlinx.collections.immutable.ImmutableList
+import org.signal.core.util.money.FiatMoney
+import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+
+/**
+ * Represents a type of backup a user can select.
+ */
+@Stable
+data class MessageBackupsType(
+ val tier: MessageBackupTier,
+ val pricePerMonth: FiatMoney,
+ val title: String,
+ val features: ImmutableList
+)
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 ab0b620d60..cee46ef94e 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
@@ -21,7 +21,6 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -40,8 +39,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
@@ -52,7 +49,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
-import java.util.Currency
/**
* Screen which allows the user to select their preferred backup type.
@@ -61,7 +57,7 @@ import java.util.Currency
@Composable
fun MessageBackupsTypeSelectionScreen(
selectedBackupTier: MessageBackupTier?,
- availableBackupTiers: List,
+ availableBackupTypes: List,
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
onNavigationClick: () -> Unit,
onReadMoreClicked: () -> Unit,
@@ -129,16 +125,13 @@ fun MessageBackupsTypeSelectionScreen(
}
itemsIndexed(
- availableBackupTiers,
- { _, item -> item }
+ availableBackupTypes,
+ { _, item -> item.tier }
) { index, item ->
- val type = remember(item) {
- getTierDetails(item)
- }
MessageBackupsTypeBlock(
- messageBackupsType = type,
- isSelected = item == selectedBackupTier,
- onSelected = { onMessageBackupsTierSelected(item) },
+ messageBackupsType = item,
+ isSelected = item.tier == selectedBackupTier,
+ onSelected = { onMessageBackupsTierSelected(item.tier) },
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
)
}
@@ -167,7 +160,7 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
Previews.Preview {
MessageBackupsTypeSelectionScreen(
selectedBackupTier = MessageBackupTier.FREE,
- availableBackupTiers = listOf(MessageBackupTier.FREE, MessageBackupTier.PAID),
+ availableBackupTypes = emptyList(),
onMessageBackupsTierSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
@@ -236,54 +229,3 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
}
}
-
-@Stable
-data class MessageBackupsType(
- val tier: MessageBackupTier,
- val pricePerMonth: FiatMoney,
- val title: String,
- val features: ImmutableList
-)
-
-fun getTierDetails(tier: MessageBackupTier): MessageBackupsType {
- return when (tier) {
- MessageBackupTier.FREE -> MessageBackupsType(
- tier = MessageBackupTier.FREE,
- pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
- title = "Text + 30 days of media",
- features = persistentListOf(
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_thread_compact_bold_16,
- label = "Full text message backup"
- ),
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_album_compact_bold_16,
- label = "Last 30 days of media"
- )
- )
- )
- MessageBackupTier.PAID -> MessageBackupsType(
- tier = MessageBackupTier.PAID,
- pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
- title = "Text + All your media",
- features = persistentListOf(
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_thread_compact_bold_16,
- label = "Full text message backup"
- ),
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_album_compact_bold_16,
- label = "Full media backup"
- ),
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_thread_compact_bold_16,
- label = "1TB of storage (~250K photos)"
- ),
- MessageBackupsTypeFeature(
- iconResourceId = R.drawable.symbol_heart_compact_bold_16,
- label = "Thanks for supporting Signal!"
- )
- )
- )
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt
deleted file mode 100644
index 783af09214..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowActivity.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.thoughtcrime.securesms.badges.gifts.flow
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.OnBackPressedCallback
-import androidx.fragment.app.Fragment
-import androidx.navigation.findNavController
-import androidx.navigation.fragment.NavHostFragment
-import io.reactivex.rxjava3.subjects.PublishSubject
-import io.reactivex.rxjava3.subjects.Subject
-import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.components.FragmentWrapperActivity
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
-import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
-
-/**
- * Activity which houses the gift flow.
- */
-class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent {
-
- override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
-
- override val googlePayResultPublisher: Subject = PublishSubject.create()
-
- override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
- super.onCreate(savedInstanceState, ready)
- onBackPressedDispatcher.addCallback(this, OnBackPressed())
- }
-
- override fun getFragment(): Fragment {
- return NavHostFragment.create(R.navigation.gift_flow)
- }
-
- @Suppress("DEPRECATION")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
- }
-
- private inner class OnBackPressed : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (!findNavController(R.id.fragment_container).popBackStack()) {
- finish()
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt
index ca36a9efd8..8b94fa9e75 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt
@@ -6,6 +6,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
@@ -13,8 +14,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.concurrent.LifecycleDisposable
+import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.InputAwareLayout
@@ -25,6 +28,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
import org.thoughtcrime.securesms.components.settings.models.TextInput
@@ -35,10 +39,11 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
+import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
-import java.util.Optional
+import java.math.BigDecimal
/**
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
@@ -69,7 +74,6 @@ class GiftFlowConfirmationFragment :
private lateinit var emojiKeyboard: MediaKeyboard
private val lifecycleDisposable = LifecycleDisposable()
- private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
private lateinit var processingDonationPaymentDialog: AlertDialog
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
@@ -81,13 +85,9 @@ class GiftFlowConfirmationFragment :
RecipientPreference.register(adapter)
GiftRowItem.register(adapter)
- keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
+ val checkoutDelegate = DonationCheckoutDelegate(this, this, viewModel.state.filter { it.inAppPaymentId != null }.map { it.inAppPaymentId!! })
- donationCheckoutDelegate = DonationCheckoutDelegate(
- this,
- this,
- viewModel.state.mapOptional { Optional.ofNullable(it.inAppPaymentId) }.distinctUntilChanged()
- )
+ keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog)
@@ -104,6 +104,15 @@ class GiftFlowConfirmationFragment :
emojiKeyboard.setFragmentManager(childFragmentManager)
+ setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
+ if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
+ showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
+ } else {
+ val inAppPayment: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, InAppPaymentTable.InAppPayment::class.java)!!
+ checkoutDelegate.handleGatewaySelectionResponse(inAppPayment)
+ }
+ }
+
val continueButton = requireView().findViewById(R.id.continue_button)
continueButton.setOnClickListener {
lifecycleDisposable += viewModel.insertInAppPayment(requireContext()).subscribe { inAppPayment ->
@@ -191,7 +200,6 @@ class GiftFlowConfirmationFragment :
processingDonationPaymentDialog.dismiss()
debouncer.clear()
verifyingRecipientDonationPaymentDialog.dismiss()
- donationCheckoutDelegate = null
}
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
@@ -245,25 +253,44 @@ class GiftFlowConfirmationFragment :
}
}
+ private fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) {
+ val max = FiatMoneyUtil.format(resources, sepaEuroMaximum, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.DonateToSignal__donation_amount_too_high)
+ .setMessage(getString(R.string.DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer, max))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
+
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
- findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
+ findNavController().safeNavigate(
+ GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(
+ DonationProcessorAction.PROCESS_NEW_DONATION,
+ inAppPayment,
+ inAppPayment.type
+ )
+ )
}
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
- findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
+ findNavController().safeNavigate(
+ GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(
+ DonationProcessorAction.PROCESS_NEW_DONATION,
+ inAppPayment,
+ inAppPayment.type
+ )
+ )
}
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
- findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment))
+ findNavController().safeNavigate(
+ GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment)
+ )
}
- override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
- error("Unsupported operation")
- }
+ override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) = error("iDEAL transfer isn't supported for gifts.")
- override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
- error("Unsupported operation")
- }
+ override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) = error("Bank transfer isn't supported for gifts.")
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
val mainActivityIntent = MainActivity.clearTop(requireContext())
@@ -277,11 +304,13 @@ class GiftFlowConfirmationFragment :
}
}
- override fun onProcessorActionProcessed() = Unit
+ override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) = error("Not supported for gifts")
- override fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) = error("Unsupported operation")
+ override fun onProcessorActionProcessed() {
+ // TODO [alex] -- what do?
+ }
- override fun onUserLaunchedAnExternalApplication() = Unit
+ override fun onUserLaunchedAnExternalApplication() = error("Not supported for gifts.")
- override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Unsupported operation")
+ override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Not supported for gifts")
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt
index e86e8fec9a..c0a672bc7f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowRepository.kt
@@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Context
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
-import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
@@ -25,14 +25,10 @@ import java.util.Locale
*/
class GiftFlowRepository {
- companion object {
- private val TAG = Log.tag(GiftFlowRepository::class.java)
- }
-
fun insertInAppPayment(context: Context, giftSnapshot: GiftFlowState): Single {
return Single.fromCallable {
SignalDatabase.inAppPayments.insert(
- type = InAppPaymentTable.Type.ONE_TIME_GIFT,
+ type = InAppPaymentType.ONE_TIME_GIFT,
state = InAppPaymentTable.State.CREATED,
subscriberId = null,
endOfPeriod = null,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt
index b6ba572134..c87aa9cda0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowStartFragment.kt
@@ -6,6 +6,7 @@ import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
@@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.models.Ne
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.components.settings.models.SplashImage
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -92,7 +92,7 @@ class GiftFlowStartFragment : DSLSettingsFragment(
selectedCurrency = state.currency,
isEnabled = state.stage == GiftFlowState.Stage.READY,
onClick = {
- val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(InAppPaymentTable.Type.ONE_TIME_GIFT, viewModel.getSupportedCurrencyCodes().toTypedArray())
+ val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(InAppPaymentType.ONE_TIME_GIFT, viewModel.getSupportedCurrencyCodes().toTypedArray())
findNavController().safeNavigate(action)
}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt
index 43bbe1b409..35df67c60c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt
@@ -7,13 +7,13 @@ import androidx.navigation.NavDirections
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -31,12 +31,12 @@ private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create"
-class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
+class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent {
private var wasConfigurationUpdated = false
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
- override val googlePayResultPublisher: Subject = PublishSubject.create()
+ override val googlePayResultPublisher: Subject = PublishSubject.create()
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
@@ -57,8 +57,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
- StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.RECURRING_DONATION)
- StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.ONE_TIME_DONATION)
+ StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToCheckout(InAppPaymentType.RECURRING_DONATION)
+ StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToCheckout(InAppPaymentType.ONE_TIME_DONATION)
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
@@ -128,7 +128,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
+ googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
}
companion object {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
index 40f542bd08..16a4a2ec1c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
@@ -1,13 +1,13 @@
package org.thoughtcrime.securesms.components.settings.app.chats
-import android.content.Intent
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -92,7 +92,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
if (state.remoteBackupsEnabled) {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_remoteBackupsSettingsFragment)
} else {
- startActivity(Intent(requireContext(), MessageBackupsFlowActivity::class.java))
+ startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.RECURRING_BACKUP))
}
}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt
index 5ca05462f0..6f77293b64 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt
@@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups
-import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.background
@@ -38,6 +37,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.collections.immutable.persistentListOf
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -50,12 +51,14 @@ 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.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.getTierDetails
+import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
@@ -63,6 +66,8 @@ import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
+import java.math.BigDecimal
+import java.util.Currency
import java.util.Locale
/**
@@ -82,7 +87,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
val callbacks = remember { Callbacks() }
RemoteBackupsSettingsContent(
- messageBackupTier = state.messageBackupsTier,
+ messageBackupsType = state.messageBackupsType,
lastBackupTimestamp = state.lastBackupTimestamp,
canBackUpUsingCellular = state.canBackUpUsingCellular,
backupsFrequency = state.backupsFrequency,
@@ -101,7 +106,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onEnableBackupsClick() {
- startActivity(Intent(requireContext(), MessageBackupsFlowActivity::class.java))
+ startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.RECURRING_BACKUP))
}
override fun onBackUpUsingCellularClick(canUseCellular: Boolean) {
@@ -137,7 +142,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onTurnOffAndDeleteBackupsConfirm() {
- viewModel.turnOffAndDeleteBackups()
+ // TODO [alex] CheckoutFlowStartFragment.launchForBackupsCancellation(childFragmentManager)
}
override fun onBackupsTypeClick() {
@@ -159,6 +164,12 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
super.onResume()
viewModel.refresh()
}
+
+// override fun onCheckoutFlowResult(result: CheckoutFlowStartFragment.Result) {
+// if (result is CheckoutFlowStartFragment.Result.CancelationSuccess) {
+// Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
+// }
+// }
}
/**
@@ -181,7 +192,7 @@ private interface ContentCallbacks {
@Composable
private fun RemoteBackupsSettingsContent(
- messageBackupTier: MessageBackupTier?,
+ messageBackupsType: MessageBackupsType?,
lastBackupTimestamp: Long,
canBackUpUsingCellular: Boolean,
backupsFrequency: BackupFrequency,
@@ -209,13 +220,13 @@ private fun RemoteBackupsSettingsContent(
) {
item {
BackupTypeRow(
- messageBackupTier = messageBackupTier,
+ messageBackupsType = messageBackupsType,
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick
)
}
- if (messageBackupTier == null) {
+ if (messageBackupsType == null) {
item {
Rows.TextRow(
text = "Payment history",
@@ -253,7 +264,7 @@ private fun RemoteBackupsSettingsContent(
color = MaterialTheme.colorScheme.onSurface
)
Text(
- text = Util.getPrettyFileSize(backupSize ?: 0),
+ text = Util.getPrettyFileSize(backupSize),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -358,16 +369,14 @@ private fun RemoteBackupsSettingsContent(
@Composable
private fun BackupTypeRow(
- messageBackupTier: MessageBackupTier?,
+ messageBackupsType: MessageBackupsType?,
onEnableBackupsClick: () -> Unit,
onChangeBackupsTypeClick: () -> Unit
) {
- val messageBackupsType = if (messageBackupTier != null) getTierDetails(messageBackupTier) else null
-
Row(
modifier = Modifier
.fillMaxWidth()
- .clickable(enabled = messageBackupTier != null, onClick = onChangeBackupsTypeClick)
+ .clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
@@ -573,7 +582,7 @@ private fun getTextForFrequency(backupsFrequency: BackupFrequency): String {
private fun RemoteBackupsSettingsContentPreview() {
Previews.Preview {
RemoteBackupsSettingsContent(
- messageBackupTier = null,
+ messageBackupsType = null,
lastBackupTimestamp = -1,
canBackUpUsingCellular = false,
backupsFrequency = BackupFrequency.MANUAL,
@@ -591,7 +600,12 @@ private fun RemoteBackupsSettingsContentPreview() {
private fun BackupTypeRowPreview() {
Previews.Preview {
BackupTypeRow(
- messageBackupTier = MessageBackupTier.PAID,
+ messageBackupsType = MessageBackupsType(
+ tier = MessageBackupTier.FREE,
+ title = "Free",
+ pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
+ features = persistentListOf()
+ ),
onChangeBackupsTypeClick = {},
onEnableBackupsClick = {}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt
index a25ab14b30..6029a262bc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt
@@ -7,10 +7,10 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
-import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
data class RemoteBackupsSettingsState(
- val messageBackupsTier: MessageBackupTier? = null,
+ val messageBackupsType: MessageBackupsType? = null,
val canBackUpUsingCellular: Boolean = false,
val backupSize: Long = 0,
val backupsFrequency: BackupFrequency = BackupFrequency.DAILY,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt
index 1e51c502df..e6808a9151 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt
@@ -8,7 +8,10 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
@@ -21,7 +24,7 @@ import org.thoughtcrime.securesms.service.MessageBackupListener
class RemoteBackupsSettingsViewModel : ViewModel() {
private val internalState = mutableStateOf(
RemoteBackupsSettingsState(
- messageBackupsTier = SignalStore.backup().backupTier,
+ messageBackupsType = null,
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
backupSize = SignalStore.backup().totalBackupSize,
backupsFrequency = SignalStore.backup().backupFrequency
@@ -30,6 +33,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
val state: State = internalState
+ init {
+ refresh()
+ }
+
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
SignalStore.backup().backupWithCellular = canBackUpUsingCellular
internalState.value = state.value.copy(canBackUpUsingCellular = canBackUpUsingCellular)
@@ -51,12 +58,17 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
fun refresh() {
- internalState.value = state.value.copy(
- messageBackupsTier = SignalStore.backup().backupTier,
- lastBackupTimestamp = SignalStore.backup().lastBackupTime,
- backupSize = SignalStore.backup().totalBackupSize,
- backupsFrequency = SignalStore.backup().backupFrequency
- )
+ viewModelScope.launch {
+ val tier = SignalStore.backup().backupTier
+ val backupType = if (tier != null) BackupRepository.getBackupsType(tier) else null
+
+ internalState.value = state.value.copy(
+ messageBackupsType = backupType,
+ lastBackupTimestamp = SignalStore.backup().lastBackupTime,
+ backupSize = SignalStore.backup().totalBackupSize,
+ backupsFrequency = SignalStore.backup().backupFrequency
+ )
+ }
}
fun turnOffAndDeleteBackups() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt
index 61e2d4d426..4702116d2f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt
@@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
-import android.content.Intent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -19,19 +18,24 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.fragment.findNavController
+import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
+import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.getTierDetails
+import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.viewModel
+import java.math.BigDecimal
+import java.util.Currency
import java.util.Locale
/**
@@ -67,7 +71,7 @@ class BackupsTypeSettingsFragment : ComposeFragment() {
}
override fun onChangeOrCancelSubscriptionClick() {
- startActivity(Intent(requireContext(), MessageBackupsFlowActivity::class.java))
+ startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.RECURRING_BACKUP))
}
}
@@ -88,7 +92,7 @@ private fun BackupsTypeSettingsContent(
state: BackupsTypeSettingsState,
contentCallbacks: ContentCallbacks
) {
- if (state.backupsTier == null) {
+ if (state.messageBackupsType == null) {
return
}
@@ -102,7 +106,7 @@ private fun BackupsTypeSettingsContent(
) {
item {
BackupsTypeRow(
- backupsTier = state.backupsTier,
+ messageBackupsType = state.messageBackupsType,
nextRenewalTimestamp = state.nextRenewalTimestamp
)
}
@@ -132,13 +136,9 @@ private fun BackupsTypeSettingsContent(
@Composable
private fun BackupsTypeRow(
- backupsTier: MessageBackupTier,
+ messageBackupsType: MessageBackupsType,
nextRenewalTimestamp: Long
) {
- val messageBackupsType = remember(backupsTier) {
- getTierDetails(backupsTier)
- }
-
val resources = LocalContext.current.resources
val formattedAmount = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
@@ -191,7 +191,12 @@ private fun BackupsTypeSettingsContentPreview() {
Previews.Preview {
BackupsTypeSettingsContent(
state = BackupsTypeSettingsState(
- backupsTier = MessageBackupTier.PAID
+ messageBackupsType = MessageBackupsType(
+ tier = MessageBackupTier.FREE,
+ pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
+ title = "Free",
+ features = persistentListOf()
+ )
),
contentCallbacks = object : ContentCallbacks {}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt
index d1c3ccbb29..3c77c1e305 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt
@@ -7,11 +7,11 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
import androidx.compose.runtime.Stable
import org.signal.donations.PaymentSourceType
-import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
@Stable
data class BackupsTypeSettingsState(
- val backupsTier: MessageBackupTier? = null,
+ val messageBackupsType: MessageBackupsType? = null,
val paymentSourceType: PaymentSourceType = PaymentSourceType.Unknown,
val nextRenewalTimestamp: Long = 0
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt
index 8834512621..6b88c2dbaf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt
@@ -8,18 +8,26 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
class BackupsTypeSettingsViewModel : ViewModel() {
- private val internalState = mutableStateOf(
- BackupsTypeSettingsState(
- backupsTier = SignalStore.backup().backupTier
- )
- )
+ private val internalState = mutableStateOf(BackupsTypeSettingsState())
val state: State = internalState
+ init {
+ refresh()
+ }
+
fun refresh() {
- internalState.value = state.value.copy(backupsTier = SignalStore.backup().backupTier)
+ viewModelScope.launch {
+ val tier = SignalStore.backup().backupTier
+ internalState.value = state.value.copy(
+ messageBackupsType = if (tier != null) BackupRepository.getBackupsType(tier) else null
+ )
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt
index 9a3b72b120..6e1cec201e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.internal
import android.content.Context
import org.json.JSONObject
import org.signal.core.util.concurrent.SignalExecutors
-import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
@@ -34,7 +34,7 @@ class InternalSettingsRepository(context: Context) {
fun enqueueSubscriptionRedemption() {
SignalExecutors.BOUNDED.execute {
- val latest = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(InAppPaymentTable.Type.RECURRING_DONATION)
+ val latest = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(InAppPaymentType.RECURRING_DONATION)
if (latest != null) {
InAppPaymentRecurringContextJob.createJobChain(latest).enqueue()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt
index 6e59d6604e..c16a13f152 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt
@@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.LocaleRemoteConfig
@@ -24,7 +24,7 @@ object InAppDonations {
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable() || isIDEALAvailable()
}
- fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, inAppPaymentType: InAppPaymentTable.Type): Boolean {
+ fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, inAppPaymentType: InAppPaymentType): Boolean {
return when (paymentSourceType) {
PaymentSourceType.PayPal -> isPayPalAvailableForDonateToSignalType(inAppPaymentType)
PaymentSourceType.Stripe.CreditCard -> isCreditCardAvailable()
@@ -35,12 +35,12 @@ object InAppDonations {
}
}
- private fun isPayPalAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
+ private fun isPayPalAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentType): Boolean {
return when (inAppPaymentType) {
- InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
- InAppPaymentTable.Type.ONE_TIME_DONATION, InAppPaymentTable.Type.ONE_TIME_GIFT -> RemoteConfig.paypalOneTimeDonations
- InAppPaymentTable.Type.RECURRING_DONATION -> RemoteConfig.paypalRecurringDonations
- InAppPaymentTable.Type.RECURRING_BACKUP -> RemoteConfig.messageBackups && RemoteConfig.paypalRecurringDonations
+ InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
+ InAppPaymentType.ONE_TIME_DONATION, InAppPaymentType.ONE_TIME_GIFT -> RemoteConfig.paypalOneTimeDonations
+ InAppPaymentType.RECURRING_DONATION -> RemoteConfig.paypalRecurringDonations
+ InAppPaymentType.RECURRING_BACKUP -> RemoteConfig.messageBackups && RemoteConfig.paypalRecurringDonations
} && !LocaleRemoteConfig.isPayPalDisabled()
}
@@ -83,15 +83,15 @@ object InAppDonations {
* Whether the user is in a region which supports SEPA Debit transfers, based off local phone number
* and donation type.
*/
- fun isSEPADebitAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
- return inAppPaymentType != InAppPaymentTable.Type.ONE_TIME_GIFT && isSEPADebitAvailable()
+ fun isSEPADebitAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentType): Boolean {
+ return inAppPaymentType != InAppPaymentType.ONE_TIME_GIFT && isSEPADebitAvailable()
}
/**
* Whether the user is in a region which suports IDEAL transfers, based off local phone number and
* donation type
*/
- fun isIDEALAvailbleForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
- return inAppPaymentType != InAppPaymentTable.Type.ONE_TIME_GIFT && isIDEALAvailable()
+ fun isIDEALAvailbleForDonateToSignalType(inAppPaymentType: InAppPaymentType): Boolean {
+ return inAppPaymentType != InAppPaymentType.ONE_TIME_GIFT && isIDEALAvailable()
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentComponent.kt
similarity index 92%
rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt
rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentComponent.kt
index a251cf79f7..7ef70220ca 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentComponent.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentComponent.kt
@@ -5,7 +5,7 @@ import android.os.Parcelable
import io.reactivex.rxjava3.subjects.Subject
import kotlinx.parcelize.Parcelize
-interface DonationPaymentComponent {
+interface InAppPaymentComponent {
val stripeRepository: StripeRepository
val googlePayResultPublisher: Subject
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt
index cec1a67064..7e61417e9b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt
@@ -14,7 +14,9 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
+import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
@@ -38,6 +40,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
@@ -167,9 +171,9 @@ object InAppPaymentsRepository {
*/
fun resolveJobQueueKey(inAppPayment: InAppPaymentTable.InAppPayment): String {
return when (inAppPayment.type) {
- InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN.")
- InAppPaymentTable.Type.ONE_TIME_GIFT, InAppPaymentTable.Type.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
- InAppPaymentTable.Type.RECURRING_DONATION, InAppPaymentTable.Type.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
+ InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
+ InAppPaymentType.ONE_TIME_GIFT, InAppPaymentType.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
+ InAppPaymentType.RECURRING_DONATION, InAppPaymentType.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
}
}
@@ -196,13 +200,13 @@ object InAppPaymentsRepository {
/**
* Maps a payment type into a request code for grabbing a Google Pay token.
*/
- fun getGooglePayRequestCode(inAppPaymentType: InAppPaymentTable.Type): Int {
+ fun getGooglePayRequestCode(inAppPaymentType: InAppPaymentType): Int {
return when (inAppPaymentType) {
- InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
- InAppPaymentTable.Type.ONE_TIME_GIFT -> 16143
- InAppPaymentTable.Type.ONE_TIME_DONATION -> 16141
- InAppPaymentTable.Type.RECURRING_DONATION -> 16142
- InAppPaymentTable.Type.RECURRING_BACKUP -> 16144
+ InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
+ InAppPaymentType.ONE_TIME_GIFT -> 16143
+ InAppPaymentType.ONE_TIME_DONATION -> 16141
+ InAppPaymentType.RECURRING_DONATION -> 16142
+ InAppPaymentType.RECURRING_BACKUP -> 16144
}
}
@@ -210,14 +214,14 @@ object InAppPaymentsRepository {
* Converts an error source to a persistable type. For types that don't map,
* UNKNOWN is returned.
*/
- fun DonationErrorSource.toInAppPaymentType(): InAppPaymentTable.Type {
+ fun DonationErrorSource.toInAppPaymentType(): InAppPaymentType {
return when (this) {
- DonationErrorSource.ONE_TIME -> InAppPaymentTable.Type.ONE_TIME_DONATION
- DonationErrorSource.MONTHLY -> InAppPaymentTable.Type.RECURRING_DONATION
- DonationErrorSource.GIFT -> InAppPaymentTable.Type.ONE_TIME_GIFT
- DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentTable.Type.UNKNOWN
- DonationErrorSource.KEEP_ALIVE -> InAppPaymentTable.Type.UNKNOWN
- DonationErrorSource.UNKNOWN -> InAppPaymentTable.Type.UNKNOWN
+ DonationErrorSource.ONE_TIME -> InAppPaymentType.ONE_TIME_DONATION
+ DonationErrorSource.MONTHLY -> InAppPaymentType.RECURRING_DONATION
+ DonationErrorSource.GIFT -> InAppPaymentType.ONE_TIME_GIFT
+ DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentType.UNKNOWN
+ DonationErrorSource.KEEP_ALIVE -> InAppPaymentType.UNKNOWN
+ DonationErrorSource.UNKNOWN -> InAppPaymentType.UNKNOWN
}
}
@@ -249,6 +253,28 @@ object InAppPaymentsRepository {
}
}
+ fun InAppPaymentType.toErrorSource(): DonationErrorSource {
+ return when (this) {
+ InAppPaymentType.UNKNOWN -> DonationErrorSource.UNKNOWN
+ InAppPaymentType.ONE_TIME_GIFT -> DonationErrorSource.GIFT
+ InAppPaymentType.ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
+ InAppPaymentType.RECURRING_DONATION -> DonationErrorSource.MONTHLY
+ InAppPaymentType.RECURRING_BACKUP -> DonationErrorSource.UNKNOWN // TODO [message-backups] error handling
+ }
+ }
+
+ fun InAppPaymentType.toSubscriberType(): InAppPaymentSubscriberRecord.Type? {
+ return when (this) {
+ InAppPaymentType.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
+ InAppPaymentType.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
+ else -> null
+ }
+ }
+
+ fun InAppPaymentType.requireSubscriberType(): InAppPaymentSubscriberRecord.Type {
+ return requireNotNull(toSubscriberType())
+ }
+
/**
* Converts network ChargeFailure objects into the form we can persist in the database.
*/
@@ -375,6 +401,7 @@ object InAppPaymentsRepository {
@WorkerThread
fun getSubscriber(type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
val currency = SignalStore.donationsValues().getSubscriptionCurrency(type)
+ Log.d(TAG, "Attempting to retrieve subscriber of type $type for ${currency.currencyCode}")
return getSubscriber(currency, type)
}
@@ -408,13 +435,13 @@ object InAppPaymentsRepository {
/**
* Emits a stream of status updates for donations of the given type. Only One-time donations and recurring donations are currently supported.
*/
- fun observeInAppPaymentRedemption(type: InAppPaymentTable.Type): Observable {
+ fun observeInAppPaymentRedemption(type: InAppPaymentType): Observable {
val jobStatusObservable: Observable = when (type) {
- InAppPaymentTable.Type.UNKNOWN -> Observable.empty()
- InAppPaymentTable.Type.ONE_TIME_GIFT -> Observable.empty()
- InAppPaymentTable.Type.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption()
- InAppPaymentTable.Type.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption()
- InAppPaymentTable.Type.RECURRING_BACKUP -> Observable.empty()
+ InAppPaymentType.UNKNOWN -> Observable.empty()
+ InAppPaymentType.ONE_TIME_GIFT -> Observable.empty()
+ InAppPaymentType.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption()
+ InAppPaymentType.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption()
+ InAppPaymentType.RECURRING_BACKUP -> Observable.empty()
}
val fromDatabase: Observable = Observable.create { emitter ->
@@ -467,6 +494,17 @@ object InAppPaymentsRepository {
.distinctUntilChanged()
}
+ fun scheduleSyncForAccountRecordChange() {
+ SignalExecutors.BOUNDED.execute {
+ scheduleSyncForAccountRecordChangeSync()
+ }
+ }
+
+ private fun scheduleSyncForAccountRecordChangeSync() {
+ SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
+ StorageSyncHelper.scheduleSyncForDataChange()
+ }
+
private fun InAppPaymentTable.InAppPayment.toPendingOneTimeDonation(): PendingOneTimeDonation? {
if (type.recurring) {
return null
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt
index e76dc7e094..fab5918079 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt
@@ -6,6 +6,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.badges.Badges
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
@@ -96,7 +98,7 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
}
fun ensureSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type, isRotation: Boolean = false): Completable {
- Log.d(TAG, "Ensuring SubscriberId exists on Signal service {isRotation?$isRotation}...", true)
+ Log.d(TAG, "Ensuring SubscriberId for type $subscriberType exists on Signal service {isRotation?$isRotation}...", true)
val subscriberId: SubscriberId = if (isRotation) {
SubscriberId.generate()
} else {
@@ -138,7 +140,12 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
.subscribeOn(Schedulers.io())
.flatMap(ServiceResponse::flattenResult)
.ignoreElement()
- .doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) }
+ .doOnComplete {
+ Log.d(TAG, "Cancelled active subscription.", true)
+ SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
+ MultiDeviceSubscriptionSyncRequestJob.enqueue()
+ InAppPaymentsRepository.scheduleSyncForAccountRecordChange()
+ }
}
fun cancelActiveSubscriptionIfNecessary(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt
index a6949e884c..7e5e225eef 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt
@@ -5,14 +5,15 @@ import android.content.Intent
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
-import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
import org.signal.donations.json.StripeIntentStatus
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.OneTimeDonationError
@@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
-import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Environment
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret
import org.whispersystems.signalservice.internal.EmptyResponse
@@ -46,8 +46,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse
* 1. Confirm the PaymentIntent via the Stripe API
*/
class StripeRepository(
- activity: Activity,
- private val subscriberType: InAppPaymentSubscriberRecord.Type = InAppPaymentSubscriberRecord.Type.DONATION
+ activity: Activity
) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
@@ -58,17 +57,6 @@ class StripeRepository(
return googlePayApi.queryIsReadyToPay()
}
- fun scheduleSyncForAccountRecordChange() {
- SignalExecutors.BOUNDED.execute {
- scheduleSyncForAccountRecordChangeSync()
- }
- }
-
- private fun scheduleSyncForAccountRecordChangeSync() {
- SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
- StorageSyncHelper.scheduleSyncForDataChange()
- }
-
fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) {
Log.d(TAG, "Requesting a token from google pay...")
googlePayApi.requestPayment(price, label, requestCode)
@@ -118,11 +106,12 @@ class StripeRepository(
}
fun createAndConfirmSetupIntent(
+ inAppPaymentType: InAppPaymentType,
paymentSource: StripeApi.PaymentSource,
paymentSourceType: PaymentSourceType.Stripe
): Single {
Log.d(TAG, "Continuing subscription setup...", true)
- return stripeApi.createSetupIntent(paymentSourceType)
+ return stripeApi.createSetupIntent(inAppPaymentType, paymentSourceType)
.flatMap { result ->
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
stripeApi.confirmSetupIntent(paymentSource, result.setupIntent)
@@ -169,7 +158,7 @@ class StripeRepository(
* it means that the PaymentMethod is already tied to a PayPal account. We can retry in this
* situation by simply deleting the old subscriber id on the service and replacing it.
*/
- private fun createPaymentMethod(paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single {
+ private fun createPaymentMethod(subscriberType: InAppPaymentSubscriberRecord.Type, paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single {
return Single.fromCallable { InAppPaymentsRepository.requireSubscriber(subscriberType) }
.flatMap {
Single.fromCallable {
@@ -180,16 +169,16 @@ class StripeRepository(
}
.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
- recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(paymentSourceType, retryOn409 = false))
+ recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, paymentSourceType, retryOn409 = false))
} else {
serviceResponse.flattenResult()
}
}
}
- override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single {
+ override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single {
Log.d(TAG, "Fetching setup intent from Signal service...")
- return createPaymentMethod(sourceType)
+ return createPaymentMethod(inAppPaymentType.requireSubscriberType(), sourceType)
.map {
StripeIntentAccessor(
objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT,
@@ -231,6 +220,7 @@ class StripeRepository(
fun setDefaultPaymentMethod(
paymentMethodId: String,
setupIntentId: String,
+ subscriberType: InAppPaymentSubscriberRecord.Type,
paymentSourceType: PaymentSourceType
): Completable {
return Single.fromCallable {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt
index 49a23cee46..18fd88f7fd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt
@@ -5,9 +5,8 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.configure
-import org.thoughtcrime.securesms.util.fragments.requireListener
import java.util.Locale
/**
@@ -15,8 +14,6 @@ import java.util.Locale
*/
class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
- private lateinit var donationPaymentComponent: DonationPaymentComponent
-
private val viewModel: SetCurrencyViewModel by viewModels(
factoryProducer = {
val args = SetCurrencyFragmentArgs.fromBundle(requireArguments())
@@ -25,8 +22,6 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
- donationPaymentComponent = requireListener()
-
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
@@ -40,7 +35,7 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
summary = DSLSettingsText.from(currency.currencyCode),
onClick = {
viewModel.setSelectedCurrency(currency.currencyCode)
- donationPaymentComponent.stripeRepository.scheduleSyncForAccountRecordChange()
+ InAppPaymentsRepository.scheduleSyncForAccountRecordChange()
dismissAllowingStateLoss()
}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt
index c1dd9258ed..c7dfe8e39a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt
@@ -4,9 +4,10 @@ import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
-import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -16,7 +17,7 @@ import java.util.Currency
import java.util.Locale
class SetCurrencyViewModel(
- private val inAppPaymentType: InAppPaymentTable.Type,
+ private val inAppPaymentType: InAppPaymentType,
supportedCurrencyCodes: List
) : ViewModel() {
@@ -89,7 +90,7 @@ class SetCurrencyViewModel(
}
}
- class Factory(private val inAppPaymentType: InAppPaymentTable.Type, private val supportedCurrencyCodes: List) : ViewModelProvider.Factory {
+ class Factory(private val inAppPaymentType: InAppPaymentType, private val supportedCurrencyCodes: List) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return modelClass.cast(SetCurrencyViewModel(inAppPaymentType, supportedCurrencyCodes))!!
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivity.kt
new file mode 100644
index 0000000000..91fa3b6857
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivity.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.settings.app.subscription.donate
+
+import android.content.Context
+import android.content.Intent
+import androidx.fragment.app.Fragment
+import androidx.navigation.navArgs
+import io.reactivex.rxjava3.subjects.PublishSubject
+import io.reactivex.rxjava3.subjects.Subject
+import org.signal.donations.InAppPaymentType
+import org.thoughtcrime.securesms.components.FragmentWrapperActivity
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
+
+/**
+ * Home base for all checkout flows.
+ */
+class CheckoutFlowActivity : FragmentWrapperActivity(), InAppPaymentComponent {
+
+ companion object {
+ fun createIntent(context: Context, inAppPaymentType: InAppPaymentType): Intent {
+ return Intent(context, CheckoutFlowActivity::class.java).putExtras(
+ CheckoutFlowActivityArgs.Builder(inAppPaymentType).build().toBundle()
+ )
+ }
+ }
+
+ override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
+ override val googlePayResultPublisher: Subject = PublishSubject.create()
+
+ private val args by navArgs()
+
+ override fun getFragment(): Fragment {
+ return CheckoutNavHostFragment.create(args.inAppPaymentType)
+ }
+
+ @Suppress("DEPRECATION")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutNavHostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutNavHostFragment.kt
new file mode 100644
index 0000000000..07a9825c17
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutNavHostFragment.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.settings.app.subscription.donate
+
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import androidx.navigation.fragment.NavHostFragment
+import org.signal.core.util.getSerializableCompat
+import org.signal.donations.InAppPaymentType
+import org.thoughtcrime.securesms.R
+
+class CheckoutNavHostFragment : NavHostFragment() {
+
+ companion object {
+ private const val ARG_TYPE = "host_in_app_payment_type"
+
+ @JvmStatic
+ fun create(inAppPaymentType: InAppPaymentType): CheckoutNavHostFragment {
+ val actual = CheckoutNavHostFragment()
+ actual.arguments = bundleOf(ARG_TYPE to inAppPaymentType)
+
+ return actual
+ }
+ }
+
+ private val inAppPaymentType: InAppPaymentType
+ get() = requireArguments().getSerializableCompat(ARG_TYPE, InAppPaymentType::class.java)!!
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
+ val navGraph = navController.navInflater.inflate(R.navigation.checkout)
+ navGraph.setStartDestination(
+ when (inAppPaymentType) {
+ InAppPaymentType.UNKNOWN -> error("Unsupported start destination")
+ InAppPaymentType.ONE_TIME_GIFT -> R.id.giftFlowStartFragment
+ InAppPaymentType.ONE_TIME_DONATION, InAppPaymentType.RECURRING_DONATION -> R.id.donateToSignalFragment
+ InAppPaymentType.RECURRING_BACKUP -> R.id.messageBackupsFlowFragment
+ }
+ )
+
+ val startBundle = when (inAppPaymentType) {
+ InAppPaymentType.UNKNOWN -> error("Unknown payment type")
+ InAppPaymentType.ONE_TIME_GIFT, InAppPaymentType.RECURRING_BACKUP -> null
+ InAppPaymentType.ONE_TIME_DONATION, InAppPaymentType.RECURRING_DONATION -> DonateToSignalFragmentArgs.Builder(inAppPaymentType).build().toBundle()
+ }
+
+ navController.setGraph(navGraph, startBundle)
+ }
+
+ super.onCreate(savedInstanceState)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt
index b49a7b1996..c9f56cdebb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt
@@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.InAppPaymentTable
sealed class DonateToSignalAction {
- data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentTable.Type, val supportedCurrencies: List) : DonateToSignalAction()
+ data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentType, val supportedCurrencies: List) : DonateToSignalAction()
data class DisplayGatewaySelectorDialog(val inAppPayment: InAppPaymentTable.InAppPayment) : DonateToSignalAction()
object CancelSubscription : DonateToSignalAction()
data class UpdateSubscription(val inAppPayment: InAppPaymentTable.InAppPayment, val isLongRunning: Boolean) : DonateToSignalAction()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt
deleted file mode 100644
index dde2067f83..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalActivity.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.thoughtcrime.securesms.components.settings.app.subscription.donate
-
-import android.content.Intent
-import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.NavHostFragment
-import io.reactivex.rxjava3.subjects.PublishSubject
-import io.reactivex.rxjava3.subjects.Subject
-import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.components.FragmentWrapperActivity
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
-import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
-import org.thoughtcrime.securesms.database.InAppPaymentTable
-
-/**
- * Activity wrapper for donate to signal screen. An activity is needed because Google Pay uses the
- * activity [DonateToSignalActivity.startActivityForResult] flow that would be missed by a parent fragment.
- */
-class DonateToSignalActivity : FragmentWrapperActivity(), DonationPaymentComponent {
-
- override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
- override val googlePayResultPublisher: Subject = PublishSubject.create()
-
- override fun getFragment(): Fragment {
- return NavHostFragment.create(R.navigation.donate_to_signal, DonateToSignalFragmentArgs.Builder(InAppPaymentTable.Type.ONE_TIME_DONATION).build().toBundle())
- }
-
- @Suppress("DEPRECATION")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt
index a37d32ada3..a0ca88ec3b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt
@@ -5,19 +5,24 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
+import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
-import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
+import org.signal.core.util.getParcelableCompat
+import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.BadgePreview
@@ -28,13 +33,16 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
+import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.Projection
@@ -42,6 +50,7 @@ import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
+import java.math.BigDecimal
import java.util.Currency
/**
@@ -51,8 +60,8 @@ class DonateToSignalFragment :
DSLSettingsFragment(
layoutId = R.layout.donate_to_signal_fragment
),
- DonationCheckoutDelegate.Callback,
- ThanksForYourSupportBottomSheetDialogFragment.Callback {
+ ThanksForYourSupportBottomSheetDialogFragment.Callback,
+ DonationCheckoutDelegate.Callback {
companion object {
private val TAG = Log.tag(DonateToSignalFragment::class.java)
@@ -61,17 +70,19 @@ class DonateToSignalFragment :
class Dialog : WrapperDialogFragment() {
override fun getWrappedFragment(): Fragment {
- return NavHostFragment.create(
- R.navigation.donate_to_signal,
- arguments
+ return CheckoutNavHostFragment.create(
+ requireArguments().getSerializableCompat(ARG, InAppPaymentType::class.java)!!
)
}
companion object {
+
+ private const val ARG = "in_app_payment_type"
+
@JvmStatic
- fun create(inAppPaymentType: InAppPaymentTable.Type): DialogFragment {
+ fun create(inAppPaymentType: InAppPaymentType): DialogFragment {
return Dialog().apply {
- arguments = DonateToSignalFragmentArgs.Builder(inAppPaymentType).build().toBundle()
+ arguments = bundleOf(ARG to inAppPaymentType)
}
}
}
@@ -85,8 +96,6 @@ class DonateToSignalFragment :
private val disposables = LifecycleDisposable()
private val binding by ViewBinderDelegate(DonateToSignalFragmentBinding::bind)
- private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
-
private val supportTechSummary: CharSequence by lazy {
SpannableStringBuilder(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant), requireContext().getString(R.string.DonateToSignalFragment__private_messaging)))
.append(" ")
@@ -109,11 +118,7 @@ class DonateToSignalFragment :
}
override fun bindAdapter(adapter: MappingAdapter) {
- donationCheckoutDelegate = DonationCheckoutDelegate(
- this,
- this,
- viewModel.inAppPaymentId
- )
+ val checkoutDelegate = DonationCheckoutDelegate(this, this, viewModel.inAppPaymentId)
val recyclerView = this.recyclerView!!
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
@@ -137,11 +142,20 @@ class DonateToSignalFragment :
CurrencySelection.register(adapter)
DonationPillToggle.register(adapter)
+ setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
+ if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
+ showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
+ } else {
+ val inAppPayment: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, InAppPaymentTable.InAppPayment::class.java)!!
+ checkoutDelegate.handleGatewaySelectionResponse(inAppPayment)
+ }
+ }
+
disposables.bindTo(viewLifecycleOwner)
disposables += viewModel.actions.subscribe { action ->
when (action) {
is DonateToSignalAction.DisplayCurrencySelectionDialog -> {
- val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToSetDonationCurrencyFragment(
+ val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToSetCurrencyFragment(
action.inAppPaymentType,
action.supportedCurrencies.toTypedArray()
)
@@ -157,23 +171,27 @@ class DonateToSignalFragment :
}
is DonateToSignalAction.CancelSubscription -> {
- findNavController().safeNavigate(
- DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
- DonationProcessorAction.CANCEL_SUBSCRIPTION,
- null,
- InAppPaymentTable.Type.RECURRING_DONATION
- )
+ DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
+ DonationProcessorAction.CANCEL_SUBSCRIPTION,
+ null,
+ InAppPaymentType.RECURRING_DONATION
)
}
is DonateToSignalAction.UpdateSubscription -> {
- findNavController().safeNavigate(
+ if (action.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.PAYPAL) {
+ DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
+ DonationProcessorAction.UPDATE_SUBSCRIPTION,
+ action.inAppPayment,
+ action.inAppPayment.type
+ )
+ } else {
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
DonationProcessorAction.UPDATE_SUBSCRIPTION,
action.inAppPayment,
action.inAppPayment.type
)
- )
+ }
}
}
}
@@ -198,11 +216,6 @@ class DonateToSignalFragment :
}
}
- override fun onDestroyView() {
- super.onDestroyView()
- donationCheckoutDelegate = null
- }
-
private fun getConfiguration(state: DonateToSignalState): DSLConfiguration {
return configure {
space(36.dp)
@@ -251,14 +264,14 @@ class DonateToSignalFragment :
space(10.dp)
when (state.inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
- InAppPaymentTable.Type.RECURRING_DONATION -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
+ InAppPaymentType.ONE_TIME_DONATION -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
+ InAppPaymentType.RECURRING_DONATION -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
else -> error("This fragment does not support ${state.inAppPaymentType}.")
}
space(20.dp)
- if (state.inAppPaymentType == InAppPaymentTable.Type.RECURRING_DONATION && state.monthlyDonationState.isSubscriptionActive) {
+ if (state.inAppPaymentType == InAppPaymentType.RECURRING_DONATION && state.monthlyDonationState.isSubscriptionActive) {
primaryButton(
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
isEnabled = state.canUpdate,
@@ -324,7 +337,7 @@ class DonateToSignalFragment :
}
private fun showDonationPendingDialog(state: DonateToSignalState) {
- val message = if (state.inAppPaymentType == InAppPaymentTable.Type.ONE_TIME_DONATION) {
+ val message = if (state.inAppPaymentType == InAppPaymentType.ONE_TIME_DONATION) {
if (state.oneTimeDonationState.isOneTimeDonationLongRunning) {
R.string.DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_onetime
} else if (state.oneTimeDonationState.isNonVerifiedIdeal) {
@@ -444,8 +457,27 @@ class DonateToSignalFragment :
}
}
+ private fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) {
+ val max = FiatMoneyUtil.format(resources, sepaEuroMaximum, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.DonateToSignal__donation_amount_too_high)
+ .setMessage(getString(R.string.DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer, max))
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
+
+ override fun onBoostThanksSheetDismissed() {
+ findNavController().popBackStack()
+ }
+
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
- findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
+ findNavController().safeNavigate(
+ DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
+ DonationProcessorAction.PROCESS_NEW_DONATION,
+ inAppPayment,
+ inAppPayment.type
+ )
+ )
}
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
@@ -474,26 +506,19 @@ class DonateToSignalFragment :
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(Badges.fromDatabaseBadge(inAppPayment.data.badge!!)))
}
+ override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) {
+ Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
+ }
+
override fun onProcessorActionProcessed() {
- viewModel.refreshActiveSubscription()
+ // TODO [alex] - what did this used to do?
}
- override fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) {
- val max = FiatMoneyUtil.format(resources, sepaEuroMaximum, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.DonateToSignal__donation_amount_too_high)
- .setMessage(getString(R.string.DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer, max))
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ override fun onUserLaunchedAnExternalApplication() {
+ // TODO [alex] - what did this used to do?
}
- override fun onUserLaunchedAnExternalApplication() = Unit
-
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment))
}
-
- override fun onBoostThanksSheetDismissed() {
- findNavController().popBackStack()
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt
index c65454977a..c7642af844 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt
@@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
import org.thoughtcrime.securesms.database.model.isLongRunning
@@ -18,71 +18,71 @@ import java.util.Currency
import java.util.concurrent.TimeUnit
data class DonateToSignalState(
- val inAppPaymentType: InAppPaymentTable.Type,
+ val inAppPaymentType: InAppPaymentType,
val oneTimeDonationState: OneTimeDonationState = OneTimeDonationState(),
val monthlyDonationState: MonthlyDonationState = MonthlyDonationState()
) {
val areFieldsEnabled: Boolean
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.donationStage == DonationStage.READY
- InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.donationStage == DonationStage.READY
+ InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.donationStage == DonationStage.READY
+ InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.donationStage == DonationStage.READY
else -> error("This flow does not support $inAppPaymentType")
}
val badge: Badge?
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.badge
- InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription?.badge
+ InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.badge
+ InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedSubscription?.badge
else -> error("This flow does not support $inAppPaymentType")
}
val canSetCurrency: Boolean
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
- InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
+ InAppPaymentType.ONE_TIME_DONATION -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
+ InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
else -> error("This flow does not support $inAppPaymentType")
}
val selectedCurrency: Currency
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectedCurrency
- InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedCurrency
+ InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.selectedCurrency
+ InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedCurrency
else -> error("This flow does not support $inAppPaymentType")
}
val selectableCurrencyCodes: List
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectableCurrencyCodes
- InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectableCurrencyCodes
+ InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.selectableCurrencyCodes
+ InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectableCurrencyCodes
else -> error("This flow does not support $inAppPaymentType")
}
val level: Int
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> 1
- InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription!!.level
+ InAppPaymentType.ONE_TIME_DONATION -> 1
+ InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedSubscription!!.level
else -> error("This flow does not support $inAppPaymentType")
}
val continueEnabled: Boolean
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
- InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
+ InAppPaymentType.ONE_TIME_DONATION -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
+ InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
else -> error("This flow does not support $inAppPaymentType")
}
val canContinue: Boolean
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
- InAppPaymentTable.Type.RECURRING_DONATION -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
+ InAppPaymentType.ONE_TIME_DONATION -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
+ InAppPaymentType.RECURRING_DONATION -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
else -> error("This flow does not support $inAppPaymentType")
}
val canUpdate: Boolean
get() = when (inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> false
- InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid
+ InAppPaymentType.ONE_TIME_DONATION -> false
+ InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid
else -> error("This flow does not support $inAppPaymentType")
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt
index ec88733030..2a8246b304 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt
@@ -16,6 +16,7 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.core.util.money.PlatformCurrencyUtil
import org.signal.core.util.orNull
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
@@ -50,7 +51,7 @@ import java.util.Optional
* only in charge of rendering our "current view of the world."
*/
class DonateToSignalViewModel(
- startType: InAppPaymentTable.Type,
+ startType: InAppPaymentType,
private val subscriptionsRepository: RecurringInAppPaymentRepository,
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
) : ViewModel() {
@@ -137,8 +138,8 @@ class DonateToSignalViewModel(
store.update {
it.copy(
inAppPaymentType = when (it.inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> InAppPaymentTable.Type.RECURRING_DONATION
- InAppPaymentTable.Type.RECURRING_DONATION -> InAppPaymentTable.Type.ONE_TIME_DONATION
+ InAppPaymentType.ONE_TIME_DONATION -> InAppPaymentType.RECURRING_DONATION
+ InAppPaymentType.RECURRING_DONATION -> InAppPaymentType.ONE_TIME_DONATION
else -> error("Should never get here.")
}
)
@@ -222,8 +223,8 @@ class DonateToSignalViewModel(
private fun getAmount(snapshot: DonateToSignalState): FiatMoney {
return when (snapshot.inAppPaymentType) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> getOneTimeAmount(snapshot.oneTimeDonationState)
- InAppPaymentTable.Type.RECURRING_DONATION -> getSelectedSubscriptionCost()
+ InAppPaymentType.ONE_TIME_DONATION -> getOneTimeAmount(snapshot.oneTimeDonationState)
+ InAppPaymentType.RECURRING_DONATION -> getSelectedSubscriptionCost()
else -> error("This ViewModel does not support ${snapshot.inAppPaymentType}.")
}
}
@@ -237,7 +238,7 @@ class DonateToSignalViewModel(
}
private fun initializeOneTimeDonationState(oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository) {
- val oneTimeDonationFromJob: Observable> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION).map {
+ val oneTimeDonationFromJob: Observable> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION).map {
when (it) {
is DonationRedemptionJobStatus.PendingExternalVerification -> Optional.ofNullable(it.pendingOneTimeDonation)
@@ -331,7 +332,7 @@ class DonateToSignalViewModel(
}
private fun monitorLevelUpdateProcessing() {
- val redemptionJobStatus: Observable = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION)
+ val redemptionJobStatus: Observable = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
monthlyDonationDisposables += Observable
.combineLatest(redemptionJobStatus, LevelUpdate.isProcessing, ::Pair)
@@ -420,7 +421,7 @@ class DonateToSignalViewModel(
}
class Factory(
- private val startType: InAppPaymentTable.Type,
+ private val startType: InAppPaymentType,
private val subscriptionsRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService),
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(AppDependencies.donationsService)
) : ViewModelProvider.Factory {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt
index 4b1a441b4f..0e82131462 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt
@@ -11,7 +11,6 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.google.android.gms.wallet.PaymentData
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
@@ -20,16 +19,15 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
-import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment
-import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
@@ -40,9 +38,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
-import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
-import java.math.BigDecimal
/**
* Abstracts out some common UI-level interactions between gift flow and normal donate flow.
@@ -57,15 +53,14 @@ class DonationCheckoutDelegate(
private val TAG = Log.tag(DonationCheckoutDelegate::class.java)
}
- private lateinit var donationPaymentComponent: DonationPaymentComponent
+ private val inAppPaymentComponent: InAppPaymentComponent by lazy { fragment.requireListener() }
private val disposables = LifecycleDisposable()
private val viewModel: DonationCheckoutViewModel by fragment.viewModels()
private val stripePaymentViewModel: StripePaymentInProgressViewModel by fragment.navGraphViewModels(
- R.id.donate_to_signal,
+ R.id.checkout_flow,
factoryProducer = {
- donationPaymentComponent = fragment.requireListener()
- StripePaymentInProgressViewModel.Factory(donationPaymentComponent.stripeRepository)
+ StripePaymentInProgressViewModel.Factory(inAppPaymentComponent.stripeRepository)
}
)
@@ -76,18 +71,8 @@ class DonationCheckoutDelegate(
override fun onCreate(owner: LifecycleOwner) {
disposables.bindTo(fragment.viewLifecycleOwner)
- donationPaymentComponent = fragment.requireListener()
registerGooglePayCallback()
- fragment.setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
- if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
- callback.showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
- } else {
- val inAppPayment: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, InAppPaymentTable.InAppPayment::class.java)!!
- handleGatewaySelectionResponse(inAppPayment)
- }
- }
-
fragment.setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
handleDonationProcessorActionResult(result)
@@ -114,7 +99,7 @@ class DonationCheckoutDelegate(
}
}
- private fun handleGatewaySelectionResponse(inAppPayment: InAppPaymentTable.InAppPayment) {
+ fun handleGatewaySelectionResponse(inAppPayment: InAppPaymentTable.InAppPayment) {
if (InAppDonations.isPaymentSourceAvailable(inAppPayment.data.paymentMethodType.toPaymentSourceType(), inAppPayment.type)) {
when (inAppPayment.data.paymentMethodType) {
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> launchGooglePay(inAppPayment)
@@ -140,7 +125,7 @@ class DonationCheckoutDelegate(
private fun handleSuccessfulDonationProcessorActionResult(result: DonationProcessorActionResult) {
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
- Snackbar.make(fragment.requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
+ callback.onSubscriptionCancelled(result.inAppPaymentType)
} else {
callback.onPaymentComplete(result.inAppPayment!!)
}
@@ -152,7 +137,7 @@ class DonationCheckoutDelegate(
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
.setPositiveButton(android.R.string.ok) { _, _ ->
- fragment.findNavController().popBackStack()
+ fragment.findNavController().popBackStack(R.id.checkout_flow, true)
}
.show()
} else {
@@ -166,7 +151,7 @@ class DonationCheckoutDelegate(
private fun launchGooglePay(inAppPayment: InAppPaymentTable.InAppPayment) {
viewModel.provideGatewayRequestForGooglePay(inAppPayment)
- donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
+ inAppPaymentComponent.stripeRepository.requestTokenFromGooglePay(
price = inAppPayment.data.amount!!.toFiatMoney(),
label = inAppPayment.data.label,
requestCode = InAppPaymentsRepository.getGooglePayRequestCode(inAppPayment.type)
@@ -186,10 +171,10 @@ class DonationCheckoutDelegate(
}
private fun registerGooglePayCallback() {
- disposables += donationPaymentComponent.googlePayResultPublisher.subscribeBy(
+ disposables += inAppPaymentComponent.googlePayResultPublisher.subscribeBy(
onNext = { paymentResult ->
viewModel.consumeGatewayRequestForGooglePay()?.let {
- donationPaymentComponent.stripeRepository.onActivityResult(
+ inAppPaymentComponent.stripeRepository.onActivityResult(
paymentResult.requestCode,
paymentResult.resultCode,
paymentResult.data,
@@ -366,7 +351,7 @@ class DonationCheckoutDelegate(
errorDialog = null
if (!tryAgain) {
tryAgain = false
- fragment?.findNavController()?.popBackStack()
+ fragment?.findNavController()?.popBackStack(R.id.checkout_flow, true)
}
}
}
@@ -384,7 +369,7 @@ class DonationCheckoutDelegate(
fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment)
fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment)
fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment)
+ fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType)
fun onProcessorActionProcessed()
- fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt
index 211781bb4a..dda417756c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt
@@ -1,8 +1,8 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import com.google.android.material.button.MaterialButton
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.databinding.DonationPillToggleBinding
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
@@ -16,7 +16,7 @@ object DonationPillToggle {
}
class Model(
- val selected: InAppPaymentTable.Type,
+ val selected: InAppPaymentType,
val onClick: () -> Unit
) : MappingModel {
override fun areItemsTheSame(newItem: Model): Boolean = true
@@ -29,10 +29,10 @@ object DonationPillToggle {
private class ViewHolder(binding: DonationPillToggleBinding) : BindingViewHolder(binding) {
override fun bind(model: Model) {
when (model.selected) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> {
+ InAppPaymentType.ONE_TIME_DONATION -> {
presentButtons(model, binding.oneTime, binding.monthly)
}
- InAppPaymentTable.Type.RECURRING_DONATION -> {
+ InAppPaymentType.RECURRING_DONATION -> {
presentButtons(model, binding.monthly, binding.oneTime)
}
else -> {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt
index fbb91b9b89..9590e44a64 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationProcessorActionResult.kt
@@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.InAppPaymentTable
@Parcelize
class DonationProcessorActionResult(
val action: DonationProcessorAction,
val inAppPayment: InAppPaymentTable.InAppPayment?,
+ val inAppPaymentType: InAppPaymentType,
val status: Status
) : Parcelable {
enum class Status {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt
index 3b48df6203..c0769fa895 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt
@@ -16,17 +16,17 @@ import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableCompat
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
import org.thoughtcrime.securesms.components.ViewBinderDelegate
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.ViewUtil
@@ -40,9 +40,9 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
private val viewModel: CreditCardViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
- R.id.donate_to_signal,
+ R.id.checkout_flow,
factoryProducer = {
- StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
+ StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
}
)
@@ -59,7 +59,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
}
// TODO [message-backups] Copy for this button in backups checkout flow.
- binding.continueButton.text = if (args.inAppPayment.type == InAppPaymentTable.Type.RECURRING_DONATION) {
+ binding.continueButton.text = if (args.inAppPayment.type == InAppPaymentType.RECURRING_DONATION) {
getString(
R.string.CreditCardFragment__donate_s_month,
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt
index 7ba7712119..a6614e38c4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt
@@ -10,6 +10,7 @@ import androidx.navigation.fragment.navArgs
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
@@ -19,8 +20,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.NO_TINT
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
import org.thoughtcrime.securesms.components.settings.configure
@@ -41,7 +42,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
private val args: GatewaySelectorBottomSheetArgs by navArgs()
private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = {
- GatewaySelectorViewModel.Factory(args, requireListener().stripeRepository)
+ GatewaySelectorViewModel.Factory(args, requireListener().stripeRepository)
})
override fun bindAdapter(adapter: DSLSettingsAdapter) {
@@ -206,11 +207,11 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
when (inAppPayment.type) {
- InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
- InAppPaymentTable.Type.RECURRING_BACKUP -> error("This type is not supported") // TODO [message-backups] necessary?
- InAppPaymentTable.Type.RECURRING_DONATION -> presentMonthlyText(context, inAppPayment)
- InAppPaymentTable.Type.ONE_TIME_DONATION -> presentOneTimeText(context, inAppPayment)
- InAppPaymentTable.Type.ONE_TIME_GIFT -> presentGiftText(context, inAppPayment)
+ InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
+ InAppPaymentType.RECURRING_BACKUP -> error("This type is not supported") // TODO [message-backups] necessary?
+ InAppPaymentType.RECURRING_DONATION -> presentMonthlyText(context, inAppPayment)
+ InAppPaymentType.ONE_TIME_DONATION -> presentOneTimeText(context, inAppPayment)
+ InAppPaymentType.ONE_TIME_GIFT -> presentGiftText(context, inAppPayment)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt
index 34c0cbde73..ed6dba9248 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt
@@ -22,6 +22,7 @@ import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
@@ -44,7 +45,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
private val args: PayPalPaymentInProgressFragmentArgs by navArgs()
- private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.donate_to_signal, factoryProducer = {
+ private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.checkout_flow, factoryProducer = {
PayPalPaymentInProgressViewModel.Factory()
})
@@ -62,9 +63,11 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
DonationProcessorAction.PROCESS_NEW_DONATION -> {
viewModel.processNewDonation(args.inAppPayment!!, this::oneTimeConfirmationPipeline, this::monthlyConfirmationPipeline)
}
+
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
viewModel.updateSubscription(args.inAppPayment!!)
}
+
DonationProcessorAction.CANCEL_SUBSCRIPTION -> {
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION) // TODO [message-backups] Remove hardcode
}
@@ -90,11 +93,13 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
REQUEST_KEY to DonationProcessorActionResult(
action = args.action,
inAppPayment = args.inAppPayment,
+ inAppPaymentType = args.inAppPaymentType,
status = DonationProcessorActionResult.Status.FAILURE
)
)
)
}
+
DonationProcessorStage.COMPLETE -> {
viewModel.onEndAction()
findNavController().popBackStack()
@@ -104,11 +109,13 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
REQUEST_KEY to DonationProcessorActionResult(
action = args.action,
inAppPayment = args.inAppPayment,
+ inAppPaymentType = args.inAppPaymentType,
status = DonationProcessorActionResult.Status.SUCCESS
)
)
)
}
+
DonationProcessorStage.CANCELLING -> binding.progressCardStatus.setText(R.string.StripePaymentInProgressFragment__cancelling)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt
index 06e6496efd..36bc31056d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt
@@ -11,9 +11,11 @@ import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
@@ -123,7 +125,7 @@ class PayPalPaymentInProgressViewModel(
) {
Log.d(TAG, "Proceeding with one-time payment pipeline...", true)
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
- val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
+ val verifyUser = if (inAppPayment.type == InAppPaymentType.ONE_TIME_GIFT) {
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(RecipientId.from(inAppPayment.data.recipientId!!))
} else {
Completable.complete()
@@ -168,7 +170,7 @@ class PayPalPaymentInProgressViewModel(
}
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single) {
- Log.d(TAG, "Proceeding with monthly payment pipeline...")
+ Log.d(TAG, "Proceeding with monthly payment pipeline for InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
val setup = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt
index 14fba3352f..33cc4dfeef 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.s
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
@@ -43,12 +44,12 @@ data class Stripe3DSData(
intentClientSecret = stripeIntentAccessor.intentClientSecret
),
gatewayRequest = ExternalLaunchTransactionState.GatewayRequest(
- donateToSignalType = when (inAppPayment.type) {
- InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
- InAppPaymentTable.Type.ONE_TIME_DONATION -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME
- InAppPaymentTable.Type.RECURRING_DONATION -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY
- InAppPaymentTable.Type.ONE_TIME_GIFT -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT
- InAppPaymentTable.Type.RECURRING_BACKUP -> error("Unimplemented") // TODO [message-backups] do we still need this?
+ inAppPaymentType = when (inAppPayment.type) {
+ InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
+ InAppPaymentType.ONE_TIME_DONATION -> ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.ONE_TIME_DONATION
+ InAppPaymentType.RECURRING_DONATION -> ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.RECURRING_DONATION
+ InAppPaymentType.ONE_TIME_GIFT -> ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.ONE_TIME_GIFT
+ InAppPaymentType.RECURRING_BACKUP -> ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.RECURRING_BACKUPS
},
badge = inAppPayment.data.badge,
label = inAppPayment.data.label,
@@ -76,11 +77,11 @@ data class Stripe3DSData(
),
inAppPayment = InAppPaymentTable.InAppPayment(
id = InAppPaymentTable.InAppPaymentId(-1), // TODO [alex] -- can we start writing this in for new transactions?
- type = when (proto.gatewayRequest!!.donateToSignalType) {
- ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY -> InAppPaymentTable.Type.RECURRING_DONATION
- ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME -> InAppPaymentTable.Type.ONE_TIME_DONATION
- ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT -> InAppPaymentTable.Type.ONE_TIME_GIFT
- // TODO [message-backups] -- Backups?
+ type = when (proto.gatewayRequest!!.inAppPaymentType) {
+ ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.RECURRING_DONATION -> InAppPaymentType.RECURRING_DONATION
+ ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.ONE_TIME_DONATION -> InAppPaymentType.ONE_TIME_DONATION
+ ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.ONE_TIME_GIFT -> InAppPaymentType.ONE_TIME_GIFT
+ ExternalLaunchTransactionState.GatewayRequest.InAppPaymentType.RECURRING_BACKUPS -> InAppPaymentType.RECURRING_BACKUP
},
endOfPeriod = 0.milliseconds,
updatedAt = 0.milliseconds,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt
index 5dbb0e9b40..d59ac013bf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt
@@ -23,7 +23,9 @@ import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
@@ -46,9 +48,9 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
private val disposables = LifecycleDisposable()
private val viewModel: StripePaymentInProgressViewModel by navGraphViewModels(
- R.id.donate_to_signal,
+ R.id.checkout_flow,
factoryProducer = {
- StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
+ StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
}
)
@@ -94,6 +96,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
REQUEST_KEY to DonationProcessorActionResult(
action = args.action,
inAppPayment = args.inAppPayment,
+ inAppPaymentType = args.inAppPaymentType,
status = DonationProcessorActionResult.Status.FAILURE
)
)
@@ -108,6 +111,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
REQUEST_KEY to DonationProcessorActionResult(
action = args.action,
inAppPayment = args.inAppPayment,
+ inAppPaymentType = args.inAppPaymentType,
status = DonationProcessorActionResult.Status.SUCCESS
)
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt
index b858ad3182..bdc80099ee 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt
@@ -13,11 +13,14 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.donations.GooglePayPaymentSource
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
@@ -30,8 +33,6 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
-import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
-import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
@@ -76,7 +77,7 @@ class StripePaymentInProgressViewModel(
}
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, nextActionHandler: StripeNextActionHandler) {
- Log.d(TAG, "Proceeding with donation...", true)
+ Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(inAppPayment.type.toErrorSource())
@@ -145,7 +146,7 @@ class StripePaymentInProgressViewModel(
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: StripeNextActionHandler) {
val ensureSubscriberId: Completable = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
val createAndConfirmSetupIntent: Single = paymentSourceProvider.paymentSource.flatMap {
- stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
+ stripeRepository.createAndConfirmSetupIntent(inAppPayment.type, it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
}
val setLevel: Completable = recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceProvider.paymentSourceType)
@@ -171,7 +172,7 @@ class StripePaymentInProgressViewModel(
)
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, secure3DSAction.paymentMethodId) }
}
- .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, paymentSourceProvider.paymentSourceType) }
+ .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, inAppPayment.type.requireSubscriberType(), paymentSourceProvider.paymentSourceType) }
.onErrorResumeNext {
when (it) {
is DonationError -> Completable.error(it)
@@ -202,7 +203,7 @@ class StripePaymentInProgressViewModel(
val amount = inAppPayment.data.amount!!.toFiatMoney()
val recipientId = inAppPayment.data.recipientId?.let { RecipientId.from(it) } ?: Recipient.self().id
- val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
+ val verifyUser = if (inAppPayment.type == InAppPaymentType.ONE_TIME_GIFT) {
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(recipientId)
} else {
Completable.complete()
@@ -257,9 +258,6 @@ class StripePaymentInProgressViewModel(
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
onComplete = {
Log.d(TAG, "Cancellation succeeded", true)
- SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
- MultiDeviceSubscriptionSyncRequestJob.enqueue()
- stripeRepository.scheduleSyncForAccountRecordChange()
store.update { DonationProcessorStage.COMPLETE }
},
onError = { throwable ->
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt
index 9b6de984b6..c592eb1679 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt
@@ -55,8 +55,8 @@ import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
@@ -80,9 +80,9 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
private val viewModel: BankTransferDetailsViewModel by viewModels()
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
- R.id.donate_to_signal,
+ R.id.checkout_flow,
factoryProducer = {
- StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
+ StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt
index 61ead332cc..b677f97741 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt
@@ -56,8 +56,8 @@ import org.signal.core.ui.Texts
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
@@ -84,9 +84,9 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
}
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
- R.id.donate_to_signal,
+ R.id.checkout_flow,
factoryProducer = {
- StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
+ StripePaymentInProgressViewModel.Factory(requireListener().stripeRepository)
}
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt
index 2c737689be..98497fd5e9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.content.Context
import androidx.annotation.StringRes
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
@@ -38,7 +39,7 @@ class DonationErrorParams private constructor(
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> getStillProcessingErrorParams(context, callback)
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> getBadgeCredentialValidationErrorParams(context, callback)
is DonationError.BadgeRedemptionError.GenericError -> getGenericRedemptionError(context, throwable.source.toInAppPaymentType(), callback)
- else -> getGenericRedemptionError(context, InAppPaymentTable.Type.ONE_TIME_DONATION, callback)
+ else -> getGenericRedemptionError(context, InAppPaymentType.ONE_TIME_DONATION, callback)
}
}
@@ -81,9 +82,9 @@ class DonationErrorParams private constructor(
}
}
- private fun getGenericRedemptionError(context: Context, type: InAppPaymentTable.Type, callback: Callback): DonationErrorParams {
+ private fun getGenericRedemptionError(context: Context, type: InAppPaymentType, callback: Callback): DonationErrorParams {
return when (type) {
- InAppPaymentTable.Type.ONE_TIME_GIFT -> DonationErrorParams(
+ InAppPaymentType.ONE_TIME_GIFT -> DonationErrorParams(
title = R.string.DonationsErrors__donation_failed,
message = R.string.DonationsErrors__your_payment_was_processed_but,
positiveAction = callback.onContactSupport(context),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt
index dfd9f9c5bc..92036065fd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
-import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.View
@@ -11,9 +10,9 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.dp
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheet
-import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
@@ -26,7 +25,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.completed
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -167,7 +165,7 @@ class ManageDonationsFragment :
primaryWrappedButton(
text = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_to_signal),
onClick = {
- findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.ONE_TIME_DONATION))
+ findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentType.ONE_TIME_DONATION))
}
)
@@ -277,7 +275,7 @@ class ManageDonationsFragment :
subscriberRequiresCancel = state.subscriberRequiresCancel,
onRowClick = {
if (it != ManageDonationsState.RedemptionState.IN_PROGRESS) {
- findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.RECURRING_DONATION))
+ findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentType.RECURRING_DONATION))
}
},
onPendingClick = {
@@ -345,7 +343,7 @@ class ManageDonationsFragment :
title = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_for_a_friend),
icon = DSLSettingsIcon.from(R.drawable.symbol_gift_24),
onClick = {
- startActivity(Intent(requireContext(), GiftFlowActivity::class.java))
+ findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentType.ONE_TIME_GIFT))
}
)
}
@@ -445,6 +443,6 @@ class ManageDonationsFragment :
}
override fun onMakeAMonthlyDonation() {
- findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.RECURRING_DONATION))
+ findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentType.RECURRING_DONATION))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt
index 9c7278862c..fe2e3e6f10 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt
@@ -12,9 +12,9 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -87,7 +87,7 @@ class ManageDonationsViewModel(
store.update { it.copy(hasReceipts = hasReceipts) }
}
- disposables += InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION).subscribeBy { redemptionStatus ->
+ disposables += InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION).subscribeBy { redemptionStatus ->
store.update { manageDonationsState ->
manageDonationsState.copy(
nonVerifiedMonthlyDonation = if (redemptionStatus is DonationRedemptionJobStatus.PendingExternalVerification) redemptionStatus.nonVerifiedMonthlyDonation else null,
@@ -98,7 +98,7 @@ class ManageDonationsViewModel(
disposables += Observable.combineLatest(
SignalStore.donationsValues().observablePendingOneTimeDonation,
- InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION)
+ InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION)
) { pendingFromStore, pendingFromJob ->
if (pendingFromStore.isPresent) {
pendingFromStore
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt
index 0cc904aa8b..c3fd893a7d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt
@@ -9,7 +9,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
@@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
/**
* Wrapper activity for ConversationFragment.
*/
-open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
+open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, InAppPaymentComponent {
companion object {
private const val STATE_WATERMARK = "share_data_watermark"
@@ -33,7 +33,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
override val voiceNoteMediaController = VoiceNoteMediaController(this, true)
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
- override val googlePayResultPublisher: Subject = PublishSubject.create()
+ override val googlePayResultPublisher: Subject = PublishSubject.create()
private val motionEventRelay: MotionEventRelay by viewModels()
private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels()
@@ -87,7 +87,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
+ googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
}
private fun replaceFragment() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
index 9f8c11b21b..409f6ba6a9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
@@ -92,6 +92,7 @@ import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.core.util.setActionItemTint
+import org.signal.donations.InAppPaymentType
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.GroupMembersDialog
@@ -102,7 +103,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.badges.gifts.OpenableGift
import org.thoughtcrime.securesms.badges.gifts.OpenableGiftItemDecoration
-import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
import org.thoughtcrime.securesms.components.AnimatingToggle
@@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.components.location.SignalPlace
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
@@ -196,7 +197,6 @@ import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment
import org.thoughtcrime.securesms.database.DraftTable
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.Mention
@@ -2926,7 +2926,7 @@ class ConversationFragment :
override fun onCallToAction(action: String) {
if ("gift_badge" == action) {
- startActivity(Intent(requireContext(), GiftFlowActivity::class.java))
+ startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.ONE_TIME_GIFT))
} else if ("username_edit" == action) {
startActivity(EditProfileActivity.getIntentForUsernameEdit(requireContext()))
}
@@ -2936,7 +2936,7 @@ class ConversationFragment :
requireActivity()
.supportFragmentManager
.beginTransaction()
- .add(DonateToSignalFragment.Dialog.create(InAppPaymentTable.Type.ONE_TIME_DONATION), "one_time_nav")
+ .add(DonateToSignalFragment.Dialog.create(InAppPaymentType.ONE_TIME_DONATION), "one_time_nav")
.commitNow()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt
index 95a2e7c2c2..77e0312144 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt
@@ -30,8 +30,7 @@ import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.updateAll
import org.signal.core.util.withinTransaction
-import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
-import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.parcelers.MillisecondDurationParceler
@@ -129,7 +128,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
}
fun insert(
- type: Type,
+ type: InAppPaymentType,
state: State,
subscriberId: SubscriberId?,
endOfPeriod: Duration?,
@@ -193,18 +192,18 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
.readToSingleObject(InAppPayment.Companion)
}
- fun getByEndOfPeriod(type: Type, endOfPeriod: Duration): InAppPayment? {
+ fun getByEndOfPeriod(type: InAppPaymentType, endOfPeriod: Duration): InAppPayment? {
return readableDatabase.select()
.from(TABLE_NAME)
- .where("$TYPE = ? AND $END_OF_PERIOD = ?", Type.serialize(type), endOfPeriod.inWholeSeconds)
+ .where("$TYPE = ? AND $END_OF_PERIOD = ?", InAppPaymentType.serialize(type), endOfPeriod.inWholeSeconds)
.run()
.readToSingleObject(InAppPayment.Companion)
}
- fun getByLatestEndOfPeriod(type: Type): InAppPayment? {
+ fun getByLatestEndOfPeriod(type: InAppPaymentType): InAppPayment? {
return readableDatabase.select()
.from(TABLE_NAME)
- .where("$TYPE = ? AND $END_OF_PERIOD > 0", Type.serialize(type))
+ .where("$TYPE = ? AND $END_OF_PERIOD > 0", InAppPaymentType.serialize(type))
.orderBy("$END_OF_PERIOD DESC")
.limit(1)
.run()
@@ -248,9 +247,9 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
.where(
"$STATE = ? AND ($TYPE = ? OR $TYPE = ? OR $TYPE = ?)",
State.serialize(State.PENDING),
- Type.serialize(Type.RECURRING_DONATION),
- Type.serialize(Type.ONE_TIME_DONATION),
- Type.serialize(Type.ONE_TIME_GIFT)
+ InAppPaymentType.serialize(InAppPaymentType.RECURRING_DONATION),
+ InAppPaymentType.serialize(InAppPaymentType.ONE_TIME_DONATION),
+ InAppPaymentType.serialize(InAppPaymentType.ONE_TIME_GIFT)
)
.run()
}
@@ -258,12 +257,12 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
/**
* Returns whether there are any pending donations in the database.
*/
- fun hasPending(type: Type): Boolean {
+ fun hasPending(type: InAppPaymentType): Boolean {
return readableDatabase.exists(TABLE_NAME)
.where(
"$STATE = ? AND $TYPE = ?",
State.serialize(State.PENDING),
- Type.serialize(type)
+ InAppPaymentType.serialize(type)
)
.run()
}
@@ -271,7 +270,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
/**
* Retrieves from the database the latest payment of the given type that is either in the PENDING or WAITING_FOR_AUTHORIZATION state.
*/
- fun getLatestInAppPaymentByType(type: Type): InAppPayment? {
+ fun getLatestInAppPaymentByType(type: InAppPaymentType): InAppPayment? {
return readableDatabase.select()
.from(TABLE_NAME)
.where(
@@ -279,7 +278,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
State.serialize(State.PENDING),
State.serialize(State.WAITING_FOR_AUTHORIZATION),
State.serialize(State.END),
- Type.serialize(type)
+ InAppPaymentType.serialize(type)
)
.orderBy("$INSERTED_AT DESC")
.limit(1)
@@ -311,7 +310,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
@TypeParceler
data class InAppPayment(
val id: InAppPaymentId,
- val type: Type,
+ val type: InAppPaymentType,
val state: State,
val insertedAt: Duration,
val updatedAt: Duration,
@@ -329,7 +328,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
override fun serialize(data: InAppPayment): ContentValues {
return contentValuesOf(
ID to data.id.serialize(),
- TYPE to data.type.apply { check(this != Type.UNKNOWN) }.code,
+ TYPE to data.type.apply { check(this != InAppPaymentType.UNKNOWN) }.code,
STATE to data.state.code,
INSERTED_AT to data.insertedAt.inWholeSeconds,
UPDATED_AT to data.updatedAt.inWholeSeconds,
@@ -343,7 +342,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
override fun deserialize(input: Cursor): InAppPayment {
return InAppPayment(
id = InAppPaymentId(input.requireLong(ID)),
- type = Type.deserialize(input.requireInt(TYPE)),
+ type = InAppPaymentType.deserialize(input.requireInt(TYPE)),
state = State.deserialize(input.requireInt(STATE)),
insertedAt = input.requireLong(INSERTED_AT).seconds,
updatedAt = input.requireLong(UPDATED_AT).seconds,
@@ -356,61 +355,6 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
}
}
- enum class Type(val code: Int, val recurring: Boolean) {
- /**
- * Used explicitly for mapping DonationErrorSource. Writing this value
- * into an InAppPayment is an error.
- */
- UNKNOWN(-1, false),
-
- /**
- * This payment is for a gift badge
- */
- ONE_TIME_GIFT(0, false),
-
- /**
- * This payment is for a one-time donation
- */
- ONE_TIME_DONATION(1, false),
-
- /**
- * This payment is for a recurring donation
- */
- RECURRING_DONATION(2, true),
-
- /**
- * This payment is for a recurring backup payment
- */
- RECURRING_BACKUP(3, true);
-
- companion object : Serializer {
- override fun serialize(data: Type): Int = data.code
- override fun deserialize(input: Int): Type = values().first { it.code == input }
- }
-
- fun toErrorSource(): DonationErrorSource {
- return when (this) {
- UNKNOWN -> DonationErrorSource.UNKNOWN
- ONE_TIME_GIFT -> DonationErrorSource.GIFT
- ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
- RECURRING_DONATION -> DonationErrorSource.MONTHLY
- RECURRING_BACKUP -> DonationErrorSource.UNKNOWN // TODO [message-backups] error handling
- }
- }
-
- fun toSubscriberType(): InAppPaymentSubscriberRecord.Type? {
- return when (this) {
- RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
- RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
- else -> null
- }
- }
-
- fun requireSubscriberType(): InAppPaymentSubscriberRecord.Type {
- return requireNotNull(toSubscriberType())
- }
- }
-
/**
* Represents the payment pipeline state for a given in-app payment
*
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InAppPaymentSubscriberRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/InAppPaymentSubscriberRecord.kt
index acce182bf6..522ee8f552 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/InAppPaymentSubscriberRecord.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InAppPaymentSubscriberRecord.kt
@@ -5,7 +5,7 @@
package org.thoughtcrime.securesms.database.model
-import org.thoughtcrime.securesms.database.InAppPaymentTable
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
@@ -24,15 +24,15 @@ data class InAppPaymentSubscriberRecord(
/**
* Serves as the mutex by which to perform mutations to subscriptions.
*/
- enum class Type(val code: Int, val jobQueue: String, val inAppPaymentType: InAppPaymentTable.Type) {
+ enum class Type(val code: Int, val jobQueue: String, val inAppPaymentType: InAppPaymentType) {
/**
* A recurring donation
*/
- DONATION(0, "recurring-donations", InAppPaymentTable.Type.RECURRING_DONATION),
+ DONATION(0, "recurring-donations", InAppPaymentType.RECURRING_DONATION),
/**
* A recurring backups subscription
*/
- BACKUP(1, "recurring-backups", InAppPaymentTable.Type.RECURRING_BACKUP)
+ BACKUP(1, "recurring-backups", InAppPaymentType.RECURRING_BACKUP)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java
index 7556d2fbbd..a4b226bcdd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java
@@ -193,7 +193,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
Log.d(TAG, "Attempting to redeem token... isForSubscription: " + isForSubscription(), true);
ServiceResponse response = AppDependencies.getDonationsService()
- .redeemReceipt(presentation,
+ .redeemDonationReceipt(presentation,
SignalStore.donationsValues().getDisplayBadgesOnProfile(),
makePrimary);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt
index 2ef967edfd..3ab21e10d9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobs
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
@@ -15,11 +16,11 @@ import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.Companion.toDonationErrorValue
-import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
@@ -68,7 +69,7 @@ class ExternalLaunchDonationJob private constructor(
override fun onFailure() {
if (donationError != null) {
when (stripe3DSData.inAppPayment.type) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> {
+ InAppPaymentType.ONE_TIME_DONATION -> {
SignalStore.donationsValues().setPendingOneTimeDonation(
DonationSerializationHelper.createPendingOneTimeDonationProto(
Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!),
@@ -80,7 +81,7 @@ class ExternalLaunchDonationJob private constructor(
)
}
- InAppPaymentTable.Type.RECURRING_DONATION -> {
+ InAppPaymentType.RECURRING_DONATION -> {
SignalStore.donationsValues().appendToTerminalDonationQueue(
TerminalDonationQueue.TerminalDonation(
level = stripe3DSData.inAppPayment.data.level,
@@ -113,7 +114,7 @@ class ExternalLaunchDonationJob private constructor(
checkIntentStatus(stripePaymentIntent.status)
Log.i(TAG, "Creating and inserting donation receipt record.", true)
- val donationReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_DONATION) {
+ val donationReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentType.ONE_TIME_DONATION) {
DonationReceiptRecord.createForBoost(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
} else {
DonationReceiptRecord.createForGift(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
@@ -270,7 +271,7 @@ class ExternalLaunchDonationJob private constructor(
error("Not needed, this job should not be creating intents.")
}
- override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single {
+ override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single {
error("Not needed, this job should not be creating intents.")
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentAuthCheckJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentAuthCheckJob.kt
index 4d2ceddff6..6015896dd6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentAuthCheckJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentAuthCheckJob.kt
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
+import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
@@ -16,6 +17,7 @@ import org.signal.donations.json.StripePaymentIntent
import org.signal.donations.json.StripeSetupIntent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -102,7 +104,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
SignalDatabase.inAppPayments.insert(
type = pending3DSData.inAppPayment.type,
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
- subscriberId = if (pending3DSData.inAppPayment.type == InAppPaymentTable.Type.RECURRING_DONATION) {
+ subscriberId = if (pending3DSData.inAppPayment.type == InAppPaymentType.RECURRING_DONATION) {
InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION).subscriberId
} else {
null
@@ -132,8 +134,8 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
Log.i(TAG, "Creating and inserting receipt.", true)
val receipt = when (inAppPayment.type) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
- InAppPaymentTable.Type.ONE_TIME_GIFT -> DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
+ InAppPaymentType.ONE_TIME_DONATION -> DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
+ InAppPaymentType.ONE_TIME_GIFT -> DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
else -> {
Log.e(TAG, "Unexpected type ${inAppPayment.type}", true)
return CheckResult.Failure()
@@ -184,8 +186,8 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
val subscriber = InAppPaymentsRepository.requireSubscriber(
when (inAppPayment.type) {
- InAppPaymentTable.Type.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
- InAppPaymentTable.Type.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
+ InAppPaymentType.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
+ InAppPaymentType.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
else -> {
Log.e(TAG, "Expected recurring type but found ${inAppPayment.type}", true)
return CheckResult.Failure()
@@ -352,7 +354,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
error("Not needed, this job should not be creating intents.")
}
- override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single {
+ override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single {
error("Not needed, this job should not be creating intents.")
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentGiftSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentGiftSendJob.kt
index b7811a3e62..e6ee76aa5c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentGiftSendJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentGiftSendJob.kt
@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.badges.gifts.Gifts
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.InAppPaymentTable
@@ -50,13 +51,29 @@ class InAppPaymentGiftSendJob private constructor(
override fun onFailure() {
warning("Failed to send gift.")
+
+ val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
+ if (inAppPayment != null && inAppPayment.data.error == null) {
+ warn(TAG, "Marking an unknown error. Check logs for more details.")
+ SignalDatabase.inAppPayments.update(
+ inAppPayment.copy(
+ notified = true,
+ state = InAppPaymentTable.State.END,
+ data = inAppPayment.data.copy(
+ error = InAppPaymentData.Error(
+ type = InAppPaymentData.Error.Type.UNKNOWN
+ )
+ )
+ )
+ )
+ }
}
override fun onRun() {
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
requireNotNull(inAppPayment, "Not found.")
- check(inAppPayment!!.type == InAppPaymentTable.Type.ONE_TIME_GIFT, "Invalid type: ${inAppPayment.type}")
+ check(inAppPayment!!.type == InAppPaymentType.ONE_TIME_GIFT, "Invalid type: ${inAppPayment.type}")
check(inAppPayment.state == InAppPaymentTable.State.PENDING, "Invalid state: ${inAppPayment.state}")
requireNotNull(inAppPayment.data.redemption, "No redemption present on data")
check(inAppPayment.data.redemption!!.stage == InAppPaymentData.RedemptionState.Stage.REDEMPTION_STARTED, "Invalid stage: ${inAppPayment.data.redemption.stage}")
@@ -64,7 +81,21 @@ class InAppPaymentGiftSendJob private constructor(
val recipient = Recipient.resolved(RecipientId.from(requireNotNull(inAppPayment.data.recipientId, "No recipient on data.")))
val token = requireNotNull(inAppPayment.data.redemption.receiptCredentialPresentation, "No presentation present on data.")
- check(!recipient.isIndividual || recipient.registered != RecipientTable.RegisteredState.REGISTERED, "Invalid recipient ${recipient.id} for gift send.")
+ if (!recipient.isIndividual || recipient.registered != RecipientTable.RegisteredState.REGISTERED) {
+ SignalDatabase.inAppPayments.update(
+ inAppPayment.copy(
+ notified = false,
+ state = InAppPaymentTable.State.END,
+ data = inAppPayment.data.copy(
+ error = InAppPaymentData.Error(
+ type = InAppPaymentData.Error.Type.INVALID_GIFT_RECIPIENT
+ )
+ )
+ )
+ )
+
+ throw Exception("Invalid recipient ${recipient.id} for gift send.")
+ }
val thread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val outgoingMessage = Gifts.createOutgoingGiftMessage(
@@ -129,6 +160,7 @@ class InAppPaymentGiftSendJob private constructor(
private fun info(message: String, throwable: Throwable? = null) {
Log.i(TAG, "InAppPayment $inAppPaymentId: $message", throwable, true)
}
+
private fun warning(message: String, throwable: Throwable? = null) {
Log.w(TAG, "InAppPayment $inAppPaymentId: $message", throwable, true)
}
@@ -136,7 +168,7 @@ class InAppPaymentGiftSendJob private constructor(
class Factory : Job.Factory {
override fun create(parameters: Parameters, serializedData: ByteArray?): InAppPaymentGiftSendJob {
return InAppPaymentGiftSendJob(
- inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.toString().toLong()),
+ inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.decodeToString().toLong()),
parameters = parameters
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt
index 8bf4b2ee74..3237f6147f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs
import okio.ByteString.Companion.toByteString
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
+import org.signal.donations.InAppPaymentType
import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.libsignal.zkgroup.receipts.ReceiptCredential
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext
@@ -56,14 +57,14 @@ class InAppPaymentOneTimeContextJob private constructor(
fun createJobChain(inAppPayment: InAppPaymentTable.InAppPayment, makePrimary: Boolean = false): Chain {
return when (inAppPayment.type) {
- InAppPaymentTable.Type.ONE_TIME_DONATION -> {
+ InAppPaymentType.ONE_TIME_DONATION -> {
AppDependencies.jobManager
.startChain(create(inAppPayment))
.then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary))
.then(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
}
- InAppPaymentTable.Type.ONE_TIME_GIFT -> {
+ InAppPaymentType.ONE_TIME_GIFT -> {
AppDependencies.jobManager
.startChain(create(inAppPayment))
.then(InAppPaymentGiftSendJob.create(inAppPayment))
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt
index 52451e5969..d6a59509f1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt
@@ -13,6 +13,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -70,6 +71,7 @@ class InAppPaymentRecurringContextJob private constructor(
override fun onAdded() {
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
+ info("Added context job for payment with state ${inAppPayment?.state}")
if (inAppPayment?.state == InAppPaymentTable.State.CREATED) {
SignalDatabase.inAppPayments.update(
inAppPayment.copy(
@@ -357,10 +359,7 @@ class InAppPaymentRecurringContextJob private constructor(
requestContext: ReceiptCredentialRequestContext
) {
info("Submitting receipt credential request")
- val response: ServiceResponse = when (inAppPayment.type) {
- InAppPaymentTable.Type.RECURRING_DONATION -> AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
- else -> throw Exception("Unsupported type: ${inAppPayment.type}")
- }
+ val response: ServiceResponse = AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
if (response.applicationError.isPresent) {
handleApplicationError(inAppPayment, response)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt
index 689fb1785e..77adb8b09e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt
@@ -6,8 +6,10 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
+import org.signal.donations.InAppPaymentType
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
+import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
@@ -158,7 +160,7 @@ class InAppPaymentRedemptionJob private constructor(
Log.d(TAG, "Attempting to redeem receipt credential presentation...", true)
val serviceResponse = AppDependencies
.donationsService
- .redeemReceipt(
+ .redeemDonationReceipt(
receiptCredentialPresentation,
SignalStore.donationsValues().getDisplayBadgesOnProfile(),
jobData.makePrimary
@@ -206,14 +208,23 @@ class InAppPaymentRedemptionJob private constructor(
val receiptCredentialPresentation = ReceiptCredentialPresentation(credentialBytes.toByteArray())
- Log.d(TAG, "Attempting to redeem receipt credential presentation...", true)
- val serviceResponse = AppDependencies
- .donationsService
- .redeemReceipt(
- receiptCredentialPresentation,
- SignalStore.donationsValues().getDisplayBadgesOnProfile(),
- jobData.makePrimary
- )
+ val serviceResponse = if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
+ Log.d(TAG, "Attempting to redeem archive receipt credential presentation...", true)
+ AppDependencies
+ .donationsService
+ .redeemArchivesReceipt(
+ receiptCredentialPresentation
+ )
+ } else {
+ Log.d(TAG, "Attempting to redeem donation receipt credential presentation...", true)
+ AppDependencies
+ .donationsService
+ .redeemDonationReceipt(
+ receiptCredentialPresentation,
+ SignalStore.donationsValues().getDisplayBadgesOnProfile(),
+ jobData.makePrimary
+ )
+ }
verifyServiceResponse(serviceResponse) {
val protoError = InAppPaymentData.Error(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java
index 6a368de1b0..67020884a0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionBadges.java
@@ -4,6 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
+import org.signal.donations.InAppPaymentType;
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository;
import org.thoughtcrime.securesms.database.InAppPaymentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
@@ -29,7 +30,7 @@ final class LogSectionBadges implements LogSection {
return "Self not yet available!";
}
- InAppPaymentTable.InAppPayment latestRecurringDonation = SignalDatabase.inAppPayments().getLatestInAppPaymentByType(InAppPaymentTable.Type.RECURRING_DONATION);
+ InAppPaymentTable.InAppPayment latestRecurringDonation = SignalDatabase.inAppPayments().getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION);
if (latestRecurringDonation != null) {
return new StringBuilder().append("Badge Count : ").append(Recipient.self().getBadges().size()).append("\n")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt
index ad93c30012..a7fd24e1c9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt
@@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.megaphone
import android.app.Application
import android.content.Context
-import android.content.Intent
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import org.json.JSONArray
import org.json.JSONException
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
-import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
+import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
-import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalActivity
+import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.database.RemoteMegaphoneTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
@@ -53,12 +52,12 @@ object RemoteMegaphoneRepository {
}
private val donate: Action = Action { context, controller, remote ->
- controller.onMegaphoneNavigationRequested(Intent(context, DonateToSignalActivity::class.java))
+ controller.onMegaphoneNavigationRequested(CheckoutFlowActivity.createIntent(context, InAppPaymentType.ONE_TIME_DONATION))
snooze.run(context, controller, remote)
}
private val donateForFriend: Action = Action { context, controller, remote ->
- controller.onMegaphoneNavigationRequested(Intent(context, GiftFlowActivity::class.java))
+ controller.onMegaphoneNavigationRequested(CheckoutFlowActivity.createIntent(context, InAppPaymentType.ONE_TIME_GIFT))
snooze.run(context, controller, remote)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
index f85d2c02e5..fc11abdc9f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.lock.v2.SvrConstants;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
+import org.thoughtcrime.securesms.lock.v2.SvrConstants;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
diff --git a/app/src/main/protowire/Database.proto b/app/src/main/protowire/Database.proto
index 0be6504e07..2a9aa3d073 100644
--- a/app/src/main/protowire/Database.proto
+++ b/app/src/main/protowire/Database.proto
@@ -473,20 +473,21 @@ message ExternalLaunchTransactionState {
}
message GatewayRequest {
- enum DonateToSignalType {
- MONTHLY = 0;
- ONE_TIME = 1;
- GIFT = 2;
+ enum InAppPaymentType {
+ RECURRING_DONATION = 0;
+ ONE_TIME_DONATION = 1;
+ ONE_TIME_GIFT = 2;
+ RECURRING_BACKUPS = 3;
}
- DonateToSignalType donateToSignalType = 1;
- BadgeList.Badge badge = 2;
- string label = 3;
- DecimalValue price = 4;
- string currencyCode = 5;
- int64 level = 6;
- int64 recipient_id = 7;
- string additionalMessage = 8;
+ InAppPaymentType inAppPaymentType = 1;
+ BadgeList.Badge badge = 2;
+ string label = 3;
+ DecimalValue price = 4;
+ string currencyCode = 5;
+ int64 level = 6;
+ int64 recipient_id = 7;
+ string additionalMessage = 8;
}
StripeIntentAccessor stripeIntentAccessor = 1;
diff --git a/app/src/main/res/navigation/app_settings.xml b/app/src/main/res/navigation/app_settings.xml
index 953c96fafe..5c9d3c2bcc 100644
--- a/app/src/main/res/navigation/app_settings.xml
+++ b/app/src/main/res/navigation/app_settings.xml
@@ -535,21 +535,14 @@
app:popUpToInclusive="true" />
-
-
-
-
+ app:popUpToInclusive="true" />
-
-
-
+ app:popExitAnim="@anim/fragment_close_exit" />
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/app_settings_with_change_number_v2.xml b/app/src/main/res/navigation/app_settings_with_change_number_v2.xml
index 98354df387..2d19f576c3 100644
--- a/app/src/main/res/navigation/app_settings_with_change_number_v2.xml
+++ b/app/src/main/res/navigation/app_settings_with_change_number_v2.xml
@@ -535,21 +535,14 @@
app:popUpToInclusive="true" />
-
-
-
-
+ app:popUpToInclusive="true" />
-
-
-
+ app:popExitAnim="@anim/fragment_close_exit" />
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/donate_to_signal.xml b/app/src/main/res/navigation/checkout.xml
similarity index 70%
rename from app/src/main/res/navigation/donate_to_signal.xml
rename to app/src/main/res/navigation/checkout.xml
index dba68d5d37..9223d3103c 100644
--- a/app/src/main/res/navigation/donate_to_signal.xml
+++ b/app/src/main/res/navigation/checkout.xml
@@ -1,86 +1,15 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ app:argType="org.signal.donations.InAppPaymentType" />
-
+ android:defaultValue="false"
+ app:argType="boolean" />
-
-
-
-
+ app:argType="org.signal.donations.InAppPaymentType" />
@@ -300,8 +209,204 @@
\ No newline at end of file
diff --git a/app/src/main/res/navigation/gift_flow.xml b/app/src/main/res/navigation/gift_flow.xml
deleted file mode 100644
index 51b2a664f3..0000000000
--- a/app/src/main/res/navigation/gift_flow.xml
+++ /dev/null
@@ -1,205 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/core-util/src/main/java/org/signal/core/util/BundleExtensions.kt b/core-util/src/main/java/org/signal/core/util/BundleExtensions.kt
index 656ebc391a..484b2f1e14 100644
--- a/core-util/src/main/java/org/signal/core/util/BundleExtensions.kt
+++ b/core-util/src/main/java/org/signal/core/util/BundleExtensions.kt
@@ -3,6 +3,16 @@ package org.signal.core.util
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
+import java.io.Serializable
+
+fun Bundle.getSerializableCompat(key: String, clazz: Class): T? {
+ return if (Build.VERSION.SDK_INT >= 33) {
+ this.getSerializable(key, clazz)
+ } else {
+ @Suppress("DEPRECATION", "UNCHECKED_CAST")
+ this.getSerializable(key) as T?
+ }
+}
fun Bundle.getParcelableCompat(key: String, clazz: Class): T? {
return if (Build.VERSION.SDK_INT >= 33) {
diff --git a/donations/lib/src/main/java/org/signal/donations/InAppPaymentType.kt b/donations/lib/src/main/java/org/signal/donations/InAppPaymentType.kt
new file mode 100644
index 0000000000..1e31c436e7
--- /dev/null
+++ b/donations/lib/src/main/java/org/signal/donations/InAppPaymentType.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.signal.donations
+
+import org.signal.core.util.Serializer
+
+enum class InAppPaymentType(val code: Int, val recurring: Boolean) {
+ /**
+ * Used explicitly for mapping DonationErrorSource. Writing this value
+ * into an InAppPayment is an error.
+ */
+ UNKNOWN(-1, false),
+
+ /**
+ * This payment is for a gift badge
+ */
+ ONE_TIME_GIFT(0, false),
+
+ /**
+ * This payment is for a one-time donation
+ */
+ ONE_TIME_DONATION(1, false),
+
+ /**
+ * This payment is for a recurring donation
+ */
+ RECURRING_DONATION(2, true),
+
+ /**
+ * This payment is for a recurring backup payment
+ */
+ RECURRING_BACKUP(3, true);
+
+ companion object : Serializer {
+ override fun serialize(data: InAppPaymentType): Int = data.code
+ override fun deserialize(input: Int): InAppPaymentType = entries.first { it.code == input }
+ }
+}
diff --git a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt
index 0ec6a7d3c2..6006e461a8 100644
--- a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt
+++ b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt
@@ -62,9 +62,9 @@ class StripeApi(
data class Failure(val reason: Throwable) : CreatePaymentSourceFromCardDataResult()
}
- fun createSetupIntent(sourceType: PaymentSourceType.Stripe): Single {
+ fun createSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single {
return setupIntentHelper
- .fetchSetupIntent(sourceType)
+ .fetchSetupIntent(inAppPaymentType, sourceType)
.map { CreateSetupIntentResult(it) }
.subscribeOn(Schedulers.io())
}
@@ -588,6 +588,7 @@ class StripeApi(
interface SetupIntentHelper {
fun fetchSetupIntent(
+ inAppPaymentType: InAppPaymentType,
sourceType: PaymentSourceType.Stripe
): Single
}
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java
index 09721453d4..88f6e11801 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java
@@ -80,7 +80,7 @@ public class DonationsService {
* @param visible Whether the badge will be visible on the user's profile immediately after redemption
* @param primary Whether the badge will be made primary immediately after redemption
*/
- public ServiceResponse redeemReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) {
+ public ServiceResponse redeemDonationReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) {
try {
pushServiceSocket.redeemDonationReceipt(receiptCredentialPresentation, visible, primary);
return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null);
@@ -89,6 +89,20 @@ public class DonationsService {
}
}
+ /**
+ * Allows a user to redeem a given receipt they were given after submitting a donation successfully.
+ *
+ * @param receiptCredentialPresentation Receipt
+ */
+ public ServiceResponse redeemArchivesReceipt(ReceiptCredentialPresentation receiptCredentialPresentation) {
+ try {
+ pushServiceSocket.redeemArchivesReceipt(receiptCredentialPresentation);
+ return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null);
+ } catch (Exception e) {
+ return ServiceResponse.forUnknownError(e);
+ }
+ }
+
/**
* Submits price information to the server to generate a payment intent via the payment gateway.
*
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index ee6998bad8..ded25a4ddb 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -288,6 +288,7 @@ public class PushServiceSocket {
private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push";
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
+ private static final String ARCHIVES_REDEEM_RECEIPT = "/v1/archives/redeem-receipt";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
@@ -1312,10 +1313,15 @@ public class PushServiceSocket {
}
public void redeemDonationReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) throws IOException {
- String payload = JsonUtil.toJson(new RedeemReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize()), visible, primary));
+ String payload = JsonUtil.toJson(new RedeemDonationReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize()), visible, primary));
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
}
+ public void redeemArchivesReceipt(ReceiptCredentialPresentation receiptCredentialPresentation) throws IOException {
+ String payload = JsonUtil.toJson(new RedeemArchivesReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize())));
+ makeServiceRequest(ARCHIVES_REDEEM_RECEIPT, "POST", payload);
+ }
+
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, String paymentMethod, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level, paymentMethod));
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt
new file mode 100644
index 0000000000..21ed0312d3
--- /dev/null
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt
@@ -0,0 +1,13 @@
+package org.whispersystems.signalservice.internal.push
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+
+/**
+ * POST /v1/archives/redeem-receipt
+ *
+ * Request object for redeeming a receipt from a donation transaction.
+ *
+ * @param receiptCredentialPresentation base64-encoded no-newlines standard-character-set with-padding of the bytes of a [ReceiptCredentialPresentation] object
+ */
+internal class RedeemArchivesReceiptRequest @JsonCreator constructor(@param:JsonProperty("receiptCredentialPresentation") val receiptCredentialPresentation: String)
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java
similarity index 94%
rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java
rename to libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java
index fc38966fbf..ffa5d761fc 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java
@@ -10,7 +10,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
*
* Request object for redeeming a receipt from a donation transaction.
*/
-class RedeemReceiptRequest {
+class RedeemDonationReceiptRequest {
private final String receiptCredentialPresentation;
private final boolean visible;
@@ -21,8 +21,7 @@ class RedeemReceiptRequest {
* @param visible boolean indicating if the new badge should be visible or not on the profile
* @param primary boolean indicating if the new badge should be primary or not on the profile; is always treated as false if `visible` is false
*/
- @JsonCreator
- RedeemReceiptRequest(
+ @JsonCreator RedeemDonationReceiptRequest(
@JsonProperty("receiptCredentialPresentation") String receiptCredentialPresentation,
@JsonProperty("visible") boolean visible,
@JsonProperty("primary") boolean primary) {
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java
index 8cc0d6792b..27de4d719a 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java
@@ -23,6 +23,7 @@ public class SubscriptionsConfiguration {
public static final int BOOST_LEVEL = 1;
public static final int GIFT_LEVEL = 100;
+ public static final int BACKUPS_LEVEL = 201;
public static final HashSet SUBSCRIPTION_LEVELS = new HashSet<>(Arrays.asList(500, 1000, 2000));
@JsonProperty("currencies")