Donation error sheet wiring and UI.
This commit is contained in:
parent
e12d467627
commit
079400f89e
33 changed files with 1015 additions and 369 deletions
|
@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
|||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.DonationCompletedDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -51,7 +51,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
private lateinit var reminderView: Stub<ReminderView>
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewLifecycleOwner.lifecycle.addObserver(DonationCompletedDelegate(childFragmentManager, viewLifecycleOwner))
|
||||
viewLifecycleOwner.lifecycle.addObserver(TerminalDonationDelegate(childFragmentManager, viewLifecycleOwner))
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
reminderView = ViewUtil.findStubById(view, R.id.reminder_stub)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
|
@ -30,11 +29,11 @@ import org.signal.core.ui.Buttons
|
|||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.donor.DonationErrorValueCodeSelector
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.donor.DonationErrorValueTypeSelector
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
|
@ -55,7 +54,7 @@ class InternalPendingOneTimeDonationConfigurationFragment : ComposeFragment() {
|
|||
findNavController().popBackStack()
|
||||
},
|
||||
onAddError = {
|
||||
viewModel.state.value = viewModel.state.value.copy(error = PendingOneTimeDonation.Error())
|
||||
viewModel.state.value = viewModel.state.value.copy(error = DonationErrorValue())
|
||||
},
|
||||
onClearError = {
|
||||
viewModel.state.value = viewModel.state.value.copy(error = null)
|
||||
|
@ -83,7 +82,7 @@ private fun ContentPreview() {
|
|||
SignalTheme {
|
||||
Surface {
|
||||
Content(
|
||||
state = PendingOneTimeDonation.Builder().error(PendingOneTimeDonation.Error()).build(),
|
||||
state = PendingOneTimeDonation.Builder().error(DonationErrorValue()).build(),
|
||||
onNavigationClick = {},
|
||||
onClearError = {},
|
||||
onAddError = {},
|
||||
|
@ -104,7 +103,7 @@ private fun Content(
|
|||
onAddError: () -> Unit,
|
||||
onClearError: () -> Unit,
|
||||
onPaymentMethodTypeSelected: (PendingOneTimeDonation.PaymentMethodType) -> Unit,
|
||||
onErrorTypeSelected: (PendingOneTimeDonation.Error.Type) -> Unit,
|
||||
onErrorTypeSelected: (DonationErrorValue.Type) -> Unit,
|
||||
onErrorCodeChanged: (String) -> Unit,
|
||||
onSave: () -> Unit
|
||||
) {
|
||||
|
@ -114,10 +113,6 @@ private fun Content(
|
|||
navigationContentDescription = null,
|
||||
onNavigationClick = onNavigationClick
|
||||
) {
|
||||
val isCodedError = remember(state.error?.type) {
|
||||
state.error?.type in setOf(PendingOneTimeDonation.Error.Type.PROCESSOR_CODE, PendingOneTimeDonation.Error.Type.DECLINE_CODE, PendingOneTimeDonation.Error.Type.FAILURE_CODE)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
modifier = Modifier.padding(it)
|
||||
|
@ -174,85 +169,20 @@ private fun Content(
|
|||
|
||||
if (state.error != null) {
|
||||
item {
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
value = state.error.type.name,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.menuAnchor()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
PendingOneTimeDonation.Error.Type.values().filterNot {
|
||||
state.paymentMethodType == PendingOneTimeDonation.PaymentMethodType.PAYPAL && it == PendingOneTimeDonation.Error.Type.FAILURE_CODE
|
||||
}.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = item.name) },
|
||||
onClick = {
|
||||
onErrorTypeSelected(item)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DonationErrorValueTypeSelector(
|
||||
selectedPaymentMethodType = state.paymentMethodType,
|
||||
selectedErrorType = state.error.type,
|
||||
onErrorTypeSelected = onErrorTypeSelected
|
||||
)
|
||||
}
|
||||
|
||||
if (isCodedError) {
|
||||
item {
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
value = state.error.code,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.menuAnchor()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
when (state.error.type) {
|
||||
PendingOneTimeDonation.Error.Type.PROCESSOR_CODE -> {
|
||||
ProcessorErrorsDropdown(state.paymentMethodType, onErrorCodeChanged)
|
||||
}
|
||||
|
||||
PendingOneTimeDonation.Error.Type.DECLINE_CODE -> {
|
||||
DeclineCodeErrorsDropdown(state.paymentMethodType, onErrorCodeChanged)
|
||||
}
|
||||
|
||||
PendingOneTimeDonation.Error.Type.FAILURE_CODE -> {
|
||||
FailureCodeErrorsDropdown(onErrorCodeChanged)
|
||||
}
|
||||
|
||||
else -> error("This should never happen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DonationErrorValueCodeSelector(
|
||||
selectedPaymentMethodType = state.paymentMethodType,
|
||||
selectedErrorType = state.error.type,
|
||||
selectedErrorCode = state.error.code,
|
||||
onErrorCodeSelected = onErrorCodeChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,54 +197,3 @@ private fun Content(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.ProcessorErrorsDropdown(
|
||||
paymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = when (paymentMethodType) {
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> arrayOf("2046", "2074")
|
||||
else -> arrayOf("currency_not_supported", "call_issuer")
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DeclineCodeErrorsDropdown(
|
||||
paymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = remember(paymentMethodType) {
|
||||
when (paymentMethodType) {
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> PayPalDeclineCode.KnownCode.values()
|
||||
else -> StripeDeclineCode.Code.values()
|
||||
}.map { it.name }.toTypedArray()
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FailureCodeErrorsDropdown(
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = remember {
|
||||
StripeFailureCode.Code.values().map { it.name }.toTypedArray()
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ValuesDropdown(values: Array<String>, onErrorCodeSelected: (String) -> Unit) {
|
||||
values.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = item) },
|
||||
onClick = {
|
||||
onErrorCodeSelected(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.LogDatabase
|
|||
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
|
||||
|
@ -491,6 +492,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Enqueue terminal donation"),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToTerminalDonationConfigurationFragment())
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("Release channel"))
|
||||
|
@ -757,7 +765,12 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
}
|
||||
|
||||
private fun enqueueSubscriptionRedemption() {
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(-1L, false).enqueue()
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(
|
||||
-1L,
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = 1000
|
||||
)
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionKeepAlive() {
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal
|
||||
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.donor.DonationErrorValueCodeSelector
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.donor.DonationErrorValueTypeSelector
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
/**
|
||||
* Configuration fragment for [TerminalDonationQueue.TerminalDonation]
|
||||
*/
|
||||
class InternalTerminalDonationConfigurationFragment : ComposeFragment() {
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
InternalTerminalDonationConfigurationContent(
|
||||
onAddClick = {
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(it)
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InternalTerminalDonationConfigurationContentPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
InternalTerminalDonationConfigurationContent(
|
||||
onAddClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InternalTerminalDonationConfigurationContent(
|
||||
onAddClick: (TerminalDonationQueue.TerminalDonation) -> Unit
|
||||
) {
|
||||
val terminalDonationState: MutableState<TerminalDonationQueue.TerminalDonation> = remember {
|
||||
mutableStateOf(
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = 1000L,
|
||||
isLongRunningPaymentMethod = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val paymentMethodType = remember(terminalDonationState.value.isLongRunningPaymentMethod) {
|
||||
if (terminalDonationState.value.isLongRunningPaymentMethod) PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT else PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = terminalDonationState.value.isLongRunningPaymentMethod,
|
||||
text = "Long-running payment method",
|
||||
onCheckChanged = {
|
||||
terminalDonationState.value = terminalDonationState.value.copy(isLongRunningPaymentMethod = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = terminalDonationState.value.error != null,
|
||||
text = "Enable error",
|
||||
onCheckChanged = {
|
||||
val error = if (it) {
|
||||
DonationErrorValue()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
terminalDonationState.value = terminalDonationState.value.copy(error = error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val error = terminalDonationState.value.error
|
||||
if (error != null) {
|
||||
item {
|
||||
DonationErrorValueTypeSelector(
|
||||
selectedPaymentMethodType = paymentMethodType,
|
||||
selectedErrorType = error.type,
|
||||
onErrorTypeSelected = {
|
||||
terminalDonationState.value = terminalDonationState.value.copy(
|
||||
error = error.copy(
|
||||
type = it,
|
||||
code = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
DonationErrorValueCodeSelector(
|
||||
selectedPaymentMethodType = paymentMethodType,
|
||||
selectedErrorType = error.type,
|
||||
selectedErrorCode = error.code,
|
||||
onErrorCodeSelected = {
|
||||
terminalDonationState.value = terminalDonationState.value.copy(
|
||||
error = error.copy(
|
||||
code = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onAddClick(terminalDonationState.value) },
|
||||
modifier = Modifier.defaultMinSize(minWidth = 220.dp)
|
||||
) {
|
||||
Text(text = "Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.donor
|
||||
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
|
||||
/**
|
||||
* Displays a dropdown widget for selecting an error type.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DonationErrorValueTypeSelector(
|
||||
selectedPaymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
selectedErrorType: DonationErrorValue.Type,
|
||||
onErrorTypeSelected: (DonationErrorValue.Type) -> Unit
|
||||
) {
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
value = selectedErrorType.name,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.menuAnchor()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DonationErrorValue.Type.values().filterNot {
|
||||
selectedPaymentMethodType == PendingOneTimeDonation.PaymentMethodType.PAYPAL && it == DonationErrorValue.Type.FAILURE_CODE
|
||||
}.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = item.name) },
|
||||
onClick = {
|
||||
onErrorTypeSelected(item)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dropdown widget for selecting an error code, if the corresponding type
|
||||
* allows for such things.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DonationErrorValueCodeSelector(
|
||||
selectedPaymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
selectedErrorType: DonationErrorValue.Type,
|
||||
selectedErrorCode: String,
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val isCodedError = remember(selectedErrorType) {
|
||||
selectedErrorType in setOf(DonationErrorValue.Type.PROCESSOR_CODE, DonationErrorValue.Type.DECLINE_CODE, DonationErrorValue.Type.FAILURE_CODE)
|
||||
}
|
||||
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (isCodedError) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
value = selectedErrorCode,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.menuAnchor()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
when (selectedErrorType) {
|
||||
DonationErrorValue.Type.PROCESSOR_CODE -> {
|
||||
ProcessorErrorsDropdown(selectedPaymentMethodType, onErrorCodeSelected)
|
||||
}
|
||||
|
||||
DonationErrorValue.Type.DECLINE_CODE -> {
|
||||
DeclineCodeErrorsDropdown(selectedPaymentMethodType, onErrorCodeSelected)
|
||||
}
|
||||
|
||||
DonationErrorValue.Type.FAILURE_CODE -> {
|
||||
FailureCodeErrorsDropdown(onErrorCodeSelected)
|
||||
}
|
||||
|
||||
else -> error("This should never happen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProcessorErrorsDropdown(
|
||||
paymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = when (paymentMethodType) {
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> arrayOf("2046", "2074")
|
||||
else -> arrayOf("currency_not_supported", "call_issuer")
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeclineCodeErrorsDropdown(
|
||||
paymentMethodType: PendingOneTimeDonation.PaymentMethodType,
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = remember(paymentMethodType) {
|
||||
when (paymentMethodType) {
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> PayPalDeclineCode.KnownCode.values()
|
||||
else -> StripeDeclineCode.Code.values()
|
||||
}.map { it.name }.toTypedArray()
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FailureCodeErrorsDropdown(
|
||||
onErrorCodeSelected: (String) -> Unit
|
||||
) {
|
||||
val values = remember {
|
||||
StripeFailureCode.Code.values().map { it.name }.toTypedArray()
|
||||
}
|
||||
|
||||
ValuesDropdown(values = values, onErrorCodeSelected = onErrorCodeSelected)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ValuesDropdown(values: Array<String>, onErrorCodeSelected: (String) -> Unit) {
|
||||
values.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = item) },
|
||||
onClick = {
|
||||
onErrorCodeSelected(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -30,7 +31,7 @@ fun BadgeImage112(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
Box(modifier = modifier.background(color = Color.Red))
|
||||
Box(modifier = modifier.background(color = Color.Black, shape = CircleShape))
|
||||
} else {
|
||||
AndroidView(
|
||||
factory = {
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.ga
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
|
@ -190,7 +191,12 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
|||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey, isLongRunning).enqueue { _, jobState ->
|
||||
val terminalDonation = TerminalDonationQueue.TerminalDonation(
|
||||
level = gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = isLongRunning
|
||||
)
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey, terminalDonation).enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
|
|||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||
|
@ -137,12 +138,17 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
|||
)
|
||||
)
|
||||
|
||||
val terminalDonation = TerminalDonationQueue.TerminalDonation(
|
||||
level = gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = isLongRunning
|
||||
)
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val chain = if (isBoost) {
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning)
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, gatewayRequest.uiSessionKey, terminalDonation)
|
||||
} else {
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, gatewayRequest.recipientId, gatewayRequest.additionalMessage, gatewayRequest.level, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning)
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, gatewayRequest.recipientId, gatewayRequest.additionalMessage, gatewayRequest.level, donationProcessor, gatewayRequest.uiSessionKey, terminalDonation)
|
||||
}
|
||||
|
||||
chain.enqueue { _, jobState ->
|
||||
|
|
|
@ -202,10 +202,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
* that we are successful and proceed as normal. If the payment didn't actually succeed, then we
|
||||
* expect an error later in the chain to inform us of this.
|
||||
*/
|
||||
fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor): Single<StatusAndPaymentMethodId> {
|
||||
fun getStatusAndPaymentMethodId(
|
||||
stripeIntentAccessor: StripeIntentAccessor,
|
||||
paymentMethodId: String?
|
||||
): Single<StatusAndPaymentMethodId> {
|
||||
return Single.fromCallable {
|
||||
when (stripeIntentAccessor.objectType) {
|
||||
StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(stripeIntentAccessor.intentId, StripeIntentStatus.SUCCEEDED, null)
|
||||
StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(stripeIntentAccessor.intentId, StripeIntentStatus.SUCCEEDED, paymentMethodId)
|
||||
StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> stripeApi.getPaymentIntent(stripeIntentAccessor).let {
|
||||
if (it.status == null) {
|
||||
Log.d(TAG, "Returned payment intent had a null status.", true)
|
||||
|
@ -230,7 +233,6 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
Log.d(TAG, "Setting default payment method via Signal service...")
|
||||
// TODO [sepa] -- iDEAL has its own call
|
||||
Single.fromCallable {
|
||||
if (paymentSourceType == PaymentSourceType.Stripe.IDEAL) {
|
||||
ApplicationDependencies
|
||||
|
|
|
@ -6,23 +6,29 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.completed
|
||||
|
||||
import android.content.DialogInterface
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -35,26 +41,27 @@ import org.signal.core.ui.theme.SignalTheme
|
|||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Bottom Sheet displayed when the app notices that a long-running donation has
|
||||
* completed.
|
||||
*/
|
||||
class DonationCompletedBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
class TerminalDonationBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_DONATION_COMPLETED = "arg.donation.completed"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, donationCompleted: DonationCompletedQueue.DonationCompleted) {
|
||||
DonationCompletedBottomSheet().apply {
|
||||
fun show(fragmentManager: FragmentManager, terminalDonation: TerminalDonationQueue.TerminalDonation) {
|
||||
TerminalDonationBottomSheet().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_DONATION_COMPLETED to donationCompleted.encode()
|
||||
ARG_DONATION_COMPLETED to terminalDonation.encode()
|
||||
)
|
||||
|
||||
show(fragmentManager, null)
|
||||
|
@ -62,16 +69,42 @@ class DonationCompletedBottomSheet : ComposeBottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private val donationCompleted: DonationCompletedQueue.DonationCompleted by lazy(LazyThreadSafetyMode.NONE) {
|
||||
DonationCompletedQueue.DonationCompleted.ADAPTER.decode(requireArguments().getByteArray(ARG_DONATION_COMPLETED)!!)
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
private val terminalDonation: TerminalDonationQueue.TerminalDonation by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TerminalDonationQueue.TerminalDonation.ADAPTER.decode(requireArguments().getByteArray(ARG_DONATION_COMPLETED)!!)
|
||||
}
|
||||
|
||||
private val viewModel: DonationCompletedViewModel by viewModel {
|
||||
DonationCompletedViewModel(donationCompleted, badgeRepository = BadgeRepository(requireContext()))
|
||||
private val viewModel: TerminalDonationViewModel by viewModel {
|
||||
TerminalDonationViewModel(terminalDonation, badgeRepository = BadgeRepository(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
if (terminalDonation.error != null) {
|
||||
PaymentFailureBottomSheet()
|
||||
} else {
|
||||
CompletedSheet()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentFailureBottomSheet() {
|
||||
val badge by viewModel.badge
|
||||
|
||||
DonationPaymentFailureBottomSheet(
|
||||
badge = badge,
|
||||
onTryAgainClick = {
|
||||
startActivity(AppSettingsActivity.manageSubscriptions(requireContext()))
|
||||
},
|
||||
onNotNowClick = {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompletedSheet() {
|
||||
val badge by viewModel.badge
|
||||
val isToggleChecked by viewModel.isToggleChecked
|
||||
val toggleType by viewModel.toggleType
|
||||
|
@ -92,6 +125,103 @@ class DonationCompletedBottomSheet : ComposeBottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DonationPaymentFailureBottomSheet() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
DonationPaymentFailureBottomSheet(
|
||||
badge = null,
|
||||
onTryAgainClick = {},
|
||||
onNotNowClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DonationPaymentFailureBottomSheet(
|
||||
badge: Badge?,
|
||||
onTryAgainClick: () -> Unit,
|
||||
onNotNowClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 21.dp, bottom = 16.dp)
|
||||
) {
|
||||
BadgeImage112(
|
||||
badge = badge,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.padding(2.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
shape = CircleShape
|
||||
)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_error_circle_fill_24),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationErrorBottomSheet__donation_couldnt_be_processed),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 45.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationErrorBottomSheet__were_having_trouble),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, bottom = 24.dp)
|
||||
.padding(horizontal = 45.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onTryAgainClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(top = 32.dp, bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationErrorBottomSheet__try_again)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNotNowClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = 56.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.DonationErrorBottomSheet__not_now)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DonationCompletedSheetContentPreview() {
|
||||
|
@ -100,7 +230,7 @@ private fun DonationCompletedSheetContentPreview() {
|
|||
DonationCompletedSheetContent(
|
||||
badge = null,
|
||||
isToggleChecked = false,
|
||||
toggleType = DonationCompletedViewModel.ToggleType.NONE,
|
||||
toggleType = TerminalDonationViewModel.ToggleType.NONE,
|
||||
onCheckChanged = {},
|
||||
onDoneClick = {}
|
||||
)
|
||||
|
@ -112,7 +242,7 @@ private fun DonationCompletedSheetContentPreview() {
|
|||
private fun DonationCompletedSheetContent(
|
||||
badge: Badge?,
|
||||
isToggleChecked: Boolean,
|
||||
toggleType: DonationCompletedViewModel.ToggleType,
|
||||
toggleType: TerminalDonationViewModel.ToggleType,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
onDoneClick: () -> Unit
|
||||
) {
|
||||
|
@ -147,7 +277,7 @@ private fun DonationCompletedSheetContent(
|
|||
.padding(horizontal = 45.dp)
|
||||
)
|
||||
|
||||
if (toggleType == DonationCompletedViewModel.ToggleType.NONE) {
|
||||
if (toggleType == TerminalDonationViewModel.ToggleType.NONE) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
DonationToggleRow(
|
|
@ -12,13 +12,14 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
/**
|
||||
* Handles displaying the "Thank You" or "Donation completed" sheet when the user navigates to an appropriate screen.
|
||||
* These sheets are one-shot.
|
||||
*/
|
||||
class DonationCompletedDelegate(
|
||||
class TerminalDonationDelegate(
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val lifecycleOwner: LifecycleOwner
|
||||
) : DefaultLifecycleObserver {
|
||||
|
@ -27,13 +28,13 @@ class DonationCompletedDelegate(
|
|||
bindTo(lifecycleOwner)
|
||||
}
|
||||
|
||||
private val badgeRepository = DonationCompletedRepository()
|
||||
private val badgeRepository = TerminalDonationRepository()
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
val donations = SignalStore.donationsValues().consumeDonationCompletionList()
|
||||
val donations = SignalStore.donationsValues().consumeTerminalDonations()
|
||||
for (donation in donations) {
|
||||
if (donation.isLongRunningPaymentMethod) {
|
||||
DonationCompletedBottomSheet.show(fragmentManager, donation)
|
||||
if (donation.isLongRunningPaymentMethod && (donation.error == null || donation.error.type != DonationErrorValue.Type.REDEMPTION)) {
|
||||
TerminalDonationBottomSheet.show(fragmentManager, donation)
|
||||
} else {
|
||||
lifecycleDisposable += badgeRepository.getBadge(donation).observeOn(AndroidSchedulers.mainThread()).subscribe { badge ->
|
||||
val args = ThanksForYourSupportBottomSheetDialogFragmentArgs.Builder(badge).build().toBundle()
|
|
@ -9,19 +9,19 @@ import io.reactivex.rxjava3.core.Single
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import java.util.Locale
|
||||
|
||||
class DonationCompletedRepository(
|
||||
class TerminalDonationRepository(
|
||||
private val donationsService: DonationsService = ApplicationDependencies.getDonationsService()
|
||||
) {
|
||||
fun getBadge(donationCompleted: DonationCompletedQueue.DonationCompleted): Single<Badge> {
|
||||
fun getBadge(terminalDonation: TerminalDonationQueue.TerminalDonation): Single<Badge> {
|
||||
return Single
|
||||
.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.levels[donationCompleted.level.toInt()]!! }
|
||||
.map { it.levels[terminalDonation.level.toInt()]!! }
|
||||
.map { Badges.fromServiceBadge(it.badge) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
|
@ -18,18 +18,18 @@ import org.signal.core.util.logging.Log
|
|||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class DonationCompletedViewModel(
|
||||
donationCompleted: DonationCompletedQueue.DonationCompleted,
|
||||
repository: DonationCompletedRepository = DonationCompletedRepository(),
|
||||
class TerminalDonationViewModel(
|
||||
donationCompleted: TerminalDonationQueue.TerminalDonation,
|
||||
repository: TerminalDonationRepository = TerminalDonationRepository(),
|
||||
private val badgeRepository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonationCompletedViewModel::class.java)
|
||||
private val TAG = Log.tag(TerminalDonationViewModel::class.java)
|
||||
}
|
||||
|
||||
private val disposables = CompositeDisposable()
|
|
@ -134,7 +134,7 @@ class DonationCheckoutDelegate(
|
|||
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
|
||||
Snackbar.make(fragment.requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
SignalStore.donationsValues().removeDonationComplete(result.request.level)
|
||||
SignalStore.donationsValues().removeTerminalDonation(result.request.level)
|
||||
callback.onPaymentComplete(result.request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,7 +165,7 @@ class StripePaymentInProgressViewModel(
|
|||
paymentSourceProvider.paymentSourceType.code
|
||||
)
|
||||
)
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult) }
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, secure3DSAction.paymentMethodId) }
|
||||
}
|
||||
.flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, paymentSourceProvider.paymentSourceType) }
|
||||
.onErrorResumeNext {
|
||||
|
@ -214,17 +214,18 @@ class StripePaymentInProgressViewModel(
|
|||
|
||||
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
|
||||
stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId)
|
||||
.flatMap {
|
||||
nextActionHandler.handle(
|
||||
it,
|
||||
Stripe3DSData(
|
||||
it.stripeIntentAccessor,
|
||||
request,
|
||||
paymentSourceProvider.paymentSourceType.code
|
||||
.flatMap { action ->
|
||||
nextActionHandler
|
||||
.handle(
|
||||
action,
|
||||
Stripe3DSData(
|
||||
action.stripeIntentAccessor,
|
||||
request,
|
||||
paymentSourceProvider.paymentSourceType.code
|
||||
)
|
||||
)
|
||||
)
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it, action.paymentMethodId) }
|
||||
}
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
|
||||
.flatMapCompletable {
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
gatewayRequest = request,
|
||||
|
|
|
@ -226,7 +226,7 @@ private fun BankTransferDetailsContent(
|
|||
val fullString = stringResource(id = R.string.BankTransferDetailsFragment__enter_your_bank_details, learnMore)
|
||||
|
||||
Texts.LinkifiedText(
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [sepa] -- final URL
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_faq_url)),
|
||||
onUrlClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
|
|
|
@ -204,7 +204,7 @@ private fun IdealTransferDetailsContent(
|
|||
val fullString = stringResource(id = R.string.IdealTransferDetailsFragment__enter_your_bank, learnMore)
|
||||
|
||||
Texts.LinkifiedText(
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [sepa] -- final URL
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_faq_url)),
|
||||
onUrlClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
|
@ -257,7 +257,11 @@ private fun IdealTransferDetailsContent(
|
|||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onDonateClick() }
|
||||
onDone = {
|
||||
if (state.canProceed()) {
|
||||
onDonateClick()
|
||||
}
|
||||
}
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.signal.donations.StripeDeclineCode
|
|||
import org.signal.donations.StripeError
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
|
||||
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
|
||||
|
||||
|
@ -147,7 +148,44 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
|||
}
|
||||
|
||||
@JvmStatic
|
||||
fun routeBackgroundError(context: Context, uiSessionKey: Long, error: DonationError) {
|
||||
fun DonationError.toDonationErrorValue(): DonationErrorValue {
|
||||
return when (this) {
|
||||
is PaymentSetupError.GenericError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.PAYMENT,
|
||||
code = ""
|
||||
)
|
||||
is PaymentSetupError.StripeCodedError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.PROCESSOR_CODE,
|
||||
code = this.errorCode
|
||||
)
|
||||
is PaymentSetupError.StripeDeclinedError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.DECLINE_CODE,
|
||||
code = this.declineCode.rawCode
|
||||
)
|
||||
is PaymentSetupError.StripeFailureCodeError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.FAILURE_CODE,
|
||||
code = this.failureCode.rawCode
|
||||
)
|
||||
is PaymentSetupError.PayPalCodedError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.PROCESSOR_CODE,
|
||||
code = this.errorCode.toString()
|
||||
)
|
||||
is PaymentSetupError.PayPalDeclinedError -> DonationErrorValue(
|
||||
type = DonationErrorValue.Type.DECLINE_CODE,
|
||||
code = this.code.code.toString()
|
||||
)
|
||||
else -> error("Don't know how to convert error $this")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun routeBackgroundError(
|
||||
context: Context,
|
||||
uiSessionKey: Long,
|
||||
error: DonationError,
|
||||
suppressNotification: Boolean = true
|
||||
) {
|
||||
if (error.source == DonationErrorSource.GIFT_REDEMPTION) {
|
||||
routeDonationError(context, error)
|
||||
return
|
||||
|
@ -159,6 +197,9 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
|||
Log.i(TAG, "Routing background donation error to uiSessionKey $uiSessionKey dialog", error)
|
||||
subject.onNext(error)
|
||||
}
|
||||
suppressNotification -> {
|
||||
Log.i(TAG, "Suppressing notification for error.", error)
|
||||
}
|
||||
else -> {
|
||||
Log.i(TAG, "Routing background donation error to uiSessionKey $uiSessionKey notification", error)
|
||||
DonationErrorNotifications.displayErrorNotification(context, error)
|
||||
|
|
|
@ -21,12 +21,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
|||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.DonationCompletedDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
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.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -68,7 +68,7 @@ class ManageDonationsFragment :
|
|||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewLifecycleOwner.lifecycle.addObserver(DonationCompletedDelegate(childFragmentManager, viewLifecycleOwner))
|
||||
viewLifecycleOwner.lifecycle.addObserver(TerminalDonationDelegate(childFragmentManager, viewLifecycleOwner))
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
|
@ -344,14 +344,38 @@ class ManageDonationsFragment :
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun displayPendingOneTimeDonationErrorDialog(error: PendingOneTimeDonation.Error) {
|
||||
// TODO [sepa] -- actual dialog text?
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__error_processing_payment)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(null)
|
||||
private fun displayPendingOneTimeDonationErrorDialog(error: DonationErrorValue) {
|
||||
when (error.type) {
|
||||
DonationErrorValue.Type.REDEMPTION -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__couldnt_add_badge)
|
||||
.setMessage(R.string.DonationsErrors__your_badge_could_not)
|
||||
.setNegativeButton(R.string.DonationsErrors__learn_more) { _, _ ->
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
.setPositiveButton(R.string.Subscription__contact_support) { _, _ ->
|
||||
requireActivity().finish()
|
||||
startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
}
|
||||
.setOnDismissListener {
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(null)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
.show()
|
||||
else -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__error_processing_payment)
|
||||
.setMessage(R.string.DonationsErrors__try_another_payment_method)
|
||||
.setNegativeButton(R.string.DonationsErrors__learn_more) { _, _ ->
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setOnDismissListener {
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(null)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMakeAMonthlyDonation() {
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.R
|
|||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.databinding.MySupportPreferenceBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
|
@ -33,7 +34,7 @@ object OneTimeDonationPreference {
|
|||
class Model(
|
||||
val pendingOneTimeDonation: PendingOneTimeDonation,
|
||||
val onPendingClick: (FiatMoney) -> Unit,
|
||||
val onErrorClick: (PendingOneTimeDonation.Error) -> Unit
|
||||
val onErrorClick: (DonationErrorValue) -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
|
@ -63,7 +64,7 @@ object OneTimeDonationPreference {
|
|||
}
|
||||
}
|
||||
|
||||
private fun presentErrorState(model: Model, error: PendingOneTimeDonation.Error) {
|
||||
private fun presentErrorState(model: Model, error: DonationErrorValue) {
|
||||
expiry.text = getErrorSubtitle(error)
|
||||
|
||||
itemView.setOnClickListener { model.onErrorClick(error) }
|
||||
|
@ -81,9 +82,9 @@ object OneTimeDonationPreference {
|
|||
progress.visible = model.pendingOneTimeDonation.paymentMethodType != PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT
|
||||
}
|
||||
|
||||
private fun getErrorSubtitle(error: PendingOneTimeDonation.Error): String {
|
||||
private fun getErrorSubtitle(error: DonationErrorValue): String {
|
||||
return when (error.type) {
|
||||
PendingOneTimeDonation.Error.Type.REDEMPTION -> context.getString(R.string.DonationsErrors__couldnt_add_badge)
|
||||
DonationErrorValue.Type.REDEMPTION -> context.getString(R.string.DonationsErrors__couldnt_add_badge)
|
||||
else -> context.getString(R.string.DonationsErrors__donation_failed)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,8 +111,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
|||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UsernameOutOfSyncReminder;
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.DonationCompletedBottomSheet;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.DonationCompletedDelegate;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
|
@ -135,7 +134,6 @@ import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs;
|
||||
|
@ -279,7 +277,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(new DonationCompletedDelegate(getParentFragmentManager(), getViewLifecycleOwner()));
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(new TerminalDonationDelegate(getParentFragmentManager(), getViewLifecycleOwner()));
|
||||
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
|
|
@ -19,8 +19,8 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
|
|||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError;
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
|
@ -38,6 +38,8 @@ import java.io.IOException;
|
|||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Job responsible for submitting ReceiptCredentialRequest objects to the server until
|
||||
* we get a response.
|
||||
|
@ -58,16 +60,17 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
private static final String DATA_BADGE_LEVEL = "data.badge.level";
|
||||
private static final String DATA_DONATION_PROCESSOR = "data.donation.processor";
|
||||
private static final String DATA_UI_SESSION_KEY = "data.ui.session.key";
|
||||
private static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
private static final String DATA_TERMINAL_DONATION = "data.terminal.donation";
|
||||
|
||||
private ReceiptCredentialRequestContext requestContext;
|
||||
private TerminalDonationQueue.TerminalDonation terminalDonation;
|
||||
|
||||
private ReceiptCredentialRequestContext requestContext;
|
||||
|
||||
private final DonationErrorSource donationErrorSource;
|
||||
private final String paymentIntentId;
|
||||
private final long badgeLevel;
|
||||
private final DonationProcessor donationProcessor;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
private final long uiSessionKey;
|
||||
|
||||
private static String resolveQueue(DonationErrorSource donationErrorSource, boolean isLongRunning) {
|
||||
String baseQueue = donationErrorSource == DonationErrorSource.ONE_TIME ? BOOST_QUEUE : GIFT_QUEUE;
|
||||
|
@ -78,13 +81,19 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
return isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1);
|
||||
}
|
||||
|
||||
private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor, long uiSessionKey, boolean isLongRunning) {
|
||||
private static BoostReceiptRequestResponseJob createJob(@NonNull String paymentIntentId,
|
||||
@NonNull DonationErrorSource donationErrorSource,
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey,
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
return new BoostReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(resolveQueue(donationErrorSource, isLongRunning))
|
||||
.setLifespan(resolveLifespan(isLongRunning))
|
||||
.setQueue(resolveQueue(donationErrorSource, terminalDonation.isLongRunningPaymentMethod))
|
||||
.setLifespan(resolveLifespan(terminalDonation.isLongRunningPaymentMethod))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
null,
|
||||
|
@ -93,17 +102,17 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
badgeLevel,
|
||||
donationProcessor,
|
||||
uiSessionKey,
|
||||
isLongRunning
|
||||
terminalDonation
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.ONE_TIME, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, isLongRunning);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey, isLongRunning);
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.ONE_TIME, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, terminalDonation);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey, terminalDonation.isLongRunningPaymentMethod);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
||||
|
@ -120,9 +129,9 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey, isLongRunning);
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey, terminalDonation);
|
||||
GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage);
|
||||
|
||||
|
||||
|
@ -138,16 +147,16 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunningDonationPaymentType)
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
super(parameters);
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.donationErrorSource = donationErrorSource;
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunningDonationPaymentType;
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.donationErrorSource = donationErrorSource;
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.terminalDonation = terminalDonation;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -157,7 +166,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
.putLong(DATA_BADGE_LEVEL, badgeLevel)
|
||||
.putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode())
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType);
|
||||
.putBlobAsString(DATA_TERMINAL_DONATION, terminalDonation.encode());
|
||||
|
||||
if (requestContext != null) {
|
||||
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
|
||||
|
@ -173,11 +182,16 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
if (terminalDonation.error != null) {
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(terminalDonation);
|
||||
} else {
|
||||
Log.w(TAG, "Job is in terminal state without an error on TerminalDonation.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (isLongRunningDonationPaymentType) {
|
||||
if (terminalDonation.isLongRunningPaymentMethod) {
|
||||
return TimeUnit.DAYS.toMillis(1);
|
||||
} else {
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
|
@ -221,6 +235,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
ReceiptCredentialPresentation receiptCredentialPresentation = getReceiptCredentialPresentation(receiptCredential);
|
||||
setOutputData(new JsonJobData.Builder().putBlobAsString(DonationReceiptRedemptionJob.INPUT_RECEIPT_CREDENTIAL_PRESENTATION,
|
||||
receiptCredentialPresentation.serialize())
|
||||
.putBlobAsString(DonationReceiptRedemptionJob.INPUT_TERMINAL_DONATION, terminalDonation.encode())
|
||||
.serialize());
|
||||
|
||||
enqueueDonationComplete();
|
||||
|
@ -242,48 +257,60 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
* Sets the pending one-time donation error according to the status code.
|
||||
*/
|
||||
private void setPendingOneTimeDonationGenericRedemptionError(int statusCode) {
|
||||
DonationErrorValue donationErrorValue = new DonationErrorValue.Builder()
|
||||
.type(statusCode == 402
|
||||
? DonationErrorValue.Type.PAYMENT
|
||||
: DonationErrorValue.Type.REDEMPTION)
|
||||
.code(Integer.toString(statusCode))
|
||||
.build();
|
||||
|
||||
SignalStore.donationsValues().setPendingOneTimeDonationError(
|
||||
new PendingOneTimeDonation.Error.Builder()
|
||||
.type(statusCode == 402
|
||||
? PendingOneTimeDonation.Error.Type.PAYMENT
|
||||
: PendingOneTimeDonation.Error.Type.REDEMPTION)
|
||||
.code(Integer.toString(statusCode))
|
||||
.build()
|
||||
donationErrorValue
|
||||
);
|
||||
|
||||
terminalDonation = terminalDonation.newBuilder()
|
||||
.error(donationErrorValue)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pending one-time donation error according to the given charge failure.
|
||||
*/
|
||||
private void setPendingOneTimeDonationChargeFailureError(@NonNull ActiveSubscription.ChargeFailure chargeFailure) {
|
||||
final PendingOneTimeDonation.Error.Type type;
|
||||
final String code;
|
||||
final DonationErrorValue.Type type;
|
||||
final String code;
|
||||
|
||||
if (donationProcessor == DonationProcessor.PAYPAL) {
|
||||
code = chargeFailure.getCode();
|
||||
type = PendingOneTimeDonation.Error.Type.PROCESSOR_CODE;
|
||||
type = DonationErrorValue.Type.PROCESSOR_CODE;
|
||||
} else {
|
||||
StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason());
|
||||
StripeFailureCode failureCode = StripeFailureCode.Companion.getFromCode(chargeFailure.getCode());
|
||||
|
||||
if (failureCode.isKnown()) {
|
||||
code = failureCode.toString();
|
||||
type = PendingOneTimeDonation.Error.Type.FAILURE_CODE;
|
||||
type = DonationErrorValue.Type.FAILURE_CODE;
|
||||
} else if (declineCode.isKnown()) {
|
||||
code = declineCode.toString();
|
||||
type = PendingOneTimeDonation.Error.Type.DECLINE_CODE;
|
||||
type = DonationErrorValue.Type.DECLINE_CODE;
|
||||
} else {
|
||||
code = chargeFailure.getCode();
|
||||
type = PendingOneTimeDonation.Error.Type.PROCESSOR_CODE;
|
||||
type = DonationErrorValue.Type.PROCESSOR_CODE;
|
||||
}
|
||||
}
|
||||
|
||||
DonationErrorValue donationErrorValue = new DonationErrorValue.Builder()
|
||||
.type(type)
|
||||
.code(code)
|
||||
.build();
|
||||
|
||||
SignalStore.donationsValues().setPendingOneTimeDonationError(
|
||||
new PendingOneTimeDonation.Error.Builder()
|
||||
.type(type)
|
||||
.code(code)
|
||||
.build()
|
||||
donationErrorValue
|
||||
);
|
||||
|
||||
terminalDonation = terminalDonation.newBuilder()
|
||||
.error(donationErrorValue)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void handleApplicationError(Context context, ServiceResponse<ReceiptCredentialResponse> response, @NonNull DonationErrorSource donationErrorSource) throws Exception {
|
||||
|
@ -299,7 +326,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
throw new Exception(applicationException);
|
||||
case 402:
|
||||
Log.w(TAG, "User payment failed.", applicationException, true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericPaymentFailure(donationErrorSource));
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericPaymentFailure(donationErrorSource), terminalDonation.isLongRunningPaymentMethod);
|
||||
|
||||
if (applicationException instanceof DonationReceiptCredentialError) {
|
||||
setPendingOneTimeDonationChargeFailureError(((DonationReceiptCredentialError) applicationException).getChargeFailure());
|
||||
|
@ -379,22 +406,40 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
JsonJobData data = JsonJobData.deserialize(serializedData);
|
||||
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.ONE_TIME.serialize()));
|
||||
long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL));
|
||||
String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode());
|
||||
DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor);
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.ONE_TIME.serialize()));
|
||||
long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL));
|
||||
String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode());
|
||||
DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor);
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
byte[] rawTerminalDonation = data.getStringAsBlob(DATA_TERMINAL_DONATION);
|
||||
|
||||
TerminalDonationQueue.TerminalDonation terminalDonation = null;
|
||||
if (rawTerminalDonation != null) {
|
||||
try {
|
||||
terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to parse terminal donation. Generating a default.");
|
||||
}
|
||||
}
|
||||
|
||||
if (terminalDonation == null) {
|
||||
terminalDonation = new TerminalDonationQueue.TerminalDonation(
|
||||
-1,
|
||||
false,
|
||||
null,
|
||||
ByteString.EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (data.hasString(DATA_REQUEST_BYTES)) {
|
||||
byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES);
|
||||
ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob);
|
||||
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, terminalDonation);
|
||||
} else {
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, terminalDonation);
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
|
@ -12,9 +12,9 @@ import org.thoughtcrime.securesms.database.MessageTable;
|
|||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
|
@ -30,6 +30,8 @@ import java.util.Collections;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Job to redeem a verified donation receipt. It is up to the Job prior in the chain to specify a valid
|
||||
* presentation object via setOutputData. This is expected to be the byte[] blob of a ReceiptCredentialPresentation object.
|
||||
|
@ -45,18 +47,19 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
private static final String LONG_RUNNING_QUEUE_SUFFIX = "__LONG_RUNNING";
|
||||
|
||||
public static final String INPUT_RECEIPT_CREDENTIAL_PRESENTATION = "data.receipt.credential.presentation";
|
||||
public static final String INPUT_TERMINAL_DONATION = "data.terminal.donation";
|
||||
public static final String INPUT_KEEP_ALIVE_409 = "data.keep.alive.409";
|
||||
public static final String DATA_ERROR_SOURCE = "data.error.source";
|
||||
public static final String DATA_GIFT_MESSAGE_ID = "data.gift.message.id";
|
||||
public static final String DATA_PRIMARY = "data.primary";
|
||||
public static final String DATA_UI_SESSION_KEY = "data.ui.session.key";
|
||||
public static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
|
||||
private final long giftMessageId;
|
||||
private final boolean makePrimary;
|
||||
private final DonationErrorSource errorSource;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
private final long uiSessionKey;
|
||||
|
||||
private TerminalDonationQueue.TerminalDonation terminalDonation;
|
||||
|
||||
public static DonationReceiptRedemptionJob createJobForSubscription(@NonNull DonationErrorSource errorSource, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return new DonationReceiptRedemptionJob(
|
||||
|
@ -64,7 +67,6 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
false,
|
||||
errorSource,
|
||||
uiSessionKey,
|
||||
isLongRunningDonationPaymentType,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
|
@ -81,7 +83,6 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
false,
|
||||
DonationErrorSource.ONE_TIME,
|
||||
uiSessionKey,
|
||||
isLongRunningDonationPaymentType,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
|
@ -108,7 +109,6 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
primary,
|
||||
DonationErrorSource.GIFT_REDEMPTION,
|
||||
-1L,
|
||||
false,
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
|
@ -126,13 +126,12 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
.then(multiDeviceProfileContentUpdateJob);
|
||||
}
|
||||
|
||||
private DonationReceiptRedemptionJob(long giftMessageId, boolean primary, @NonNull DonationErrorSource errorSource, long uiSessionKey, boolean isLongRunning, @NonNull Job.Parameters parameters) {
|
||||
private DonationReceiptRedemptionJob(long giftMessageId, boolean primary, @NonNull DonationErrorSource errorSource, long uiSessionKey, @NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
this.giftMessageId = giftMessageId;
|
||||
this.makePrimary = primary;
|
||||
this.errorSource = errorSource;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -142,7 +141,6 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
.putLong(DATA_GIFT_MESSAGE_ID, giftMessageId)
|
||||
.putBoolean(DATA_PRIMARY, makePrimary)
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
|
@ -160,6 +158,10 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
} else if (giftMessageId != NO_ID) {
|
||||
SignalDatabase.messages().markGiftRedemptionFailed(giftMessageId);
|
||||
}
|
||||
|
||||
if (terminalDonation != null) {
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(terminalDonation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -181,7 +183,9 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
}
|
||||
|
||||
private void doRun() throws Exception {
|
||||
boolean isKeepAlive409 = getInputData() != null && JsonJobData.deserialize(getInputData()).getBooleanOrDefault(INPUT_KEEP_ALIVE_409, false);
|
||||
JsonJobData inputData = getInputData() != null ? JsonJobData.deserialize(getInputData()) : null;
|
||||
boolean isKeepAlive409 = inputData != null && inputData.getBooleanOrDefault(INPUT_KEEP_ALIVE_409, false);
|
||||
|
||||
if (isKeepAlive409) {
|
||||
Log.d(TAG, "Keep-Alive redemption job hit a 409. Exiting.", true);
|
||||
return;
|
||||
|
@ -193,6 +197,17 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
byte[] rawTerminalDonation = inputData != null ? inputData.getStringAsBlob(INPUT_TERMINAL_DONATION) : null;
|
||||
if (rawTerminalDonation != null) {
|
||||
Log.d(TAG, "Retrieved terminal donation information from input data.");
|
||||
terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation);
|
||||
} else {
|
||||
Log.d(TAG, "Input data does not contain terminal donation data. Creating one with sane defaults.");
|
||||
terminalDonation = new TerminalDonationQueue.TerminalDonation.Builder()
|
||||
.level(presentation.getReceiptLevel())
|
||||
.build();
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempting to redeem token... isForSubscription: " + isForSubscription(), true);
|
||||
ServiceResponse<EmptyResponse> response = ApplicationDependencies.getDonationsService()
|
||||
.redeemReceipt(presentation,
|
||||
|
@ -208,12 +223,18 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(errorSource));
|
||||
|
||||
if (isForOneTimeDonation()) {
|
||||
DonationErrorValue donationErrorValue = new DonationErrorValue.Builder()
|
||||
.type(DonationErrorValue.Type.REDEMPTION)
|
||||
.code(Integer.toString(response.getStatus()))
|
||||
.build();
|
||||
|
||||
SignalStore.donationsValues().setPendingOneTimeDonationError(
|
||||
new PendingOneTimeDonation.Error.Builder()
|
||||
.type(PendingOneTimeDonation.Error.Type.REDEMPTION)
|
||||
.code(Integer.toString(response.getStatus()))
|
||||
.build()
|
||||
donationErrorValue
|
||||
);
|
||||
|
||||
terminalDonation = terminalDonation.newBuilder()
|
||||
.error(donationErrorValue)
|
||||
.build();
|
||||
}
|
||||
|
||||
throw new IOException(response.getApplicationError().get());
|
||||
|
@ -224,7 +245,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
}
|
||||
|
||||
Log.i(TAG, "Successfully redeemed token with response code " + response.getStatus() + "... isForSubscription: " + isForSubscription(), true);
|
||||
enqueueDonationComplete(presentation.getReceiptLevel());
|
||||
enqueueDonationComplete();
|
||||
|
||||
if (isForSubscription()) {
|
||||
Log.d(TAG, "Clearing subscription failure", true);
|
||||
|
@ -310,7 +331,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
return Objects.requireNonNull(getParameters().getQueue()).startsWith(ONE_TIME_QUEUE) && giftMessageId == NO_ID;
|
||||
}
|
||||
|
||||
private void enqueueDonationComplete(long receiptLevel) {
|
||||
private void enqueueDonationComplete() {
|
||||
if (errorSource == DonationErrorSource.GIFT || errorSource == DonationErrorSource.GIFT_REDEMPTION) {
|
||||
Log.i(TAG, "Skipping donation complete sheet for GIFT related redemption.");
|
||||
return;
|
||||
|
@ -321,12 +342,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
SignalStore.donationsValues().appendToDonationCompletionList(
|
||||
new DonationCompletedQueue.DonationCompleted.Builder()
|
||||
.isLongRunningPaymentMethod(isLongRunningDonationPaymentType)
|
||||
.level(receiptLevel)
|
||||
.build()
|
||||
);
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(terminalDonation);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -347,9 +363,8 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
|||
boolean primary = data.getBooleanOrDefault(DATA_PRIMARY, false);
|
||||
DonationErrorSource errorSource = DonationErrorSource.deserialize(serializedErrorSource);
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
return new DonationReceiptRedemptionJob(messageId, primary, errorSource, uiSessionKey, isLongRunningDonationPaymentType, parameters);
|
||||
return new DonationReceiptRedemptionJob(messageId, primary, errorSource, uiSessionKey, parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
|
@ -27,7 +29,6 @@ import org.thoughtcrime.securesms.subscription.LevelUpdate
|
|||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
|
@ -53,12 +54,20 @@ class ExternalLaunchDonationJob private constructor(
|
|||
stripe3DSData.stripeIntentAccessor.intentId,
|
||||
DonationProcessor.STRIPE,
|
||||
-1L,
|
||||
stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.SEPADebit
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = stripe3DSData.gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.SEPADebit
|
||||
)
|
||||
)
|
||||
|
||||
DonateToSignalType.MONTHLY -> SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(
|
||||
-1L,
|
||||
stripe3DSData.paymentSourceType.isBankTransfer
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = stripe3DSData.gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = stripe3DSData.paymentSourceType.isBankTransfer
|
||||
)
|
||||
)
|
||||
|
||||
DonateToSignalType.GIFT -> BoostReceiptRequestResponseJob.createJobChainForGift(
|
||||
stripe3DSData.stripeIntentAccessor.intentId,
|
||||
stripe3DSData.gatewayRequest.recipientId,
|
||||
|
@ -66,7 +75,10 @@ class ExternalLaunchDonationJob private constructor(
|
|||
stripe3DSData.gatewayRequest.level,
|
||||
DonationProcessor.STRIPE,
|
||||
-1L,
|
||||
false
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = stripe3DSData.gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.SEPADebit
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -100,6 +112,7 @@ class ExternalLaunchDonationJob private constructor(
|
|||
Log.w(TAG, "NONE type does not require confirmation. Failing Permanently.")
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> runForPaymentIntent()
|
||||
StripeIntentAccessor.ObjectType.SETUP_INTENT -> runForSetupIntent()
|
||||
}
|
||||
|
@ -189,10 +202,12 @@ class ExternalLaunchDonationJob private constructor(
|
|||
null, StripeIntentStatus.SUCCEEDED -> {
|
||||
Log.i(TAG, "Stripe Intent is in the SUCCEEDED state, we can proceed.", true)
|
||||
}
|
||||
|
||||
StripeIntentStatus.CANCELED -> {
|
||||
Log.i(TAG, "Stripe Intent is cancelled, we cannot proceed.", true)
|
||||
throw Exception("User cancelled payment.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.i(TAG, "Stripe Intent is still processing, retry later.", true)
|
||||
throw RetryException()
|
||||
|
@ -209,20 +224,33 @@ class ExternalLaunchDonationJob private constructor(
|
|||
} else if (serviceResponse.applicationError.isPresent) {
|
||||
Log.w(TAG, "An application error was present. ${serviceResponse.status}", serviceResponse.applicationError.get(), true)
|
||||
doOnApplicationError()
|
||||
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = stripe3DSData.gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = stripe3DSData.gatewayRequest.donateToSignalType == DonateToSignalType.MONTHLY && stripe3DSData.paymentSourceType.isBankTransfer ||
|
||||
stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.SEPADebit,
|
||||
error = DonationErrorValue(
|
||||
DonationErrorValue.Type.PAYMENT,
|
||||
code = serviceResponse.status.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
throw serviceResponse.applicationError.get()
|
||||
} else if (serviceResponse.executionError.isPresent) {
|
||||
Log.w(TAG, "An execution error was present. ${serviceResponse.status}", serviceResponse.executionError.get(), true)
|
||||
throw serviceResponse.executionError.get()
|
||||
throw RetryException(serviceResponse.executionError.get())
|
||||
}
|
||||
|
||||
error("Should never get here.")
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean {
|
||||
return e is RetryException || e is IOException
|
||||
return e is RetryException
|
||||
}
|
||||
|
||||
class RetryException : Exception()
|
||||
class RetryException(cause: Throwable? = null) : Exception(cause)
|
||||
|
||||
class Factory : Job.Factory<ExternalLaunchDonationJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): ExternalLaunchDonationJob {
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
@ -19,6 +20,8 @@ import java.util.Locale;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Job that, once there is a valid local subscriber id, should be run every 3 days
|
||||
* to ensure that a user's subscription does not lapse.
|
||||
|
@ -132,13 +135,19 @@ public class SubscriptionKeepAliveJob extends BaseJob {
|
|||
MultiDeviceSubscriptionSyncRequestJob.enqueue();
|
||||
}
|
||||
|
||||
boolean isLongRunning = Objects.equals(activeSubscription.getActiveSubscription().getPaymentMethod(), ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT);
|
||||
TerminalDonationQueue.TerminalDonation terminalDonation = new TerminalDonationQueue.TerminalDonation(
|
||||
activeSubscription.getActiveSubscription().getLevel(),
|
||||
Objects.equals(activeSubscription.getActiveSubscription().getPaymentMethod(), ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT),
|
||||
null,
|
||||
ByteString.EMPTY
|
||||
);
|
||||
|
||||
if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodConversionStarted()) {
|
||||
Log.i(TAG, "Subscription end of period is after the conversion end of period. Storing it, generating a credential, and enqueuing the continuation job chain.", true);
|
||||
SignalStore.donationsValues().setSubscriptionEndOfPeriodConversionStarted(endOfCurrentPeriod);
|
||||
SignalStore.donationsValues().refreshSubscriptionRequestCredential();
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue();
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, terminalDonation).enqueue();
|
||||
} else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedemptionStarted()) {
|
||||
if (SignalStore.donationsValues().getSubscriptionRequestCredential() == null) {
|
||||
Log.i(TAG, "We have not started a redemption, but do not have a request credential. Possible that the subscription changed.", true);
|
||||
|
@ -146,7 +155,7 @@ public class SubscriptionKeepAliveJob extends BaseJob {
|
|||
}
|
||||
|
||||
Log.i(TAG, "We have a request credential and have not yet turned it into a redeemable token.", true);
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue();
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, terminalDonation).enqueue();
|
||||
} else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedeemed()) {
|
||||
if (SignalStore.donationsValues().getSubscriptionReceiptCredential() == null) {
|
||||
Log.i(TAG, "We have successfully started redemption but have no stored token. Possible that the subscription changed.", true);
|
||||
|
|
|
@ -20,6 +20,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -35,6 +37,8 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
|
|||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* Job responsible for submitting ReceiptCredentialRequest objects to the server until
|
||||
* we get a response.
|
||||
|
@ -49,40 +53,44 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
private static final String DATA_SUBSCRIBER_ID = "data.subscriber.id";
|
||||
private static final String DATA_IS_FOR_KEEP_ALIVE = "data.is.for.keep.alive";
|
||||
private static final String DATA_UI_SESSION_KEY = "data.ui.session.key";
|
||||
private static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
private static final String DATA_TERMINAL_DONATION = "data.terminal.donation";
|
||||
|
||||
public static final Object MUTEX = new Object();
|
||||
|
||||
private final SubscriberId subscriberId;
|
||||
private final boolean isForKeepAlive;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunningDonationPaymentType;
|
||||
private final SubscriberId subscriberId;
|
||||
private final boolean isForKeepAlive;
|
||||
private final long uiSessionKey;
|
||||
private TerminalDonationQueue.TerminalDonation terminalDonation;
|
||||
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(@NonNull SubscriberId subscriberId,
|
||||
boolean isForKeepAlive,
|
||||
long uiSessionKey,
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
return new SubscriptionReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("ReceiptRedemption")
|
||||
.setMaxInstancesForQueue(1)
|
||||
.setLifespan(isLongRunningDonationPaymentType ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1))
|
||||
.setLifespan(terminalDonation.isLongRunningPaymentMethod ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
subscriberId,
|
||||
isForKeepAlive,
|
||||
uiSessionKey,
|
||||
isLongRunningDonationPaymentType
|
||||
terminalDonation
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey, terminalDonation);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, boolean isLongRunningDonationPaymentType) {
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, @NonNull TerminalDonationQueue.TerminalDonation terminalDonation) {
|
||||
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey, isLongRunningDonationPaymentType);
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, terminalDonation);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey, terminalDonation.isLongRunningPaymentMethod);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
||||
|
@ -97,13 +105,13 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
@NonNull SubscriberId subscriberId,
|
||||
boolean isForKeepAlive,
|
||||
long uiSessionKey,
|
||||
boolean isLongRunningDonationPaymentType)
|
||||
@NonNull TerminalDonationQueue.TerminalDonation terminalDonation)
|
||||
{
|
||||
super(parameters);
|
||||
this.subscriberId = subscriberId;
|
||||
this.isForKeepAlive = isForKeepAlive;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunningDonationPaymentType = isLongRunningDonationPaymentType;
|
||||
this.subscriberId = subscriberId;
|
||||
this.isForKeepAlive = isForKeepAlive;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.terminalDonation = terminalDonation;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,7 +119,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
JsonJobData.Builder builder = new JsonJobData.Builder().putBlobAsString(DATA_SUBSCRIBER_ID, subscriberId.getBytes())
|
||||
.putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive)
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunningDonationPaymentType);
|
||||
.putBlobAsString(DATA_TERMINAL_DONATION, terminalDonation.encode());
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
|
@ -123,6 +131,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
if (terminalDonation.error != null) {
|
||||
SignalStore.donationsValues().appendToTerminalDonationQueue(terminalDonation);
|
||||
} else {
|
||||
Log.w(TAG, "Job is in terminal state without an error on TerminalDonation.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -132,6 +145,15 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (terminalDonation.isLongRunningPaymentMethod) {
|
||||
return TimeUnit.DAYS.toMillis(1);
|
||||
} else {
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void doRun() throws Exception {
|
||||
ReceiptCredentialRequestContext requestContext = SignalStore.donationsValues().getSubscriptionRequestCredential();
|
||||
ActiveSubscription activeSubscription = getLatestSubscriptionInformation();
|
||||
|
@ -202,7 +224,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
ReceiptCredential receiptCredential = getReceiptCredential(requestContext, response.getResult().get());
|
||||
|
||||
if (!isCredentialValid(subscription, receiptCredential)) {
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new IOException("Could not validate receipt credential");
|
||||
}
|
||||
|
||||
|
@ -214,6 +236,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
SignalStore.donationsValues().clearSubscriptionRequestCredential();
|
||||
SignalStore.donationsValues().setSubscriptionReceiptCredential(receiptCredentialPresentation);
|
||||
SignalStore.donationsValues().setSubscriptionEndOfPeriodRedemptionStarted(subscription.getEndOfCurrentPeriod());
|
||||
|
||||
setOutputData(new JsonJobData.Builder()
|
||||
.putBlobAsString(DonationReceiptRedemptionJob.INPUT_TERMINAL_DONATION, terminalDonation.encode())
|
||||
.build()
|
||||
.serialize());
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a retryable exception: " + response.getStatus(), response.getExecutionError().orElse(null), true);
|
||||
throw new RetryableException();
|
||||
|
@ -228,7 +255,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
return activeSubscription.getResult().get();
|
||||
} else if (activeSubscription.getApplicationError().isPresent()) {
|
||||
Log.w(TAG, "Unrecoverable error getting the user's current subscription. Failing.", activeSubscription.getApplicationError().get(), true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new IOException(activeSubscription.getApplicationError().get());
|
||||
} else {
|
||||
throw new RetryableException();
|
||||
|
@ -265,18 +292,18 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
throw new RetryableException();
|
||||
case 400:
|
||||
Log.w(TAG, "Receipt credential request failed to validate.", response.getApplicationError().get(), true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
case 402:
|
||||
Log.w(TAG, "Payment looks like a failure but may be retried.", response.getApplicationError().get(), true);
|
||||
throw new RetryableException();
|
||||
case 403:
|
||||
Log.w(TAG, "SubscriberId password mismatch or account auth was present.", response.getApplicationError().get(), true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
case 404:
|
||||
Log.w(TAG, "SubscriberId not found or misformed.", response.getApplicationError().get(), true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
case 409:
|
||||
onAlreadyRedeemed(response);
|
||||
|
@ -287,6 +314,35 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private void onGenericRedemptionError() {
|
||||
terminalDonation = terminalDonation.newBuilder()
|
||||
.error(new DonationErrorValue(
|
||||
DonationErrorValue.Type.REDEMPTION,
|
||||
"",
|
||||
ByteString.EMPTY
|
||||
))
|
||||
.build();
|
||||
|
||||
DonationError.routeBackgroundError(
|
||||
context,
|
||||
uiSessionKey,
|
||||
DonationError.genericBadgeRedemptionFailure(getErrorSource())
|
||||
);
|
||||
}
|
||||
|
||||
private void onPaymentFailedError(DonationError.PaymentSetupError paymentFailure) {
|
||||
terminalDonation = terminalDonation.newBuilder()
|
||||
.error(DonationError.toDonationErrorValue(paymentFailure))
|
||||
.build();
|
||||
|
||||
DonationError.routeBackgroundError(
|
||||
context,
|
||||
uiSessionKey,
|
||||
paymentFailure,
|
||||
terminalDonation.isLongRunningPaymentMethod
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles state updates and error routing for a payment failure.
|
||||
* <p>
|
||||
|
@ -342,7 +398,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
|
||||
Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, paymentSetupError);
|
||||
onPaymentFailedError(paymentSetupError);
|
||||
} else if (chargeFailure != null && processor == ActiveSubscription.Processor.BRAINTREE) {
|
||||
Log.d(TAG, "PayPal charge failure detected: " + chargeFailure, true);
|
||||
|
||||
|
@ -380,10 +436,10 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
|
||||
Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, paymentSetupError);
|
||||
onPaymentFailedError(paymentSetupError);
|
||||
} else {
|
||||
Log.d(TAG, "Not for a keep-alive and we have a failure status. Routing a payment setup error...", true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, new DonationError.PaymentSetupError.GenericError(
|
||||
onPaymentFailedError(new DonationError.PaymentSetupError.GenericError(
|
||||
getErrorSource(),
|
||||
new Exception("Got a failure status from the subscription object.")
|
||||
));
|
||||
|
@ -399,7 +455,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
setOutputData(new JsonJobData.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_KEEP_ALIVE_409, true).serialize());
|
||||
} else {
|
||||
Log.w(TAG, "Latest paid receipt on subscription already redeemed with a different request credential.", response.getApplicationError().get(), true);
|
||||
DonationError.routeBackgroundError(context, uiSessionKey, DonationError.genericBadgeRedemptionFailure(getErrorSource()));
|
||||
onGenericRedemptionError();
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
}
|
||||
}
|
||||
|
@ -446,12 +502,12 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
public @NonNull SubscriptionReceiptRequestResponseJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
JsonJobData data = JsonJobData.deserialize(serializedData);
|
||||
|
||||
SubscriberId subscriberId = SubscriberId.fromBytes(data.getStringAsBlob(DATA_SUBSCRIBER_ID));
|
||||
boolean isForKeepAlive = data.getBooleanOrDefault(DATA_IS_FOR_KEEP_ALIVE, false);
|
||||
String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null);
|
||||
byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null;
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
boolean isLongRunningDonationPaymentType = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
SubscriberId subscriberId = SubscriberId.fromBytes(data.getStringAsBlob(DATA_SUBSCRIBER_ID));
|
||||
boolean isForKeepAlive = data.getBooleanOrDefault(DATA_IS_FOR_KEEP_ALIVE, false);
|
||||
String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null);
|
||||
byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null;
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
byte[] rawTerminalDonation = data.getStringAsBlob(DATA_TERMINAL_DONATION);
|
||||
|
||||
ReceiptCredentialRequestContext requestContext;
|
||||
if (requestContextBytes != null && SignalStore.donationsValues().getSubscriptionRequestCredential() == null) {
|
||||
|
@ -464,7 +520,25 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, isLongRunningDonationPaymentType);
|
||||
TerminalDonationQueue.TerminalDonation terminalDonation = null;
|
||||
if (rawTerminalDonation != null) {
|
||||
try {
|
||||
terminalDonation = TerminalDonationQueue.TerminalDonation.ADAPTER.decode(rawTerminalDonation);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to parse terminal donation. Generating a default.");
|
||||
}
|
||||
}
|
||||
|
||||
if (terminalDonation == null) {
|
||||
terminalDonation = new TerminalDonationQueue.TerminalDonation(
|
||||
-1,
|
||||
false,
|
||||
null,
|
||||
ByteString.EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, terminalDonation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.isExpired
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
|
@ -505,35 +506,35 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
var subscriptionEndOfPeriodRedemptionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_REDEEM, 0L)
|
||||
var subscriptionEndOfPeriodRedeemed by longValue(SUBSCRIPTION_EOP_REDEEMED, 0L)
|
||||
|
||||
fun appendToDonationCompletionList(donationCompleted: DonationCompletedQueue.DonationCompleted) {
|
||||
fun appendToTerminalDonationQueue(terminalDonation: TerminalDonationQueue.TerminalDonation) {
|
||||
synchronized(this) {
|
||||
val pendingBytes = getBlob(DONATION_COMPLETE_QUEUE, null)
|
||||
val queue: DonationCompletedQueue = pendingBytes?.let { DonationCompletedQueue.ADAPTER.decode(pendingBytes) } ?: DonationCompletedQueue()
|
||||
val newQueue: DonationCompletedQueue = queue.copy(donationsCompleted = queue.donationsCompleted + donationCompleted)
|
||||
val queue: TerminalDonationQueue = pendingBytes?.let { TerminalDonationQueue.ADAPTER.decode(pendingBytes) } ?: TerminalDonationQueue()
|
||||
val newQueue: TerminalDonationQueue = queue.copy(terminalDonations = queue.terminalDonations + terminalDonation)
|
||||
|
||||
putBlob(DONATION_COMPLETE_QUEUE, newQueue.encode())
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeDonationCompletionList(): List<DonationCompletedQueue.DonationCompleted> {
|
||||
fun consumeTerminalDonations(): List<TerminalDonationQueue.TerminalDonation> {
|
||||
synchronized(this) {
|
||||
val pendingBytes = getBlob(DONATION_COMPLETE_QUEUE, null)
|
||||
if (pendingBytes == null) {
|
||||
return emptyList()
|
||||
} else {
|
||||
val queue: DonationCompletedQueue = DonationCompletedQueue.ADAPTER.decode(pendingBytes)
|
||||
val queue: TerminalDonationQueue = TerminalDonationQueue.ADAPTER.decode(pendingBytes)
|
||||
remove(DONATION_COMPLETE_QUEUE)
|
||||
|
||||
return queue.donationsCompleted
|
||||
return queue.terminalDonations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDonationComplete(level: Long) {
|
||||
fun removeTerminalDonation(level: Long) {
|
||||
synchronized(this) {
|
||||
val donationCompletionList = consumeDonationCompletionList()
|
||||
val donationCompletionList = consumeTerminalDonations()
|
||||
donationCompletionList.filterNot { it.level == level }.forEach {
|
||||
appendToDonationCompletionList(it)
|
||||
appendToTerminalDonationQueue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -551,7 +552,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
}
|
||||
}
|
||||
|
||||
fun setPendingOneTimeDonationError(error: PendingOneTimeDonation.Error) {
|
||||
fun setPendingOneTimeDonationError(error: DonationErrorValue) {
|
||||
synchronized(this) {
|
||||
val pendingOneTimeDonation = getPendingOneTimeDonation()
|
||||
if (pendingOneTimeDonation != null) {
|
||||
|
|
|
@ -285,20 +285,20 @@ message FiatValue {
|
|||
uint64 timestamp = 3;
|
||||
}
|
||||
|
||||
message PendingOneTimeDonation {
|
||||
message Error {
|
||||
enum Type {
|
||||
PROCESSOR_CODE = 0; // Generic processor error (e.g. Stripe returned an error code)
|
||||
DECLINE_CODE = 1; // Stripe or PayPal decline Code
|
||||
FAILURE_CODE = 2; // Stripe bank transfer failure code
|
||||
REDEMPTION = 3; // Generic redemption error (status is HTTP code)
|
||||
PAYMENT = 4; // Generic payment error (status is HTTP code)
|
||||
}
|
||||
|
||||
Type type = 1;
|
||||
string code = 2;
|
||||
message DonationErrorValue {
|
||||
enum Type {
|
||||
PROCESSOR_CODE = 0; // Generic processor error (e.g. Stripe returned an error code)
|
||||
DECLINE_CODE = 1; // Stripe or PayPal decline Code
|
||||
FAILURE_CODE = 2; // Stripe bank transfer failure code
|
||||
REDEMPTION = 3; // Generic redemption error (status is HTTP code)
|
||||
PAYMENT = 4; // Generic payment error (status is HTTP code)
|
||||
}
|
||||
|
||||
Type type = 1;
|
||||
string code = 2;
|
||||
}
|
||||
|
||||
message PendingOneTimeDonation {
|
||||
enum PaymentMethodType {
|
||||
CARD = 0;
|
||||
SEPA_DEBIT = 1;
|
||||
|
@ -306,20 +306,27 @@ message PendingOneTimeDonation {
|
|||
IDEAL = 3;
|
||||
}
|
||||
|
||||
PaymentMethodType paymentMethodType = 1;
|
||||
FiatValue amount = 2;
|
||||
BadgeList.Badge badge = 3;
|
||||
int64 timestamp = 4;
|
||||
optional Error error = 5;
|
||||
PaymentMethodType paymentMethodType = 1;
|
||||
FiatValue amount = 2;
|
||||
BadgeList.Badge badge = 3;
|
||||
int64 timestamp = 4;
|
||||
optional DonationErrorValue error = 5;
|
||||
}
|
||||
|
||||
message DonationCompletedQueue {
|
||||
message DonationCompleted {
|
||||
int64 level = 1;
|
||||
bool isLongRunningPaymentMethod = 2;
|
||||
/**
|
||||
* Contains the data necessary to show the corresponding terminal sheet
|
||||
* for a given donation. Note that the word "terminal" here is used in
|
||||
* the same way that it is used in Rx, where we simply mean that, regardless
|
||||
* of outcome, a donation has completed processing.
|
||||
*/
|
||||
message TerminalDonationQueue {
|
||||
message TerminalDonation {
|
||||
int64 level = 1;
|
||||
bool isLongRunningPaymentMethod = 2;
|
||||
optional DonationErrorValue error = 3;
|
||||
}
|
||||
|
||||
repeated DonationCompleted donationsCompleted = 1;
|
||||
repeated TerminalDonation terminalDonations = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M1.5 12C1.5 6.2 6.2 1.5 12 1.5S22.5 6.2 22.5 12 17.8 22.5 12 22.5 1.5 17.8 1.5 12ZM12 6.5c-0.7 0-1.24 0.6-1.2 1.29l0.43 5.5C11.26 13.69 11.6 14 12 14s0.74-0.31 0.77-0.71l0.42-5.5C13.24 7.09 12.7 6.5 12 6.5Zm0 8.75c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.25 1.25 1.25 1.25-0.56 1.25-1.25-0.56-1.25-1.25-1.25Z"/>
|
||||
</vector>
|
|
@ -592,8 +592,16 @@
|
|||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_oneTimeDonationConfigurationFragment"
|
||||
app:destination="@id/oneTimeDonationConfigurationFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_terminalDonationConfigurationFragment"
|
||||
app:destination="@id/terminalDonationConfigurationFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/terminalDonationConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalTerminalDonationConfigurationFragment"
|
||||
android:label="terminal_donation_configuration_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/oneTimeDonationConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalPendingOneTimeDonationConfigurationFragment"
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<string name="username_support_url" translatable="false">https://support.signal.org/hc/articles/5389476324250</string>
|
||||
<string name="export_account_data_url" translatable="false">https://support.signal.org/hc/articles/5538911756954</string>
|
||||
<string name="pending_transfer_url" translatable="false">https://support.signal.org/hc/articles/360031949872#pending</string>
|
||||
<string name="donate_faq_url" translatable="false">https://support.signal.org/hc/articles/360031949872#donate</string>
|
||||
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
|
@ -4829,6 +4830,10 @@
|
|||
<string name="SubscribeFragment__processing_payment">Processing payment…</string>
|
||||
<!-- Displayed in notification when user payment fails to process on Stripe -->
|
||||
<string name="DonationsErrors__error_processing_payment">Error processing payment</string>
|
||||
<!-- Displayed on manage donations screen as a dialog message when payment method failed -->
|
||||
<string name="DonationsErrors__try_another_payment_method">Try another payment method or contact your bank for more information.</string>
|
||||
<!-- Displayed on manage donations screen error dialogs as an action label -->
|
||||
<string name="DonationsErrors__learn_more">Learn more</string>
|
||||
<!-- Displayed on "My Support" screen when user subscription payment method failed. -->
|
||||
<string name="DonationsErrors__error_processing_payment_s">Error processing payment. %1$s</string>
|
||||
<string name="DonationsErrors__your_payment">Your payment couldn\'t be processed and you have not been charged. Please try again.</string>
|
||||
|
@ -5954,6 +5959,15 @@
|
|||
<!-- Confirmation button for donation pending sheet displayed after donating via a bank transfer. -->
|
||||
<string name="DonationPendingBottomSheet__done">Done</string>
|
||||
|
||||
<!-- Title of donation error sheet displayed after making a bank transfer that fails -->
|
||||
<string name="DonationErrorBottomSheet__donation_couldnt_be_processed">Donation couldn\'t be processed</string>
|
||||
<!-- Text block of donation error sheet displayed after making a bank transfer that fails -->
|
||||
<string name="DonationErrorBottomSheet__were_having_trouble">We\'re having trouble processing your bank transfer. You have not been charged. Try another payment method or contact your bank for more information.</string>
|
||||
<!-- Button label for retry button of donation error sheet displayed after making a bank transfer that fails -->
|
||||
<string name="DonationErrorBottomSheet__try_again">Try again</string>
|
||||
<!-- Button label for not now button of donation error sheet displayed after making a bank transfer that fails -->
|
||||
<string name="DonationErrorBottomSheet__not_now">Not now</string>
|
||||
|
||||
<!-- Title of \'Donation Complete\' sheet displayed after a bank transfer completes and the badge is redeemed -->
|
||||
<string name="DonationCompletedBottomSheet__donation_complete">Donation Complete</string>
|
||||
<!-- Text block of \'Donation Complete\' sheet displayed after a bank transfer completes and the badge is redeemed -->
|
||||
|
|
|
@ -3,10 +3,10 @@ package org.signal.donations
|
|||
/**
|
||||
* Stripe Payment Processor decline codes
|
||||
*/
|
||||
sealed class StripeDeclineCode {
|
||||
sealed class StripeDeclineCode(val rawCode: String) {
|
||||
|
||||
data class Known(val code: Code) : StripeDeclineCode()
|
||||
data class Unknown(val code: String) : StripeDeclineCode()
|
||||
data class Known(val code: Code) : StripeDeclineCode(code.code)
|
||||
data class Unknown(val code: String) : StripeDeclineCode(code)
|
||||
|
||||
fun isKnown(): Boolean = this is Known
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ package org.signal.donations
|
|||
* Bank Transfer failure codes, as detailed here:
|
||||
* https://stripe.com/docs/payments/sepa-debit#failed-payments
|
||||
*/
|
||||
sealed interface StripeFailureCode {
|
||||
data class Known(val code: Code) : StripeFailureCode
|
||||
data class Unknown(val code: String) : StripeFailureCode
|
||||
sealed class StripeFailureCode(val rawCode: String) {
|
||||
data class Known(val code: Code) : StripeFailureCode(code.code)
|
||||
data class Unknown(val code: String) : StripeFailureCode(code)
|
||||
|
||||
val isKnown get() = this is Known
|
||||
enum class Code(val code: String) {
|
||||
|
|
Loading…
Add table
Reference in a new issue