From d0f8e4f181cacd0ee968a770de96ca1778c83222 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 13:47:01 +1100 Subject: [PATCH 1/8] New T&Cs - still need real crowdin strings --- .../libsession/utilities/StringSubKeys.kt | 3 ++ .../prosettings/chooseplan/ChoosePlan.kt | 50 +++++++++++++------ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt b/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt index 542203da87..4008f3f1bb 100644 --- a/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt +++ b/app/src/main/java/org/session/libsession/utilities/StringSubKeys.kt @@ -62,4 +62,7 @@ object StringSubstitutionConstants { const val PERCENT_KEY: StringSubKey = "percent" const val DEVICE_TYPE_KEY: StringSubKey = "device_type" const val SESSION_FOUNDATION_KEY: StringSubKey = "session_foundation" + const val ACTION_TYPE_KEY: StringSubKey = "action_type" + const val ACTIVATION_TYPE_KEY: StringSubKey = "activation_type" + const val ENTITY_KEY: StringSubKey = "entity" } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt index 7cc98f1380..bcb4254d0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt @@ -48,9 +48,13 @@ import com.squareup.phrase.Phrase import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants +import org.session.libsession.utilities.StringSubstitutionConstants.ACTION_TYPE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.ACTIVATION_TYPE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CURRENT_PLAN_LENGTH_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.ENTITY_KEY import org.session.libsession.utilities.StringSubstitutionConstants.ICON_KEY import org.session.libsession.utilities.StringSubstitutionConstants.MONTHLY_PRICE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRICE_KEY @@ -191,25 +195,21 @@ fun ChoosePlan( Spacer(Modifier.height(LocalDimensions.current.xxsSpacing)) - val footer = when (planData.subscriptionType) { - is SubscriptionType.Expired -> - Phrase.from(LocalContext.current.getText(R.string.proRenewTosPrivacy)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(ICON_KEY, iconExternalLink) - .format() + val (footerAction, footerActivation) = when (planData.subscriptionType) { + is SubscriptionType.Expired -> stringResource(R.string.upgrade) to stringResource(R.string.upgrade) //todo STRINGS need crowdin strings here - is SubscriptionType.Active -> Phrase.from(LocalContext.current.getText(R.string.proTosPrivacy)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(ICON_KEY, iconExternalLink) - .format() + is SubscriptionType.Active -> stringResource(R.string.upgrade) to "" //todo STRINGS need crowdin strings here is SubscriptionType.NeverSubscribed -> - Phrase.from(LocalContext.current.getText(R.string.proUpgradingTosPrivacy)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(ICON_KEY, iconExternalLink) - .format() + stringResource(R.string.upgrade) to stringResource(R.string.upgrade)//todo STRINGS need crowdin strings here } + val footer = Phrase.from(LocalContext.current.getText(R.string.noteTosPrivacyPolicy)) + .put(ACTION_TYPE_KEY, footerAction) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(ICON_KEY, iconExternalLink) + .format() + Text( modifier = Modifier.fillMaxWidth() .clickable( @@ -224,13 +224,33 @@ fun ChoosePlan( .clip(MaterialTheme.shapes.extraSmall), text = annotatedStringResource(footer), textAlign = TextAlign.Center, - style = LocalType.current.small, + style = LocalType.current.base, color = LocalColors.current.text, inlineContent = inlineContentMap( textSize = LocalType.current.small.fontSize, imageColor = LocalColors.current.text ), ) + + // add another label in cases other than an active subscription + if (planData.subscriptionType !is SubscriptionType.Active) { + Spacer(Modifier.height(LocalDimensions.current.xxxsSpacing)) + Text( + modifier = Modifier.fillMaxWidth(), + text = annotatedStringResource( + Phrase.from(LocalContext.current.getText(R.string.proTosDescription)) + .put(ACTION_TYPE_KEY, footerAction) + .put(ACTIVATION_TYPE_KEY, footerActivation) + .put(ENTITY_KEY, NonTranslatableStringConstants.ENTITY_STF) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .format() + ), + textAlign = TextAlign.Center, + style = LocalType.current.base, + color = LocalColors.current.text, + ) + } } } From 449b5d9aece6d6551a715b69d1f8c5317e540f76 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 15:31:44 +1100 Subject: [PATCH 2/8] New Error handling in payment - Server side error with custom dialog --- .../prosettings/ProSettingsViewModel.kt | 37 ++++++++++++++++++- .../prosettings/chooseplan/ChoosePlan.kt | 11 ++++-- .../pro/subscription/SubscriptionManager.kt | 12 +++++- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index f04198ddd4..a3bcbbb8e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.icu.util.MeasureUnit import android.widget.Toast +import androidx.compose.ui.res.stringResource import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants +import org.session.libsession.utilities.StringSubstitutionConstants.ACTION_TYPE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CURRENT_PLAN_LENGTH_KEY @@ -108,7 +110,7 @@ class ProSettingsViewModel @AssistedInject constructor( navigator.navigate(destination = ProSettingsDestination.PlanConfirmation) } - is SubscriptionManager.PurchaseEvent.Failed -> { + is SubscriptionManager.PurchaseEvent.Failed.GenericError -> { Toast.makeText( context, purchaseEvent.errorMessage ?: context.getString(R.string.errorGeneric), @@ -116,6 +118,39 @@ class ProSettingsViewModel @AssistedInject constructor( ).show() } + is SubscriptionManager.PurchaseEvent.Failed.ServerError -> { + // this is a special case of failure. We should display a custom dialog and allow the user to retry + _dialogState.update { + val action = context.getString( + when(_proSettingsUIState.value.subscriptionState.type) { + is SubscriptionType.Active -> R.string.proUpdatingAction + is SubscriptionType.Expired -> R.string.proRenewingAction + else -> R.string.proUpgradingAction + } + ) + + it.copy( + showSimpleDialog = SimpleDialogData( + title = context.getString(R.string.paymentError), + message = Phrase.from(context, R.string.proAutoRenewTime) + .put(ACTION_TYPE_KEY, action) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format(), + positiveText = context.getString(R.string.retry), + negativeText = context.getString(R.string.helpSupport), + positiveStyleDanger = false, + showXIcon = true, + onPositive = { + getPlanFromProvider() // retry getting the plan from provider + }, + onNegative = { + onCommand(ShowOpenUrlDialog(ProStatusManager.URL_PRO_SUPPORT)) + } + ) + ) + } + } + is SubscriptionManager.PurchaseEvent.Cancelled -> { // nothing to do in this case } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt index bcb4254d0c..2f6c8c2700 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlan.kt @@ -196,12 +196,17 @@ fun ChoosePlan( Spacer(Modifier.height(LocalDimensions.current.xxsSpacing)) val (footerAction, footerActivation) = when (planData.subscriptionType) { - is SubscriptionType.Expired -> stringResource(R.string.upgrade) to stringResource(R.string.upgrade) //todo STRINGS need crowdin strings here + is SubscriptionType.Expired -> + stringResource(R.string.proRenewingAction) to + stringResource(R.string.proReactivatingActivation) - is SubscriptionType.Active -> stringResource(R.string.upgrade) to "" //todo STRINGS need crowdin strings here + + is SubscriptionType.Active -> stringResource(R.string.proUpdatingAction) to "" is SubscriptionType.NeverSubscribed -> - stringResource(R.string.upgrade) to stringResource(R.string.upgrade)//todo STRINGS need crowdin strings here + stringResource(R.string.proUpgradingAction) to + stringResource(R.string.proActivatingActivation) + } val footer = Phrase.from(LocalContext.current.getText(R.string.noteTosPrivacyPolicy)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt index 6c3d9ddd74..ddd07fb013 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt @@ -34,7 +34,10 @@ abstract class SubscriptionManager( sealed interface PurchaseEvent { data object Success : PurchaseEvent data object Cancelled : PurchaseEvent - data class Failed(val errorMessage: String? = null) : PurchaseEvent + sealed interface Failed : PurchaseEvent { + data class GenericError(val errorMessage: String? = null): Failed + data object ServerError : Failed + } } // purchase events @@ -74,11 +77,16 @@ abstract class SubscriptionManager( proStatusManager.appProPaymentToBackend() _purchaseEvents.emit(PurchaseEvent.Success) } catch (e: Exception) { - _purchaseEvents.emit(PurchaseEvent.Failed()) + when (e) { + is PaymentServerException -> _purchaseEvents.emit(PurchaseEvent.Failed.ServerError) + else -> _purchaseEvents.emit(PurchaseEvent.Failed.GenericError(e.message)) + } } } } + class PaymentServerException: Exception() + data class SubscriptionPricing( val subscriptionDuration: ProSubscriptionDuration, val priceAmountMicros: Long, From dab39935c5b22a73f5d18426f2e70d3af7748957 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 15:38:18 +1100 Subject: [PATCH 3/8] SES-4838 - Fixing Expired footer --- .../prosettings/ProSettingsHomeScreen.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt index adac24d3ed..f0b3d3a7b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsHomeScreen.kt @@ -907,14 +907,16 @@ fun ProSettingsFooter( inSheet: Boolean, sendCommand: (ProSettingsViewModel.Commands) -> Unit, ) { - // Manage Pro - Pro - Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) - ProManage( - data = subscriptionType, - inSheet = inSheet, - subscriptionRefreshState = subscriptionRefreshState, - sendCommand = sendCommand, - ) + // Manage Pro - Expired has this in the header so exclude it here + if(subscriptionType !is SubscriptionType.Expired) { + Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) + ProManage( + data = subscriptionType, + inSheet = inSheet, + subscriptionRefreshState = subscriptionRefreshState, + sendCommand = sendCommand, + ) + } // Help Spacer(Modifier.height(LocalDimensions.current.spacing)) From 50711d5bebb90de8b267d8c4cc90ce7331e9e9f6 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 15:47:55 +1100 Subject: [PATCH 4/8] Fixed string key --- .../securesms/preferences/prosettings/ProSettingsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index a3bcbbb8e3..582e0e544a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -132,7 +132,7 @@ class ProSettingsViewModel @AssistedInject constructor( it.copy( showSimpleDialog = SimpleDialogData( title = context.getString(R.string.paymentError), - message = Phrase.from(context, R.string.proAutoRenewTime) + message = Phrase.from(context, R.string.paymentProError) .put(ACTION_TYPE_KEY, action) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), From b5e399895ec277be2b5383455fba30f784f10e08 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 16:04:34 +1100 Subject: [PATCH 5/8] Fix up logic --- .../prosettings/ProSettingsViewModel.kt | 10 ++-------- .../pro/subscription/SubscriptionManager.kt | 2 +- .../subscription/PlayStoreSubscriptionManager.kt | 14 ++------------ 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index 582e0e544a..a67fde9380 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.icu.util.MeasureUnit import android.widget.Toast -import androidx.compose.ui.res.stringResource import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -35,7 +34,6 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.SELECTED_PLAN_LENGTH_KEY import org.session.libsession.utilities.StringSubstitutionConstants.SELECTED_PLAN_LENGTH_SINGULAR_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY -import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.SubscriptionState @@ -734,12 +732,12 @@ class ProSettingsViewModel @AssistedInject constructor( viewModelScope.launch { val selectedPlan = getSelectedPlan() ?: return@launch - val purchaseStarted = subscriptionCoordinator.getCurrentManager().purchasePlan( + val purchaseResult = subscriptionCoordinator.getCurrentManager().purchasePlan( selectedPlan.durationType ) val data = choosePlanState.value - if(purchaseStarted.isSuccess && data is State.Success) { + if(purchaseResult.isSuccess && data is State.Success) { _choosePlanState.update { State.Success( data.value.copy(purchaseInProgress = true) @@ -749,10 +747,6 @@ class ProSettingsViewModel @AssistedInject constructor( } } - fun getSubscriptionManager(): SubscriptionManager { - return subscriptionCoordinator.getCurrentManager() - } - private fun navigateTo( destination: ProSettingsDestination, navOptions: NavOptionsBuilder.() -> Unit = {} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt index ddd07fb013..f365e1ca3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/SubscriptionManager.kt @@ -79,7 +79,7 @@ abstract class SubscriptionManager( } catch (e: Exception) { when (e) { is PaymentServerException -> _purchaseEvents.emit(PurchaseEvent.Failed.ServerError) - else -> _purchaseEvents.emit(PurchaseEvent.Failed.GenericError(e.message)) + else -> _purchaseEvents.emit(PurchaseEvent.Failed.GenericError()) } } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt index 347f7d4a0f..c2a2a7eb27 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.pro.subscription import android.app.Application -import android.widget.Toast import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams @@ -17,15 +16,10 @@ import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -33,14 +27,11 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.debugmenu.DebugLogGroup import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager.PurchaseEvent import org.thoughtcrime.securesms.util.CurrentActivityObserver import java.time.Instant import java.time.temporal.ChronoUnit @@ -178,9 +169,8 @@ class PlayStoreSubscriptionManager @Inject constructor( } catch (e: Exception) { Log.e(DebugLogGroup.PRO_SUBSCRIPTION.label, "Error purchase plan", e) - withContext(Dispatchers.Main) { - Toast.makeText(application, application.getString(R.string.errorGeneric), Toast.LENGTH_LONG).show() - } + // pass the purchase error information to subscribers + _purchaseEvents.emit(PurchaseEvent.Failed.GenericError()) return Result.failure(e) } From 2cafc80801ca3f00c49d5b4e552e3c82813b5990 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 16:07:39 +1100 Subject: [PATCH 6/8] Comments --- .../preferences/prosettings/ProSettingsViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index a67fde9380..c56c7483eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -732,12 +732,15 @@ class ProSettingsViewModel @AssistedInject constructor( viewModelScope.launch { val selectedPlan = getSelectedPlan() ?: return@launch - val purchaseResult = subscriptionCoordinator.getCurrentManager().purchasePlan( + // let the provider handle the plan from their UI + val providerResult = subscriptionCoordinator.getCurrentManager().purchasePlan( selectedPlan.durationType ) + // check if we managed to display the plan from the provider val data = choosePlanState.value - if(purchaseResult.isSuccess && data is State.Success) { + if(providerResult.isSuccess && data is State.Success) { + // show a loader while the user is looking at the UI from the provider _choosePlanState.update { State.Success( data.value.copy(purchaseInProgress = true) From 2ea466b49d84615855208c6e8bdeaff8640a254d Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 16:27:54 +1100 Subject: [PATCH 7/8] Incorporating PRD logic for timeout and retries --- .../securesms/pro/ProStatusManager.kt | 76 ++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt index 62b87f16c4..8d4dcb896b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.pro import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.ProStatus @@ -25,6 +28,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager import org.thoughtcrime.securesms.util.State import java.time.Duration import java.time.Instant @@ -272,32 +276,52 @@ class ProStatusManager @Inject constructor( } suspend fun appProPaymentToBackend() { - //todo PRO call AddProPaymentRequest in libsession - - // we should `AddProPaymentRequest` with exponential backoff - - /** - * Here are the errors from the back end that we will need to be aware of - * UnknownPayment: means it's potentially not acknowledged yet so might need to keep trying until this work or times out - * Error: is non retryable - we might want a custom UI for this. - * - * - * /// Payment was claimed and the pro proof was successfully generated - * Success = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_SUCCESS, - * - * /// Backend encountered an error when attempting to claim the payment - * Error = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ERROR, - * - * /// Request JSON failed to be parsed correctly, payload was malformed or missing values - * ParseError = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_PARSE_ERROR, - * - * /// Payment is already claimed - * AlreadyRedeemed = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ALREADY_REDEEMED, - * - * /// Payment transaction attempted to claim a payment that the backend does not have. Either the - * /// payment doesn't exist or the backend has not witnessed the payment from the provider yet. - * UnknownPayment = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT, - */ + // max 3 attempts as per PRD + val maxAttempts = 3 + + for (attempt in 1..maxAttempts) { + try { + // 5s timeout as per PRD + withTimeout(5_000L) { + //todo PRO call AddProPaymentRequest in libsession + /** + * Here are the errors from the back end that we will need to be aware of + * UnknownPayment: means it's potentially not acknowledged yet so might need to keep trying until this work or times out + * Error: is non retryable - we might want a custom UI for this. + * + * + * /// Payment was claimed and the pro proof was successfully generated + * Success = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_SUCCESS, + * + * /// Backend encountered an error when attempting to claim the payment + * Error = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ERROR, + * + * /// Request JSON failed to be parsed correctly, payload was malformed or missing values + * ParseError = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_PARSE_ERROR, + * + * /// Payment is already claimed + * AlreadyRedeemed = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ALREADY_REDEEMED, + * + * /// Payment transaction attempted to claim a payment that the backend does not have. Either the + * /// payment doesn't exist or the backend has not witnessed the payment from the provider yet. + * UnknownPayment = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT, + */ + + } + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // If not the last attempt, backoff a little and retry + if (attempt < maxAttempts) { + // small incremental backoff before retry + val backoffMs = 300L * attempt + delay(backoffMs) + } + } + } + + // All attempts failed - throw our custom exception + throw SubscriptionManager.PaymentServerException() } enum class MessageProFeature { From 4f43d3b8cdf38713d14200b3edf7eaad73f49951 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 12 Nov 2025 16:31:22 +1100 Subject: [PATCH 8/8] Updated comment based on error logic --- .../java/org/thoughtcrime/securesms/pro/ProStatusManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt index 8d4dcb896b..d8325ac7c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -286,8 +286,9 @@ class ProStatusManager @Inject constructor( //todo PRO call AddProPaymentRequest in libsession /** * Here are the errors from the back end that we will need to be aware of - * UnknownPayment: means it's potentially not acknowledged yet so might need to keep trying until this work or times out - * Error: is non retryable - we might want a custom UI for this. + * UnknownPayment: retryable > increment counter and try again + * Error, ParseError: is non retryable - throw PaymentServerException + * Success, AlreadyRedeemed - all good * * * /// Payment was claimed and the pro proof was successfully generated