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