Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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
Expand All @@ -33,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
Expand Down Expand Up @@ -108,14 +108,47 @@ 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),
Toast.LENGTH_SHORT
).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.paymentProError)
.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
}
Expand Down Expand Up @@ -699,12 +732,15 @@ class ProSettingsViewModel @AssistedInject constructor(
viewModelScope.launch {
val selectedPlan = getSelectedPlan() ?: return@launch

val purchaseStarted = 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(purchaseStarted.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)
Expand All @@ -714,10 +750,6 @@ class ProSettingsViewModel @AssistedInject constructor(
}
}

fun getSubscriptionManager(): SubscriptionManager {
return subscriptionCoordinator.getCurrentManager()
}

private fun navigateTo(
destination: ProSettingsDestination,
navOptions: NavOptionsBuilder.() -> Unit = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -191,25 +195,26 @@ fun ChoosePlan(

Spacer(Modifier.height(LocalDimensions.current.xxsSpacing))

val footer = when (planData.subscriptionType) {
val (footerAction, footerActivation) = 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()
stringResource(R.string.proRenewingAction) to
stringResource(R.string.proReactivatingActivation)

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.proUpdatingAction) to ""

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.proUpgradingAction) to
stringResource(R.string.proActivatingActivation)

}

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(
Expand All @@ -224,13 +229,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,
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -272,32 +276,53 @@ 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: 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
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}
}
}

class PaymentServerException: Exception()

data class SubscriptionPricing(
val subscriptionDuration: ProSubscriptionDuration,
val priceAmountMicros: Long,
Expand Down
Loading