Implement start of backups payment integration work.

This commit is contained in:
Alex Hart
2024-05-29 16:48:33 -03:00
committed by Greyson Parrelli
parent 680223c4b6
commit 6b50be78c0
81 changed files with 1492 additions and 1141 deletions

View File

@@ -5,11 +5,14 @@
package org.thoughtcrime.securesms.backup.v2
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.LongSerializer
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
@@ -17,6 +20,7 @@ import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
@@ -35,8 +39,11 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
@@ -58,13 +65,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.Exception
import java.math.BigDecimal
import java.time.ZonedDateTime
import java.util.Currency
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
object BackupRepository {
@@ -673,6 +683,82 @@ object BackupRepository {
}
}
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
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<MessageBackupTier?> {
override fun serialize(data: MessageBackupTier?): Long {
return data?.value?.toLong() ?: -1
}
override fun deserialize(data: Long): MessageBackupTier? {
return values().firstOrNull { it.value == data.toInt() }
}
}
}

View File

@@ -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<MessageBackupTier?> {
override fun serialize(data: MessageBackupTier?): Long {
return data?.value?.toLong() ?: -1
}
override fun deserialize(data: Long): MessageBackupTier? {
return entries.firstOrNull { it.value == data.toInt() }
}
}
}

View File

@@ -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<InAppPaymentData.PaymentMethodType>,
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<InAppPaymentData.PaymentMethodType>,
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 = {}
)

View File

@@ -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()
}
)
}
}
}
}
}
}

View File

@@ -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<InAppPaymentTable.InAppPaymentId>()
@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?
}
}

View File

@@ -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

View File

@@ -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<MessageBackupTier> = emptyList(),
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
val selectedPaymentMethod: InAppPaymentData.PaymentMethodType? = null,
val availablePaymentMethods: List<InAppPaymentData.PaymentMethodType> = 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
)

View File

@@ -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<MessageBackupsFlowState> = 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
}
}

View File

@@ -12,5 +12,7 @@ enum class MessageBackupsScreen {
TYPE_SELECTION,
CHECKOUT_SHEET,
PROCESS_PAYMENT,
COMPLETED
COMPLETED;
fun isAfter(other: MessageBackupsScreen): Boolean = ordinal > other.ordinal
}

View File

@@ -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<MessageBackupsTypeFeature>
)

View File

@@ -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<MessageBackupTier>,
availableBackupTypes: List<MessageBackupsType>,
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<MessageBackupsTypeFeature>
)
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!"
)
)
)
}
}

View File

@@ -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<DonationPaymentComponent.GooglePayResult> = 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()
}
}
}
}

View File

@@ -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<MaterialButton>(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")
}

View File

@@ -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<InAppPaymentTable.InAppPayment> {
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,

View File

@@ -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)
}
)

View File

@@ -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<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
override val googlePayResultPublisher: Subject<InAppPaymentComponent.GooglePayResult> = 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 {

View File

@@ -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))
}
}
)

View File

@@ -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 = {}
)

View File

@@ -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,

View File

@@ -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<RemoteBackupsSettingsState> = 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() {

View File

@@ -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 {}
)

View File

@@ -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
)

View File

@@ -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<BackupsTypeSettingsState> = 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
)
}
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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<GooglePayResult>

View File

@@ -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<DonationRedemptionJobStatus> {
fun observeInAppPaymentRedemption(type: InAppPaymentType): Observable<DonationRedemptionJobStatus> {
val jobStatusObservable: Observable<DonationRedemptionJobStatus> = 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<DonationRedemptionJobStatus> = 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

View File

@@ -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<EmptyResponse>::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 {

View File

@@ -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<StripeApi.Secure3DSAction> {
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<StripeClientSecret> {
private fun createPaymentMethod(subscriberType: InAppPaymentSubscriberRecord.Type, paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single<StripeClientSecret> {
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<StripeIntentAccessor> {
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
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 {

View File

@@ -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()
}
)

View File

@@ -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<String>
) : ViewModel() {
@@ -89,7 +90,7 @@ class SetCurrencyViewModel(
}
}
class Factory(private val inAppPaymentType: InAppPaymentTable.Type, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
class Factory(private val inAppPaymentType: InAppPaymentType, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(SetCurrencyViewModel(inAppPaymentType, supportedCurrencyCodes))!!
}

View File

@@ -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<InAppPaymentComponent.GooglePayResult> = PublishSubject.create()
private val args by navArgs<CheckoutFlowActivityArgs>()
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))
}
}

View File

@@ -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)
}
}

