mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Implement start of backups payment integration work.
This commit is contained in:
committed by
Greyson Parrelli
parent
680223c4b6
commit
6b50be78c0
@@ -5,11 +5,14 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.LongSerializer
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
||||
@@ -17,6 +20,7 @@ import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
@@ -35,8 +39,11 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
@@ -58,13 +65,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.Exception
|
||||
import java.math.BigDecimal
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object BackupRepository {
|
||||
@@ -673,6 +683,82 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
|
||||
return availableBackupTiers.map { getBackupsType(it) }
|
||||
}
|
||||
|
||||
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
|
||||
val backupCurrency = SignalStore.donationsValues().getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
return when (tier) {
|
||||
MessageBackupTier.FREE -> getFreeType(backupCurrency)
|
||||
MessageBackupTier.PAID -> getPaidType(backupCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFreeType(currency: Currency): MessageBackupsType {
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
|
||||
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
|
||||
val serviceResponse = withContext(Dispatchers.IO) {
|
||||
AppDependencies
|
||||
.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
|
||||
if (serviceResponse.result.isEmpty) {
|
||||
if (serviceResponse.applicationError.isPresent) {
|
||||
throw serviceResponse.applicationError.get()
|
||||
}
|
||||
|
||||
if (serviceResponse.executionError.isPresent) {
|
||||
throw serviceResponse.executionError.get()
|
||||
}
|
||||
|
||||
error("Unhandled error occurred while downloading configuration.")
|
||||
}
|
||||
|
||||
val config = serviceResponse.result.get()
|
||||
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
|
||||
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
|
||||
* Should be the basis of all backup operations.
|
||||
@@ -765,18 +851,3 @@ class BackupMetadata(
|
||||
val usedSpace: Long,
|
||||
val mediaCount: Long
|
||||
)
|
||||
|
||||
enum class MessageBackupTier(val value: Int) {
|
||||
FREE(0),
|
||||
PAID(1);
|
||||
|
||||
companion object Serializer : LongSerializer<MessageBackupTier?> {
|
||||
override fun serialize(data: MessageBackupTier?): Long {
|
||||
return data?.value?.toLong() ?: -1
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): MessageBackupTier? {
|
||||
return values().firstOrNull { it.value == data.toInt() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* Serializable enum value for what we think a user's current backup tier is.
|
||||
*
|
||||
* We should not trust the stored value on its own, we should also verify it
|
||||
* against what the server knows, but it is a useful flag that helps avoid a
|
||||
* network call in some cases.
|
||||
*/
|
||||
enum class MessageBackupTier(val value: Int) {
|
||||
FREE(0),
|
||||
PAID(1);
|
||||
|
||||
companion object Serializer : LongSerializer<MessageBackupTier?> {
|
||||
override fun serialize(data: MessageBackupTier?): Long {
|
||||
return data?.value?.toLong() ?: -1
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): MessageBackupTier? {
|
||||
return entries.firstOrNull { it.value == data.toInt() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -30,49 +32,59 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageBackupsCheckoutSheet(
|
||||
messageBackupTier: MessageBackupTier,
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentMethods: List<InAppPaymentData.PaymentMethodType>,
|
||||
sheetState: SheetState,
|
||||
onDismissRequest: () -> Unit,
|
||||
onPaymentMethodSelected: (InAppPaymentData.PaymentMethodType) -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
dragHandle = { BottomSheets.Handle() },
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
modifier = Modifier.padding()
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupTier = messageBackupTier,
|
||||
availablePaymentGateways = availablePaymentMethods,
|
||||
onPaymentGatewaySelected = onPaymentMethodSelected
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = messageBackupsType,
|
||||
availablePaymentGateways = availablePaymentMethods,
|
||||
onPaymentGatewaySelected = onPaymentMethodSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
messageBackupTier: MessageBackupTier,
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentGateways: List<InAppPaymentData.PaymentMethodType>,
|
||||
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val backupTypeDetails = remember(messageBackupTier) {
|
||||
getTierDetails(messageBackupTier)
|
||||
}
|
||||
val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
|
||||
FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(
|
||||
@@ -88,7 +100,7 @@ private fun SheetContent(
|
||||
)
|
||||
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = backupTypeDetails,
|
||||
messageBackupsType = messageBackupsType,
|
||||
isSelected = false,
|
||||
onSelected = {},
|
||||
enabled = false,
|
||||
@@ -231,7 +243,12 @@ private fun MessageBackupsCheckoutSheetPreview() {
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupTier = MessageBackupTier.PAID,
|
||||
messageBackupsType = MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
title = "Free",
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
features = persistentListOf()
|
||||
),
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = {}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Handles the selection, payment, and changing of a user's backup tier.
|
||||
*/
|
||||
class MessageBackupsFlowFragment : ComposeFragment(), DonationCheckoutDelegate.Callback {
|
||||
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
|
||||
|
||||
private val inAppPaymentIdProcessor = PublishProcessor.create<InAppPaymentTable.InAppPaymentId>()
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
val navController = rememberNavController()
|
||||
|
||||
val checkoutDelegate = remember {
|
||||
DonationCheckoutDelegate(this, this, inAppPaymentIdProcessor)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.inAppPayment?.id) {
|
||||
val inAppPaymentId = state.inAppPayment?.id
|
||||
if (inAppPaymentId != null) {
|
||||
inAppPaymentIdProcessor.onNext(inAppPaymentId)
|
||||
}
|
||||
}
|
||||
|
||||
val checkoutSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navController.setLifecycleOwner(this@MessageBackupsFlowFragment)
|
||||
navController.setOnBackPressedDispatcher(requireActivity().onBackPressedDispatcher)
|
||||
navController.enableOnBackPressed(true)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = state.startScreen.name,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
|
||||
) {
|
||||
composable(route = MessageBackupsScreen.EDUCATION.name) {
|
||||
MessageBackupsEducationScreen(
|
||||
onNavigationClick = viewModel::goToPreviousScreen,
|
||||
onEnableBackups = viewModel::goToNextScreen,
|
||||
onLearnMore = {}
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
|
||||
MessageBackupsPinEducationScreen(
|
||||
onNavigationClick = viewModel::goToPreviousScreen,
|
||||
onGeneratePinClick = {},
|
||||
onUseCurrentPinClick = viewModel::goToNextScreen,
|
||||
recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
|
||||
MessageBackupsPinConfirmationScreen(
|
||||
pin = state.pin,
|
||||
onPinChanged = viewModel::onPinEntryUpdated,
|
||||
pinKeyboardType = state.pinKeyboardType,
|
||||
onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
|
||||
onNextClick = viewModel::goToNextScreen
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupTier = state.selectedMessageBackupTier,
|
||||
availableBackupTypes = state.availableBackupTypes,
|
||||
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||
onNavigationClick = viewModel::goToPreviousScreen,
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = viewModel::goToNextScreen
|
||||
)
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
|
||||
MessageBackupsCheckoutSheet(
|
||||
messageBackupsType = state.availableBackupTypes.first { it.tier == state.selectedMessageBackupTier!! },
|
||||
availablePaymentMethods = state.availablePaymentMethods,
|
||||
sheetState = checkoutSheetState,
|
||||
onDismissRequest = {
|
||||
viewModel.goToPreviousScreen()
|
||||
},
|
||||
onPaymentMethodSelected = {
|
||||
viewModel.onPaymentMethodUpdated(it)
|
||||
viewModel.goToNextScreen()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.screen) {
|
||||
val route = navController.currentDestination?.route ?: return@LaunchedEffect
|
||||
if (route == state.screen.name) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.COMPLETED) {
|
||||
if (!findNavController().popBackStack()) {
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.PROCESS_PAYMENT) {
|
||||
checkoutDelegate.handleGatewaySelectionResponse(state.inAppPayment!!)
|
||||
viewModel.goToPreviousScreen()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val routeScreen = MessageBackupsScreen.valueOf(route)
|
||||
if (routeScreen.isAfter(state.screen)) {
|
||||
navController.popBackStack()
|
||||
} else {
|
||||
navController.navigate(state.screen.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
MessageBackupsFlowFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
// TODO [message-backups] What do? probably some kind of success thing?
|
||||
if (!findNavController().popBackStack()) {
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) = error("This view doesn't support cancellation, that is done elsewhere.")
|
||||
|
||||
override fun onProcessorActionProcessed() = Unit
|
||||
|
||||
override fun onUserLaunchedAnExternalApplication() {
|
||||
// TODO [message-backups] What do? Are we even supporting bank transfers?
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
// TODO [message-backups] What do? Are we even supporting bank transfers?
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
@@ -13,9 +14,12 @@ import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup().backupTier,
|
||||
val currentMessageBackupTier: MessageBackupTier? = SignalStore.backup().backupTier,
|
||||
val availableBackupTiers: List<MessageBackupTier> = emptyList(),
|
||||
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
val selectedPaymentMethod: InAppPaymentData.PaymentMethodType? = null,
|
||||
val availablePaymentMethods: List<InAppPaymentData.PaymentMethodType> = emptyList(),
|
||||
val pin: String = "",
|
||||
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
|
||||
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||
val startScreen: MessageBackupsScreen,
|
||||
val screen: MessageBackupsScreen = startScreen
|
||||
)
|
||||
|
||||
@@ -9,30 +9,52 @@ import android.text.TextUtils
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayOrderStrategy
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(
|
||||
MessageBackupsFlowState(
|
||||
availableBackupTiers = if (!RemoteConfig.messageBackups) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
},
|
||||
selectedMessageBackupTier = SignalStore.backup().backupTier
|
||||
availableBackupTypes = emptyList(),
|
||||
selectedMessageBackupTier = SignalStore.backup().backupTier,
|
||||
availablePaymentMethods = GatewayOrderStrategy.getStrategy().orderedGateways.filter { InAppDonations.isPaymentSourceAvailable(it.toPaymentSourceType(), InAppPaymentType.RECURRING_BACKUP) },
|
||||
startScreen = if (SignalStore.backup().backupTier == null) MessageBackupsScreen.EDUCATION else MessageBackupsScreen.TYPE_SELECTION
|
||||
)
|
||||
)
|
||||
|
||||
val state: State<MessageBackupsFlowState> = internalState
|
||||
|
||||
fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
|
||||
return when (currentScreen) {
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
internalState.value = internalState.value.copy(
|
||||
availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun goToNextScreen() {
|
||||
val nextScreen = when (internalState.value.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
|
||||
@@ -41,6 +63,27 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(screen = nextScreen)
|
||||
}
|
||||
|
||||
fun goToPreviousScreen() {
|
||||
if (internalState.value.screen == internalState.value.startScreen) {
|
||||
internalState.value = state.value.copy(screen = MessageBackupsScreen.COMPLETED)
|
||||
return
|
||||
}
|
||||
|
||||
val previousScreen = when (internalState.value.screen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.EDUCATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.TYPE_SELECTION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.TYPE_SELECTION
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(screen = previousScreen)
|
||||
}
|
||||
|
||||
fun onPinEntryUpdated(pin: String) {
|
||||
@@ -74,11 +117,48 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
|
||||
SignalStore.backup().areBackupsEnabled = true
|
||||
SignalStore.backup().backupTier = state.value.selectedMessageBackupTier!!
|
||||
return MessageBackupsScreen.COMPLETED
|
||||
// return MessageBackupsScreen.CHECKOUT_SHEET TODO [message-backups] Switch back to payment flow
|
||||
|
||||
// TODO [message-backups] - Does anything need to be kicked off?
|
||||
|
||||
return when (state.value.selectedMessageBackupTier!!) {
|
||||
MessageBackupTier.FREE -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupTier.PAID -> MessageBackupsScreen.CHECKOUT_SHEET
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
|
||||
val stateSnapshot = state.value
|
||||
val backupsType = stateSnapshot.availableBackupTypes.first { it.tier == stateSnapshot.selectedMessageBackupTier }
|
||||
|
||||
internalState.value = state.value.copy(inAppPayment = null)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.clearCreated()
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_BACKUP,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
badge = null,
|
||||
label = backupsType.title,
|
||||
amount = backupsType.pricePerMonth.toFiatValue(),
|
||||
level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
|
||||
recipientId = Recipient.self().id.serialize(),
|
||||
paymentMethodType = stateSnapshot.selectedPaymentMethod!!,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
internalState.value = state.value.copy(inAppPayment = inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
return MessageBackupsScreen.PROCESS_PAYMENT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ enum class MessageBackupsScreen {
|
||||
TYPE_SELECTION,
|
||||
CHECKOUT_SHEET,
|
||||
PROCESS_PAYMENT,
|
||||
COMPLETED
|
||||
COMPLETED;
|
||||
|
||||
fun isAfter(other: MessageBackupsScreen): Boolean = ordinal > other.ordinal
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
|
||||
/**
|
||||
* Represents a type of backup a user can select.
|
||||
*/
|
||||
@Stable
|
||||
data class MessageBackupsType(
|
||||
val tier: MessageBackupTier,
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -40,8 +39,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withAnnotation
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
@@ -52,7 +49,6 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Screen which allows the user to select their preferred backup type.
|
||||
@@ -61,7 +57,7 @@ import java.util.Currency
|
||||
@Composable
|
||||
fun MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupTier: MessageBackupTier?,
|
||||
availableBackupTiers: List<MessageBackupTier>,
|
||||
availableBackupTypes: List<MessageBackupsType>,
|
||||
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
@@ -129,16 +125,13 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
availableBackupTiers,
|
||||
{ _, item -> item }
|
||||
availableBackupTypes,
|
||||
{ _, item -> item.tier }
|
||||
) { index, item ->
|
||||
val type = remember(item) {
|
||||
getTierDetails(item)
|
||||
}
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = type,
|
||||
isSelected = item == selectedBackupTier,
|
||||
onSelected = { onMessageBackupsTierSelected(item) },
|
||||
messageBackupsType = item,
|
||||
isSelected = item.tier == selectedBackupTier,
|
||||
onSelected = { onMessageBackupsTierSelected(item.tier) },
|
||||
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
||||
)
|
||||
}
|
||||
@@ -167,7 +160,7 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupTier = MessageBackupTier.FREE,
|
||||
availableBackupTiers = listOf(MessageBackupTier.FREE, MessageBackupTier.PAID),
|
||||
availableBackupTypes = emptyList(),
|
||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
@@ -236,54 +229,3 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class MessageBackupsType(
|
||||
val tier: MessageBackupTier,
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
|
||||
fun getTierDetails(tier: MessageBackupTier): MessageBackupsType {
|
||||
return when (tier) {
|
||||
MessageBackupTier.FREE -> MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
title = "Text + 30 days of media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media"
|
||||
)
|
||||
)
|
||||
)
|
||||
MessageBackupTier.PAID -> MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
|
||||
title = "Text + All your media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
|
||||
/**
|
||||
* Activity which houses the gift flow.
|
||||
*/
|
||||
class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent {
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
onBackPressedDispatcher.addCallback(this, OnBackPressed())
|
||||
}
|
||||
|
||||
override fun getFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.gift_flow)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!findNavController(R.id.fragment_container).popBackStack()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.button.MaterialButton
|
||||
@@ -13,8 +14,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
@@ -25,6 +28,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.settings.models.TextInput
|
||||
@@ -35,10 +39,11 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.util.Optional
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
|
||||
@@ -69,7 +74,6 @@ class GiftFlowConfirmationFragment :
|
||||
private lateinit var emojiKeyboard: MediaKeyboard
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
|
||||
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
||||
@@ -81,13 +85,9 @@ class GiftFlowConfirmationFragment :
|
||||
RecipientPreference.register(adapter)
|
||||
GiftRowItem.register(adapter)
|
||||
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||
val checkoutDelegate = DonationCheckoutDelegate(this, this, viewModel.state.filter { it.inAppPaymentId != null }.map { it.inAppPaymentId!! })
|
||||
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(
|
||||
this,
|
||||
this,
|
||||
viewModel.state.mapOptional { Optional.ofNullable(it.inAppPaymentId) }.distinctUntilChanged()
|
||||
)
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
@@ -104,6 +104,15 @@ class GiftFlowConfirmationFragment :
|
||||
|
||||
emojiKeyboard.setFragmentManager(childFragmentManager)
|
||||
|
||||
setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
|
||||
if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
|
||||
showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
|
||||
} else {
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, InAppPaymentTable.InAppPayment::class.java)!!
|
||||
checkoutDelegate.handleGatewaySelectionResponse(inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
val continueButton = requireView().findViewById<MaterialButton>(R.id.continue_button)
|
||||
continueButton.setOnClickListener {
|
||||
lifecycleDisposable += viewModel.insertInAppPayment(requireContext()).subscribe { inAppPayment ->
|
||||
@@ -191,7 +200,6 @@ class GiftFlowConfirmationFragment :
|
||||
processingDonationPaymentDialog.dismiss()
|
||||
debouncer.clear()
|
||||
verifyingRecipientDonationPaymentDialog.dismiss()
|
||||
donationCheckoutDelegate = null
|
||||
}
|
||||
|
||||
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
|
||||
@@ -245,25 +253,44 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) {
|
||||
val max = FiatMoneyUtil.format(resources, sepaEuroMaximum, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonateToSignal__donation_amount_too_high)
|
||||
.setMessage(getString(R.string.DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer, max))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment))
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
error("Unsupported operation")
|
||||
}
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) = error("iDEAL transfer isn't supported for gifts.")
|
||||
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
error("Unsupported operation")
|
||||
}
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) = error("Bank transfer isn't supported for gifts.")
|
||||
|
||||
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
val mainActivityIntent = MainActivity.clearTop(requireContext())
|
||||
@@ -277,11 +304,13 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProcessorActionProcessed() = Unit
|
||||
override fun onSubscriptionCancelled(inAppPaymentType: InAppPaymentType) = error("Not supported for gifts")
|
||||
|
||||
override fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) = error("Unsupported operation")
|
||||
override fun onProcessorActionProcessed() {
|
||||
// TODO [alex] -- what do?
|
||||
}
|
||||
|
||||
override fun onUserLaunchedAnExternalApplication() = Unit
|
||||
override fun onUserLaunchedAnExternalApplication() = error("Not supported for gifts.")
|
||||
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Unsupported operation")
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Not supported for gifts")
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
@@ -25,14 +25,10 @@ import java.util.Locale
|
||||
*/
|
||||
class GiftFlowRepository {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GiftFlowRepository::class.java)
|
||||
}
|
||||
|
||||
fun insertInAppPayment(context: Context, giftSnapshot: GiftFlowState): Single<InAppPaymentTable.InAppPayment> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentTable.Type.ONE_TIME_GIFT,
|
||||
type = InAppPaymentType.ONE_TIME_GIFT,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,13 +7,13 @@ import androidx.navigation.NavDirections
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -31,12 +31,12 @@ private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_
|
||||
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||
private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create"
|
||||
|
||||
class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent {
|
||||
|
||||
private var wasConfigurationUpdated = false
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
override val googlePayResultPublisher: Subject<InAppPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||
@@ -57,8 +57,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.RECURRING_DONATION)
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.ONE_TIME_DONATION)
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToCheckout(InAppPaymentType.RECURRING_DONATION)
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToCheckout(InAppPaymentType.ONE_TIME_DONATION)
|
||||
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
|
||||
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
|
||||
@@ -128,7 +128,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,10 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
@@ -21,7 +24,7 @@ import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||
class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(
|
||||
RemoteBackupsSettingsState(
|
||||
messageBackupsTier = SignalStore.backup().backupTier,
|
||||
messageBackupsType = null,
|
||||
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
|
||||
backupSize = SignalStore.backup().totalBackupSize,
|
||||
backupsFrequency = SignalStore.backup().backupFrequency
|
||||
@@ -30,6 +33,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
val state: State<RemoteBackupsSettingsState> = internalState
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
||||
SignalStore.backup().backupWithCellular = canBackUpUsingCellular
|
||||
internalState.value = state.value.copy(canBackUpUsingCellular = canBackUpUsingCellular)
|
||||
@@ -51,12 +58,17 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
internalState.value = state.value.copy(
|
||||
messageBackupsTier = SignalStore.backup().backupTier,
|
||||
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
|
||||
backupSize = SignalStore.backup().totalBackupSize,
|
||||
backupsFrequency = SignalStore.backup().backupFrequency
|
||||
)
|
||||
viewModelScope.launch {
|
||||
val tier = SignalStore.backup().backupTier
|
||||
val backupType = if (tier != null) BackupRepository.getBackupsType(tier) else null
|
||||
|
||||
internalState.value = state.value.copy(
|
||||
messageBackupsType = backupType,
|
||||
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
|
||||
backupSize = SignalStore.backup().totalBackupSize,
|
||||
backupsFrequency = SignalStore.backup().backupFrequency
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun turnOffAndDeleteBackups() {
|
||||
|
||||
@@ -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 {}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -8,18 +8,26 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups.type
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class BackupsTypeSettingsViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(
|
||||
BackupsTypeSettingsState(
|
||||
backupsTier = SignalStore.backup().backupTier
|
||||
)
|
||||
)
|
||||
private val internalState = mutableStateOf(BackupsTypeSettingsState())
|
||||
|
||||
val state: State<BackupsTypeSettingsState> = internalState
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
internalState.value = state.value.copy(backupsTier = SignalStore.backup().backupTier)
|
||||
viewModelScope.launch {
|
||||
val tier = SignalStore.backup().backupTier
|
||||
internalState.value = state.value.copy(
|
||||
messageBackupsType = if (tier != null) BackupRepository.getBackupsType(tier) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Parcelable
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface DonationPaymentComponent {
|
||||
interface InAppPaymentComponent {
|
||||
val stripeRepository: StripeRepository
|
||||
val googlePayResultPublisher: Subject<GooglePayResult>
|
||||
|
||||
@@ -14,7 +14,9 @@ import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
@@ -38,6 +40,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
@@ -167,9 +171,9 @@ object InAppPaymentsRepository {
|
||||
*/
|
||||
fun resolveJobQueueKey(inAppPayment: InAppPaymentTable.InAppPayment): String {
|
||||
return when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT, InAppPaymentTable.Type.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
|
||||
InAppPaymentTable.Type.RECURRING_DONATION, InAppPaymentTable.Type.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||
InAppPaymentType.ONE_TIME_GIFT, InAppPaymentType.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
|
||||
InAppPaymentType.RECURRING_DONATION, InAppPaymentType.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,13 +200,13 @@ object InAppPaymentsRepository {
|
||||
/**
|
||||
* Maps a payment type into a request code for grabbing a Google Pay token.
|
||||
*/
|
||||
fun getGooglePayRequestCode(inAppPaymentType: InAppPaymentTable.Type): Int {
|
||||
fun getGooglePayRequestCode(inAppPaymentType: InAppPaymentType): Int {
|
||||
return when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> 16143
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> 16141
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> 16142
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> 16144
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentType.ONE_TIME_GIFT -> 16143
|
||||
InAppPaymentType.ONE_TIME_DONATION -> 16141
|
||||
InAppPaymentType.RECURRING_DONATION -> 16142
|
||||
InAppPaymentType.RECURRING_BACKUP -> 16144
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,14 +214,14 @@ object InAppPaymentsRepository {
|
||||
* Converts an error source to a persistable type. For types that don't map,
|
||||
* UNKNOWN is returned.
|
||||
*/
|
||||
fun DonationErrorSource.toInAppPaymentType(): InAppPaymentTable.Type {
|
||||
fun DonationErrorSource.toInAppPaymentType(): InAppPaymentType {
|
||||
return when (this) {
|
||||
DonationErrorSource.ONE_TIME -> InAppPaymentTable.Type.ONE_TIME_DONATION
|
||||
DonationErrorSource.MONTHLY -> InAppPaymentTable.Type.RECURRING_DONATION
|
||||
DonationErrorSource.GIFT -> InAppPaymentTable.Type.ONE_TIME_GIFT
|
||||
DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentTable.Type.UNKNOWN
|
||||
DonationErrorSource.KEEP_ALIVE -> InAppPaymentTable.Type.UNKNOWN
|
||||
DonationErrorSource.UNKNOWN -> InAppPaymentTable.Type.UNKNOWN
|
||||
DonationErrorSource.ONE_TIME -> InAppPaymentType.ONE_TIME_DONATION
|
||||
DonationErrorSource.MONTHLY -> InAppPaymentType.RECURRING_DONATION
|
||||
DonationErrorSource.GIFT -> InAppPaymentType.ONE_TIME_GIFT
|
||||
DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentType.UNKNOWN
|
||||
DonationErrorSource.KEEP_ALIVE -> InAppPaymentType.UNKNOWN
|
||||
DonationErrorSource.UNKNOWN -> InAppPaymentType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +253,28 @@ object InAppPaymentsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun InAppPaymentType.toErrorSource(): DonationErrorSource {
|
||||
return when (this) {
|
||||
InAppPaymentType.UNKNOWN -> DonationErrorSource.UNKNOWN
|
||||
InAppPaymentType.ONE_TIME_GIFT -> DonationErrorSource.GIFT
|
||||
InAppPaymentType.ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
|
||||
InAppPaymentType.RECURRING_DONATION -> DonationErrorSource.MONTHLY
|
||||
InAppPaymentType.RECURRING_BACKUP -> DonationErrorSource.UNKNOWN // TODO [message-backups] error handling
|
||||
}
|
||||
}
|
||||
|
||||
fun InAppPaymentType.toSubscriberType(): InAppPaymentSubscriberRecord.Type? {
|
||||
return when (this) {
|
||||
InAppPaymentType.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
|
||||
InAppPaymentType.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun InAppPaymentType.requireSubscriberType(): InAppPaymentSubscriberRecord.Type {
|
||||
return requireNotNull(toSubscriberType())
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts network ChargeFailure objects into the form we can persist in the database.
|
||||
*/
|
||||
@@ -375,6 +401,7 @@ object InAppPaymentsRepository {
|
||||
@WorkerThread
|
||||
fun getSubscriber(type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
val currency = SignalStore.donationsValues().getSubscriptionCurrency(type)
|
||||
Log.d(TAG, "Attempting to retrieve subscriber of type $type for ${currency.currencyCode}")
|
||||
|
||||
return getSubscriber(currency, type)
|
||||
}
|
||||
@@ -408,13 +435,13 @@ object InAppPaymentsRepository {
|
||||
/**
|
||||
* Emits a stream of status updates for donations of the given type. Only One-time donations and recurring donations are currently supported.
|
||||
*/
|
||||
fun observeInAppPaymentRedemption(type: InAppPaymentTable.Type): Observable<DonationRedemptionJobStatus> {
|
||||
fun observeInAppPaymentRedemption(type: InAppPaymentType): Observable<DonationRedemptionJobStatus> {
|
||||
val jobStatusObservable: Observable<DonationRedemptionJobStatus> = when (type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> Observable.empty()
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> Observable.empty()
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption()
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption()
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> Observable.empty()
|
||||
InAppPaymentType.UNKNOWN -> Observable.empty()
|
||||
InAppPaymentType.ONE_TIME_GIFT -> Observable.empty()
|
||||
InAppPaymentType.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption()
|
||||
InAppPaymentType.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption()
|
||||
InAppPaymentType.RECURRING_BACKUP -> Observable.empty()
|
||||
}
|
||||
|
||||
val fromDatabase: Observable<DonationRedemptionJobStatus> = Observable.create { emitter ->
|
||||
@@ -467,6 +494,17 @@ object InAppPaymentsRepository {
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun scheduleSyncForAccountRecordChange() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
scheduleSyncForAccountRecordChangeSync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleSyncForAccountRecordChangeSync() {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
private fun InAppPaymentTable.InAppPayment.toPendingOneTimeDonation(): PendingOneTimeDonation? {
|
||||
if (type.recurring) {
|
||||
return null
|
||||
|
||||
@@ -6,6 +6,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
|
||||
@@ -96,7 +98,7 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
|
||||
}
|
||||
|
||||
fun ensureSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type, isRotation: Boolean = false): Completable {
|
||||
Log.d(TAG, "Ensuring SubscriberId exists on Signal service {isRotation?$isRotation}...", true)
|
||||
Log.d(TAG, "Ensuring SubscriberId for type $subscriberType exists on Signal service {isRotation?$isRotation}...", true)
|
||||
val subscriberId: SubscriberId = if (isRotation) {
|
||||
SubscriberId.generate()
|
||||
} else {
|
||||
@@ -138,7 +140,12 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<EmptyResponse>::flattenResult)
|
||||
.ignoreElement()
|
||||
.doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) }
|
||||
.doOnComplete {
|
||||
Log.d(TAG, "Cancelled active subscription.", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
InAppPaymentsRepository.scheduleSyncForAccountRecordChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelActiveSubscriptionIfNecessary(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
|
||||
|
||||
@@ -5,14 +5,15 @@ import android.content.Intent
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.signal.donations.json.StripeIntentStatus
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.OneTimeDonationError
|
||||
@@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
@@ -46,8 +46,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
* 1. Confirm the PaymentIntent via the Stripe API
|
||||
*/
|
||||
class StripeRepository(
|
||||
activity: Activity,
|
||||
private val subscriberType: InAppPaymentSubscriberRecord.Type = InAppPaymentSubscriberRecord.Type.DONATION
|
||||
activity: Activity
|
||||
) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||
|
||||
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
||||
@@ -58,17 +57,6 @@ class StripeRepository(
|
||||
return googlePayApi.queryIsReadyToPay()
|
||||
}
|
||||
|
||||
fun scheduleSyncForAccountRecordChange() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
scheduleSyncForAccountRecordChangeSync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleSyncForAccountRecordChangeSync() {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) {
|
||||
Log.d(TAG, "Requesting a token from google pay...")
|
||||
googlePayApi.requestPayment(price, label, requestCode)
|
||||
@@ -118,11 +106,12 @@ class StripeRepository(
|
||||
}
|
||||
|
||||
fun createAndConfirmSetupIntent(
|
||||
inAppPaymentType: InAppPaymentType,
|
||||
paymentSource: StripeApi.PaymentSource,
|
||||
paymentSourceType: PaymentSourceType.Stripe
|
||||
): Single<StripeApi.Secure3DSAction> {
|
||||
Log.d(TAG, "Continuing subscription setup...", true)
|
||||
return stripeApi.createSetupIntent(paymentSourceType)
|
||||
return stripeApi.createSetupIntent(inAppPaymentType, paymentSourceType)
|
||||
.flatMap { result ->
|
||||
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
|
||||
stripeApi.confirmSetupIntent(paymentSource, result.setupIntent)
|
||||
@@ -169,7 +158,7 @@ class StripeRepository(
|
||||
* it means that the PaymentMethod is already tied to a PayPal account. We can retry in this
|
||||
* situation by simply deleting the old subscriber id on the service and replacing it.
|
||||
*/
|
||||
private fun createPaymentMethod(paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single<StripeClientSecret> {
|
||||
private fun createPaymentMethod(subscriberType: InAppPaymentSubscriberRecord.Type, paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single<StripeClientSecret> {
|
||||
return Single.fromCallable { InAppPaymentsRepository.requireSubscriber(subscriberType) }
|
||||
.flatMap {
|
||||
Single.fromCallable {
|
||||
@@ -180,16 +169,16 @@ class StripeRepository(
|
||||
}
|
||||
.flatMap { serviceResponse ->
|
||||
if (retryOn409 && serviceResponse.status == 409) {
|
||||
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(paymentSourceType, retryOn409 = false))
|
||||
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, paymentSourceType, retryOn409 = false))
|
||||
} else {
|
||||
serviceResponse.flattenResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
Log.d(TAG, "Fetching setup intent from Signal service...")
|
||||
return createPaymentMethod(sourceType)
|
||||
return createPaymentMethod(inAppPaymentType.requireSubscriberType(), sourceType)
|
||||
.map {
|
||||
StripeIntentAccessor(
|
||||
objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT,
|
||||
@@ -231,6 +220,7 @@ class StripeRepository(
|
||||
fun setDefaultPaymentMethod(
|
||||
paymentMethodId: String,
|
||||
setupIntentId: String,
|
||||
subscriberType: InAppPaymentSubscriberRecord.Type,
|
||||
paymentSourceType: PaymentSourceType
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,9 +4,10 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -16,7 +17,7 @@ import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
class SetCurrencyViewModel(
|
||||
private val inAppPaymentType: InAppPaymentTable.Type,
|
||||
private val inAppPaymentType: InAppPaymentType,
|
||||
supportedCurrencyCodes: List<String>
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -89,7 +90,7 @@ class SetCurrencyViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val inAppPaymentType: InAppPaymentTable.Type, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||
class Factory(private val inAppPaymentType: InAppPaymentType, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(SetCurrencyViewModel(inAppPaymentType, supportedCurrencyCodes))!!
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.navArgs
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
|
||||
/**
|
||||
* Home base for all checkout flows.
|
||||
*/
|
||||
class CheckoutFlowActivity : FragmentWrapperActivity(), InAppPaymentComponent {
|
||||
|
||||
companion object {
|
||||
fun createIntent(context: Context, inAppPaymentType: InAppPaymentType): Intent {
|
||||
return Intent(context, CheckoutFlowActivity::class.java).putExtras(
|
||||
CheckoutFlowActivityArgs.Builder(inAppPaymentType).build().toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<InAppPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
private val args by navArgs<CheckoutFlowActivityArgs>()
|
||||
|
||||
override fun getFragment(): Fragment {
|
||||
return CheckoutNavHostFragment.create(args.inAppPaymentType)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
sealed class DonateToSignalAction {
|
||||
data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentTable.Type, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentType, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayGatewaySelectorDialog(val inAppPayment: InAppPaymentTable.InAppPayment) : DonateToSignalAction()
|
||||
object CancelSubscription : DonateToSignalAction()
|
||||
data class UpdateSubscription(val inAppPayment: InAppPaymentTable.InAppPayment, val isLongRunning: Boolean) : DonateToSignalAction()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
/**
|
||||
* Activity wrapper for donate to signal screen. An activity is needed because Google Pay uses the
|
||||
* activity [DonateToSignalActivity.startActivityForResult] flow that would be missed by a parent fragment.
|
||||
*/
|
||||
class DonateToSignalActivity : FragmentWrapperActivity(), DonationPaymentComponent {
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun getFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.donate_to_signal, DonateToSignalFragmentArgs.Builder(InAppPaymentTable.Type.ONE_TIME_DONATION).build().toBundle())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.isLongRunning
|
||||
@@ -18,71 +18,71 @@ import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class DonateToSignalState(
|
||||
val inAppPaymentType: InAppPaymentTable.Type,
|
||||
val inAppPaymentType: InAppPaymentType,
|
||||
val oneTimeDonationState: OneTimeDonationState = OneTimeDonationState(),
|
||||
val monthlyDonationState: MonthlyDonationState = MonthlyDonationState()
|
||||
) {
|
||||
|
||||
val areFieldsEnabled: Boolean
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.donationStage == DonationStage.READY
|
||||
InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.donationStage == DonationStage.READY
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val badge: Badge?
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.badge
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription?.badge
|
||||
InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.badge
|
||||
InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedSubscription?.badge
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canSetCurrency: Boolean
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
InAppPaymentType.ONE_TIME_DONATION -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val selectedCurrency: Currency
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectedCurrency
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedCurrency
|
||||
InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.selectedCurrency
|
||||
InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedCurrency
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val selectableCurrencyCodes: List<String>
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectableCurrencyCodes
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectableCurrencyCodes
|
||||
InAppPaymentType.ONE_TIME_DONATION -> oneTimeDonationState.selectableCurrencyCodes
|
||||
InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectableCurrencyCodes
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val level: Int
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> 1
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription!!.level
|
||||
InAppPaymentType.ONE_TIME_DONATION -> 1
|
||||
InAppPaymentType.RECURRING_DONATION -> monthlyDonationState.selectedSubscription!!.level
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val continueEnabled: Boolean
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
InAppPaymentType.ONE_TIME_DONATION -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canContinue: Boolean
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
|
||||
InAppPaymentType.ONE_TIME_DONATION -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentType.RECURRING_DONATION -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canUpdate: Boolean
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> false
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||
InAppPaymentType.ONE_TIME_DONATION -> false
|
||||
InAppPaymentType.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.money.PlatformCurrencyUtil
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
@@ -50,7 +51,7 @@ import java.util.Optional
|
||||
* only in charge of rendering our "current view of the world."
|
||||
*/
|
||||
class DonateToSignalViewModel(
|
||||
startType: InAppPaymentTable.Type,
|
||||
startType: InAppPaymentType,
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository,
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
|
||||
) : ViewModel() {
|
||||
@@ -137,8 +138,8 @@ class DonateToSignalViewModel(
|
||||
store.update {
|
||||
it.copy(
|
||||
inAppPaymentType = when (it.inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> InAppPaymentTable.Type.RECURRING_DONATION
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> InAppPaymentTable.Type.ONE_TIME_DONATION
|
||||
InAppPaymentType.ONE_TIME_DONATION -> InAppPaymentType.RECURRING_DONATION
|
||||
InAppPaymentType.RECURRING_DONATION -> InAppPaymentType.ONE_TIME_DONATION
|
||||
else -> error("Should never get here.")
|
||||
}
|
||||
)
|
||||
@@ -222,8 +223,8 @@ class DonateToSignalViewModel(
|
||||
|
||||
private fun getAmount(snapshot: DonateToSignalState): FiatMoney {
|
||||
return when (snapshot.inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> getSelectedSubscriptionCost()
|
||||
InAppPaymentType.ONE_TIME_DONATION -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||
InAppPaymentType.RECURRING_DONATION -> getSelectedSubscriptionCost()
|
||||
else -> error("This ViewModel does not support ${snapshot.inAppPaymentType}.")
|
||||
}
|
||||
}
|
||||
@@ -237,7 +238,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun initializeOneTimeDonationState(oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository) {
|
||||
val oneTimeDonationFromJob: Observable<Optional<PendingOneTimeDonation>> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION).map {
|
||||
val oneTimeDonationFromJob: Observable<Optional<PendingOneTimeDonation>> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION).map {
|
||||
when (it) {
|
||||
is DonationRedemptionJobStatus.PendingExternalVerification -> Optional.ofNullable(it.pendingOneTimeDonation)
|
||||
|
||||
@@ -331,7 +332,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun monitorLevelUpdateProcessing() {
|
||||
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION)
|
||||
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
|
||||
monthlyDonationDisposables += Observable
|
||||
.combineLatest(redemptionJobStatus, LevelUpdate.isProcessing, ::Pair)
|
||||
@@ -420,7 +421,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startType: InAppPaymentTable.Type,
|
||||
private val startType: InAppPaymentType,
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService),
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(AppDependencies.donationsService)
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.DonationPillToggleBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
@@ -16,7 +16,7 @@ object DonationPillToggle {
|
||||
}
|
||||
|
||||
class Model(
|
||||
val selected: InAppPaymentTable.Type,
|
||||
val selected: InAppPaymentType,
|
||||
val onClick: () -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
@@ -29,10 +29,10 @@ object DonationPillToggle {
|
||||
private class ViewHolder(binding: DonationPillToggleBinding) : BindingViewHolder<Model, DonationPillToggleBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
when (model.selected) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> {
|
||||
InAppPaymentType.ONE_TIME_DONATION -> {
|
||||
presentButtons(model, binding.oneTime, binding.monthly)
|
||||
}
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
presentButtons(model, binding.monthly, binding.oneTime)
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,17 +16,17 @@ import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -40,9 +40,9 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
private val viewModel: CreditCardViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
R.id.checkout_flow,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
}
|
||||
|
||||
// TODO [message-backups] Copy for this button in backups checkout flow.
|
||||
binding.continueButton.text = if (args.inAppPayment.type == InAppPaymentTable.Type.RECURRING_DONATION) {
|
||||
binding.continueButton.text = if (args.inAppPayment.type == InAppPaymentType.RECURRING_DONATION) {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.navigation.fragment.navArgs
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
@@ -19,8 +20,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
@@ -41,7 +42,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
private val args: GatewaySelectorBottomSheetArgs by navArgs()
|
||||
|
||||
private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = {
|
||||
GatewaySelectorViewModel.Factory(args, requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
GatewaySelectorViewModel.Factory(args, requireListener<InAppPaymentComponent>().stripeRepository)
|
||||
})
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
@@ -206,11 +207,11 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> error("This type is not supported") // TODO [message-backups] necessary?
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> presentMonthlyText(context, inAppPayment)
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> presentOneTimeText(context, inAppPayment)
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> presentGiftText(context, inAppPayment)
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentType.RECURRING_BACKUP -> error("This type is not supported") // TODO [message-backups] necessary?
|
||||
InAppPaymentType.RECURRING_DONATION -> presentMonthlyText(context, inAppPayment)
|
||||
InAppPaymentType.ONE_TIME_DONATION -> presentOneTimeText(context, inAppPayment)
|
||||
InAppPaymentType.ONE_TIME_GIFT -> presentGiftText(context, inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
@@ -123,7 +125,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
) {
|
||||
Log.d(TAG, "Proceeding with one-time payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentType.ONE_TIME_GIFT) {
|
||||
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(RecipientId.from(inAppPayment.data.recipientId!!))
|
||||
} else {
|
||||
Completable.complete()
|
||||
@@ -168,7 +170,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
}
|
||||
|
||||
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>) {
|
||||
Log.d(TAG, "Proceeding with monthly payment pipeline...")
|
||||
Log.d(TAG, "Proceeding with monthly payment pipeline for InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
val setup = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,7 +23,9 @@ import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
@@ -46,9 +48,9 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
private val viewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
R.id.checkout_flow,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -94,6 +96,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
status = DonationProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -108,6 +111,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
status = DonationProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
|
||||
@@ -13,11 +13,14 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
@@ -30,8 +33,6 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
@@ -76,7 +77,7 @@ class StripePaymentInProgressViewModel(
|
||||
}
|
||||
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, nextActionHandler: StripeNextActionHandler) {
|
||||
Log.d(TAG, "Proceeding with donation...", true)
|
||||
Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(inAppPayment.type.toErrorSource())
|
||||
|
||||
@@ -145,7 +146,7 @@ class StripePaymentInProgressViewModel(
|
||||
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: StripeNextActionHandler) {
|
||||
val ensureSubscriberId: Completable = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
|
||||
val createAndConfirmSetupIntent: Single<StripeApi.Secure3DSAction> = paymentSourceProvider.paymentSource.flatMap {
|
||||
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
stripeRepository.createAndConfirmSetupIntent(inAppPayment.type, it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
}
|
||||
|
||||
val setLevel: Completable = recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceProvider.paymentSourceType)
|
||||
@@ -171,7 +172,7 @@ class StripePaymentInProgressViewModel(
|
||||
)
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, secure3DSAction.paymentMethodId) }
|
||||
}
|
||||
.flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, paymentSourceProvider.paymentSourceType) }
|
||||
.flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, inAppPayment.type.requireSubscriberType(), paymentSourceProvider.paymentSourceType) }
|
||||
.onErrorResumeNext {
|
||||
when (it) {
|
||||
is DonationError -> Completable.error(it)
|
||||
@@ -202,7 +203,7 @@ class StripePaymentInProgressViewModel(
|
||||
|
||||
val amount = inAppPayment.data.amount!!.toFiatMoney()
|
||||
val recipientId = inAppPayment.data.recipientId?.let { RecipientId.from(it) } ?: Recipient.self().id
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentType.ONE_TIME_GIFT) {
|
||||
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(recipientId)
|
||||
} else {
|
||||
Completable.complete()
|
||||
@@ -257,9 +258,6 @@ class StripePaymentInProgressViewModel(
|
||||
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Cancellation succeeded", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
stripeRepository.scheduleSyncForAccountRecordChange()
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
|
||||
@@ -55,8 +55,8 @@ import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
@@ -80,9 +80,9 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModels()
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
R.id.checkout_flow,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ import org.signal.core.ui.Texts
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
@@ -84,9 +84,9 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
}
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
R.id.checkout_flow,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<InAppPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
@@ -38,7 +39,7 @@ class DonationErrorParams<V> private constructor(
|
||||
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> getStillProcessingErrorParams(context, callback)
|
||||
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> getBadgeCredentialValidationErrorParams(context, callback)
|
||||
is DonationError.BadgeRedemptionError.GenericError -> getGenericRedemptionError(context, throwable.source.toInAppPaymentType(), callback)
|
||||
else -> getGenericRedemptionError(context, InAppPaymentTable.Type.ONE_TIME_DONATION, callback)
|
||||
else -> getGenericRedemptionError(context, InAppPaymentType.ONE_TIME_DONATION, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +82,9 @@ class DonationErrorParams<V> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getGenericRedemptionError(context: Context, type: InAppPaymentTable.Type, callback: Callback<V>): DonationErrorParams<V> {
|
||||
private fun <V> getGenericRedemptionError(context: Context, type: InAppPaymentType, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return when (type) {
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> DonationErrorParams(
|
||||
InAppPaymentType.ONE_TIME_GIFT -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__donation_failed,
|
||||
message = R.string.DonationsErrors__your_payment_was_processed_but,
|
||||
positiveAction = callback.onContactSupport(context),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
@@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
|
||||
/**
|
||||
* Wrapper activity for ConversationFragment.
|
||||
*/
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, InAppPaymentComponent {
|
||||
|
||||
companion object {
|
||||
private const val STATE_WATERMARK = "share_data_watermark"
|
||||
@@ -33,7 +33,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
|
||||
override val voiceNoteMediaController = VoiceNoteMediaController(this, true)
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
override val googlePayResultPublisher: Subject<InAppPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
private val motionEventRelay: MotionEventRelay by viewModels()
|
||||
private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels()
|
||||
@@ -87,7 +87,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
googlePayResultPublisher.onNext(InAppPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
private fun replaceFragment() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.updateAll
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.parcelers.MillisecondDurationParceler
|
||||
@@ -129,7 +128,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
}
|
||||
|
||||
fun insert(
|
||||
type: Type,
|
||||
type: InAppPaymentType,
|
||||
state: State,
|
||||
subscriberId: SubscriberId?,
|
||||
endOfPeriod: Duration?,
|
||||
@@ -193,18 +192,18 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
.readToSingleObject(InAppPayment.Companion)
|
||||
}
|
||||
|
||||
fun getByEndOfPeriod(type: Type, endOfPeriod: Duration): InAppPayment? {
|
||||
fun getByEndOfPeriod(type: InAppPaymentType, endOfPeriod: Duration): InAppPayment? {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$TYPE = ? AND $END_OF_PERIOD = ?", Type.serialize(type), endOfPeriod.inWholeSeconds)
|
||||
.where("$TYPE = ? AND $END_OF_PERIOD = ?", InAppPaymentType.serialize(type), endOfPeriod.inWholeSeconds)
|
||||
.run()
|
||||
.readToSingleObject(InAppPayment.Companion)
|
||||
}
|
||||
|
||||
fun getByLatestEndOfPeriod(type: Type): InAppPayment? {
|
||||
fun getByLatestEndOfPeriod(type: InAppPaymentType): InAppPayment? {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$TYPE = ? AND $END_OF_PERIOD > 0", Type.serialize(type))
|
||||
.where("$TYPE = ? AND $END_OF_PERIOD > 0", InAppPaymentType.serialize(type))
|
||||
.orderBy("$END_OF_PERIOD DESC")
|
||||
.limit(1)
|
||||
.run()
|
||||
@@ -248,9 +247,9 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
.where(
|
||||
"$STATE = ? AND ($TYPE = ? OR $TYPE = ? OR $TYPE = ?)",
|
||||
State.serialize(State.PENDING),
|
||||
Type.serialize(Type.RECURRING_DONATION),
|
||||
Type.serialize(Type.ONE_TIME_DONATION),
|
||||
Type.serialize(Type.ONE_TIME_GIFT)
|
||||
InAppPaymentType.serialize(InAppPaymentType.RECURRING_DONATION),
|
||||
InAppPaymentType.serialize(InAppPaymentType.ONE_TIME_DONATION),
|
||||
InAppPaymentType.serialize(InAppPaymentType.ONE_TIME_GIFT)
|
||||
)
|
||||
.run()
|
||||
}
|
||||
@@ -258,12 +257,12 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
/**
|
||||
* Returns whether there are any pending donations in the database.
|
||||
*/
|
||||
fun hasPending(type: Type): Boolean {
|
||||
fun hasPending(type: InAppPaymentType): Boolean {
|
||||
return readableDatabase.exists(TABLE_NAME)
|
||||
.where(
|
||||
"$STATE = ? AND $TYPE = ?",
|
||||
State.serialize(State.PENDING),
|
||||
Type.serialize(type)
|
||||
InAppPaymentType.serialize(type)
|
||||
)
|
||||
.run()
|
||||
}
|
||||
@@ -271,7 +270,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
/**
|
||||
* Retrieves from the database the latest payment of the given type that is either in the PENDING or WAITING_FOR_AUTHORIZATION state.
|
||||
*/
|
||||
fun getLatestInAppPaymentByType(type: Type): InAppPayment? {
|
||||
fun getLatestInAppPaymentByType(type: InAppPaymentType): InAppPayment? {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where(
|
||||
@@ -279,7 +278,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
State.serialize(State.PENDING),
|
||||
State.serialize(State.WAITING_FOR_AUTHORIZATION),
|
||||
State.serialize(State.END),
|
||||
Type.serialize(type)
|
||||
InAppPaymentType.serialize(type)
|
||||
)
|
||||
.orderBy("$INSERTED_AT DESC")
|
||||
.limit(1)
|
||||
@@ -311,7 +310,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
@TypeParceler<SubscriberId?, NullableSubscriberIdParceler>
|
||||
data class InAppPayment(
|
||||
val id: InAppPaymentId,
|
||||
val type: Type,
|
||||
val type: InAppPaymentType,
|
||||
val state: State,
|
||||
val insertedAt: Duration,
|
||||
val updatedAt: Duration,
|
||||
@@ -329,7 +328,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
override fun serialize(data: InAppPayment): ContentValues {
|
||||
return contentValuesOf(
|
||||
ID to data.id.serialize(),
|
||||
TYPE to data.type.apply { check(this != Type.UNKNOWN) }.code,
|
||||
TYPE to data.type.apply { check(this != InAppPaymentType.UNKNOWN) }.code,
|
||||
STATE to data.state.code,
|
||||
INSERTED_AT to data.insertedAt.inWholeSeconds,
|
||||
UPDATED_AT to data.updatedAt.inWholeSeconds,
|
||||
@@ -343,7 +342,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
override fun deserialize(input: Cursor): InAppPayment {
|
||||
return InAppPayment(
|
||||
id = InAppPaymentId(input.requireLong(ID)),
|
||||
type = Type.deserialize(input.requireInt(TYPE)),
|
||||
type = InAppPaymentType.deserialize(input.requireInt(TYPE)),
|
||||
state = State.deserialize(input.requireInt(STATE)),
|
||||
insertedAt = input.requireLong(INSERTED_AT).seconds,
|
||||
updatedAt = input.requireLong(UPDATED_AT).seconds,
|
||||
@@ -356,61 +355,6 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type(val code: Int, val recurring: Boolean) {
|
||||
/**
|
||||
* Used explicitly for mapping DonationErrorSource. Writing this value
|
||||
* into an InAppPayment is an error.
|
||||
*/
|
||||
UNKNOWN(-1, false),
|
||||
|
||||
/**
|
||||
* This payment is for a gift badge
|
||||
*/
|
||||
ONE_TIME_GIFT(0, false),
|
||||
|
||||
/**
|
||||
* This payment is for a one-time donation
|
||||
*/
|
||||
ONE_TIME_DONATION(1, false),
|
||||
|
||||
/**
|
||||
* This payment is for a recurring donation
|
||||
*/
|
||||
RECURRING_DONATION(2, true),
|
||||
|
||||
/**
|
||||
* This payment is for a recurring backup payment
|
||||
*/
|
||||
RECURRING_BACKUP(3, true);
|
||||
|
||||
companion object : Serializer<Type, Int> {
|
||||
override fun serialize(data: Type): Int = data.code
|
||||
override fun deserialize(input: Int): Type = values().first { it.code == input }
|
||||
}
|
||||
|
||||
fun toErrorSource(): DonationErrorSource {
|
||||
return when (this) {
|
||||
UNKNOWN -> DonationErrorSource.UNKNOWN
|
||||
ONE_TIME_GIFT -> DonationErrorSource.GIFT
|
||||
ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
|
||||
RECURRING_DONATION -> DonationErrorSource.MONTHLY
|
||||
RECURRING_BACKUP -> DonationErrorSource.UNKNOWN // TODO [message-backups] error handling
|
||||
}
|
||||
}
|
||||
|
||||
fun toSubscriberType(): InAppPaymentSubscriberRecord.Type? {
|
||||
return when (this) {
|
||||
RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
|
||||
RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun requireSubscriberType(): InAppPaymentSubscriberRecord.Type {
|
||||
return requireNotNull(toSubscriberType())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the payment pipeline state for a given in-app payment
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
|
||||
Log.d(TAG, "Attempting to redeem token... isForSubscription: " + isForSubscription(), true);
|
||||
ServiceResponse<EmptyResponse> response = AppDependencies.getDonationsService()
|
||||
.redeemReceipt(presentation,
|
||||
.redeemDonationReceipt(presentation,
|
||||
SignalStore.donationsValues().getDisplayBadgesOnProfile(),
|
||||
makePrimary);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobs
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
@@ -15,11 +16,11 @@ import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.Companion.toDonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
@@ -68,7 +69,7 @@ class ExternalLaunchDonationJob private constructor(
|
||||
override fun onFailure() {
|
||||
if (donationError != null) {
|
||||
when (stripe3DSData.inAppPayment.type) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> {
|
||||
InAppPaymentType.ONE_TIME_DONATION -> {
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(
|
||||
DonationSerializationHelper.createPendingOneTimeDonationProto(
|
||||
Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!),
|
||||
@@ -80,7 +81,7 @@ class ExternalLaunchDonationJob private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = stripe3DSData.inAppPayment.data.level,
|
||||
@@ -113,7 +114,7 @@ class ExternalLaunchDonationJob private constructor(
|
||||
checkIntentStatus(stripePaymentIntent.status)
|
||||
|
||||
Log.i(TAG, "Creating and inserting donation receipt record.", true)
|
||||
val donationReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_DONATION) {
|
||||
val donationReceiptRecord = if (stripe3DSData.inAppPayment.type == InAppPaymentType.ONE_TIME_DONATION) {
|
||||
DonationReceiptRecord.createForBoost(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
|
||||
} else {
|
||||
DonationReceiptRecord.createForGift(stripe3DSData.inAppPayment.data.amount!!.toFiatMoney())
|
||||
@@ -270,7 +271,7 @@ class ExternalLaunchDonationJob private constructor(
|
||||
error("Not needed, this job should not be creating intents.")
|
||||
}
|
||||
|
||||
override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
error("Not needed, this job should not be creating intents.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
@@ -16,6 +17,7 @@ import org.signal.donations.json.StripePaymentIntent
|
||||
import org.signal.donations.json.StripeSetupIntent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -102,7 +104,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = pending3DSData.inAppPayment.type,
|
||||
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||
subscriberId = if (pending3DSData.inAppPayment.type == InAppPaymentTable.Type.RECURRING_DONATION) {
|
||||
subscriberId = if (pending3DSData.inAppPayment.type == InAppPaymentType.RECURRING_DONATION) {
|
||||
InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.DONATION).subscriberId
|
||||
} else {
|
||||
null
|
||||
@@ -132,8 +134,8 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
||||
|
||||
Log.i(TAG, "Creating and inserting receipt.", true)
|
||||
val receipt = when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
|
||||
InAppPaymentType.ONE_TIME_DONATION -> DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
|
||||
InAppPaymentType.ONE_TIME_GIFT -> DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
|
||||
else -> {
|
||||
Log.e(TAG, "Unexpected type ${inAppPayment.type}", true)
|
||||
return CheckResult.Failure()
|
||||
@@ -184,8 +186,8 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
||||
|
||||
val subscriber = InAppPaymentsRepository.requireSubscriber(
|
||||
when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
|
||||
InAppPaymentType.RECURRING_DONATION -> InAppPaymentSubscriberRecord.Type.DONATION
|
||||
InAppPaymentType.RECURRING_BACKUP -> InAppPaymentSubscriberRecord.Type.BACKUP
|
||||
else -> {
|
||||
Log.e(TAG, "Expected recurring type but found ${inAppPayment.type}", true)
|
||||
return CheckResult.Failure()
|
||||
@@ -352,7 +354,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
||||
error("Not needed, this job should not be creating intents.")
|
||||
}
|
||||
|
||||
override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
override fun fetchSetupIntent(inAppPaymentType: InAppPaymentType, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
|
||||
error("Not needed, this job should not be creating intents.")
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.badges.gifts.Gifts
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
@@ -50,13 +51,29 @@ class InAppPaymentGiftSendJob private constructor(
|
||||
|
||||
override fun onFailure() {
|
||||
warning("Failed to send gift.")
|
||||
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||
if (inAppPayment != null && inAppPayment.data.error == null) {
|
||||
warn(TAG, "Marking an unknown error. Check logs for more details.")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = true,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.UNKNOWN
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRun() {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||
|
||||
requireNotNull(inAppPayment, "Not found.")
|
||||
check(inAppPayment!!.type == InAppPaymentTable.Type.ONE_TIME_GIFT, "Invalid type: ${inAppPayment.type}")
|
||||
check(inAppPayment!!.type == InAppPaymentType.ONE_TIME_GIFT, "Invalid type: ${inAppPayment.type}")
|
||||
check(inAppPayment.state == InAppPaymentTable.State.PENDING, "Invalid state: ${inAppPayment.state}")
|
||||
requireNotNull(inAppPayment.data.redemption, "No redemption present on data")
|
||||
check(inAppPayment.data.redemption!!.stage == InAppPaymentData.RedemptionState.Stage.REDEMPTION_STARTED, "Invalid stage: ${inAppPayment.data.redemption.stage}")
|
||||
@@ -64,7 +81,21 @@ class InAppPaymentGiftSendJob private constructor(
|
||||
val recipient = Recipient.resolved(RecipientId.from(requireNotNull(inAppPayment.data.recipientId, "No recipient on data.")))
|
||||
val token = requireNotNull(inAppPayment.data.redemption.receiptCredentialPresentation, "No presentation present on data.")
|
||||
|
||||
check(!recipient.isIndividual || recipient.registered != RecipientTable.RegisteredState.REGISTERED, "Invalid recipient ${recipient.id} for gift send.")
|
||||
if (!recipient.isIndividual || recipient.registered != RecipientTable.RegisteredState.REGISTERED) {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.INVALID_GIFT_RECIPIENT
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
throw Exception("Invalid recipient ${recipient.id} for gift send.")
|
||||
}
|
||||
|
||||
val thread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val outgoingMessage = Gifts.createOutgoingGiftMessage(
|
||||
@@ -129,6 +160,7 @@ class InAppPaymentGiftSendJob private constructor(
|
||||
private fun info(message: String, throwable: Throwable? = null) {
|
||||
Log.i(TAG, "InAppPayment $inAppPaymentId: $message", throwable, true)
|
||||
}
|
||||
|
||||
private fun warning(message: String, throwable: Throwable? = null) {
|
||||
Log.w(TAG, "InAppPayment $inAppPaymentId: $message", throwable, true)
|
||||
}
|
||||
@@ -136,7 +168,7 @@ class InAppPaymentGiftSendJob private constructor(
|
||||
class Factory : Job.Factory<InAppPaymentGiftSendJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): InAppPaymentGiftSendJob {
|
||||
return InAppPaymentGiftSendJob(
|
||||
inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.toString().toLong()),
|
||||
inAppPaymentId = InAppPaymentTable.InAppPaymentId(serializedData!!.decodeToString().toLong()),
|
||||
parameters = parameters
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -70,6 +71,7 @@ class InAppPaymentRecurringContextJob private constructor(
|
||||
|
||||
override fun onAdded() {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||
info("Added context job for payment with state ${inAppPayment?.state}")
|
||||
if (inAppPayment?.state == InAppPaymentTable.State.CREATED) {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
@@ -357,10 +359,7 @@ class InAppPaymentRecurringContextJob private constructor(
|
||||
requestContext: ReceiptCredentialRequestContext
|
||||
) {
|
||||
info("Submitting receipt credential request")
|
||||
val response: ServiceResponse<ReceiptCredentialResponse> = when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
|
||||
else -> throw Exception("Unsupported type: ${inAppPayment.type}")
|
||||
}
|
||||
val response: ServiceResponse<ReceiptCredentialResponse> = AppDependencies.donationsService.submitReceiptCredentialRequestSync(inAppPayment.subscriberId!!, requestContext.request)
|
||||
|
||||
if (response.applicationError.isPresent) {
|
||||
handleApplicationError(inAppPayment, response)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user