View File

@@ -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<String>) : DonateToSignalAction()
data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentType, val supportedCurrencies: List<String>) : DonateToSignalAction()
data class DisplayGatewaySelectorDialog(val inAppPayment: InAppPaymentTable.InAppPayment) : DonateToSignalAction()
object CancelSubscription : DonateToSignalAction()
data class UpdateSubscription(val inAppPayment: InAppPaymentTable.InAppPayment, val isLongRunning: Boolean) : DonateToSignalAction()

View File

@@ -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<DonationPaymentComponent.GooglePayResult> = 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))
}
}

View File

@@ -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()
}
}

View File

@@ -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<String>
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")
}

View File

@@ -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<Optional<PendingOneTimeDonation>> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION).map {
val oneTimeDonationFromJob: Observable<Optional<PendingOneTimeDonation>> = 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<DonationRedemptionJobStatus> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION)
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = 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 {

View File

@@ -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)
}
}

View File

@@ -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<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = true
@@ -29,10 +29,10 @@ object DonationPillToggle {
private class ViewHolder(binding: DonationPillToggleBinding) : BindingViewHolder<Model, DonationPillToggleBinding>(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 -> {

View File

@@ -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 {

View File

@@ -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<DonationPaymentComponent>().stripeRepository)
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().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())

View File

@@ -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<DonationPaymentComponent>().stripeRepository)
GatewaySelectorViewModel.Factory(args, requireListener<InAppPaymentComponent>().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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<PayPalPaymentMethodId>) {
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()))

View File

@@ -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,

View File

@@ -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<DonationPaymentComponent>().stripeRepository)
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().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
)
)

View File

@@ -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<StripeApi.Secure3DSAction> = 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 ->

View File

@@ -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<DonationPaymentComponent>().stripeRepository)
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
}
)

View File

@@ -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<DonationPaymentComponent>().stripeRepository)
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
}
)

View File

@@ -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<V> 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<V> private constructor(
}
}
private fun <V> getGenericRedemptionError(context: Context, type: InAppPaymentTable.Type, callback: Callback<V>): DonationErrorParams<V> {
private fun <V> getGenericRedemptionError(context: Context, type: InAppPaymentType, callback: Callback<V>): DonationErrorParams<V> {
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),

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
override val googlePayResultPublisher: Subject<InAppPaymentComponent.GooglePayResult> = 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() {

View File

@@ -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()
}

View File

@@ -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<SubscriberId?, NullableSubscriberIdParceler>
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<Type, Int> {
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
*

View File

@@ -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)
}
}

View File

@@ -193,7 +193,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
Log.d(TAG, "Attempting to redeem token... isForSubscription: " + isForSubscription(), true);
ServiceResponse<EmptyResponse> response = AppDependencies.getDonationsService()
.redeemReceipt(presentation,
.redeemDonationReceipt(presentation,
SignalStore.donationsValues().getDisplayBadgesOnProfile(),
makePrimary);

View File

@@ -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<StripeIntentAccessor> {
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
error("Not needed, this job should not be creating intents.")
}
}

View File

@@ -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<StripeIntentAccessor> {
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
error("Not needed, this job should not be creating intents.")
}

View File

@@ -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<InAppPaymentGiftSendJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): InAppPaymentGiftSendJob {
return InAppPaymentGiftSendJob(
inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.toString().toLong()),
inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.decodeToString().toLong()),
parameters = parameters
)
}

View File

@@ -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))

View File

@@ -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<ReceiptCredentialResponse> = when (inAppPayment.type) {
InAppPaymentTable.Type.RECURRING_DONATION -> AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
else -> throw Exception("Unsupported type: ${inAppPayment.type}")
}
val response: ServiceResponse<ReceiptCredentialResponse> = AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
if (response.applicationError.isPresent) {
handleApplicationError(inAppPayment, response)

View File

@@ -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(

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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;