diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ef76a3438..e0b86be788 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,7 @@ configurations.configureEach { exclude(module = "commons-logging") } -val canonicalVersionCode = 430 +val canonicalVersionCode = 432 val canonicalVersionName = "1.30.0" val postFixSize = 10 diff --git a/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt index ebd701fd57..f1eb2e8304 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt @@ -93,7 +93,7 @@ abstract class InputbarViewModel( fun showSessionProCTA(){ _inputBarStateDialogsState.update { - it.copy(sessionProCharLimitCTA = CharLimitCTAData(proStatusManager.subscriptionState.value.type)) + it.copy(sessionProCharLimitCTA = CharLimitCTAData(proStatusManager.proDataState.value.type)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 35705c5c39..e70e6058c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -280,7 +280,7 @@ class MessageDetailsViewModel @AssistedInject constructor( is Commands.ShowProBadgeCTA -> { val features = state.value.proFeatures _dialogState.update { - val proSubscription = proStatusManager.subscriptionState.value.type + val proSubscription = proStatusManager.proDataState.value.type it.copy( proBadgeCTA = when{ features.size > 1 -> ProBadgeCTA.Generic(proSubscription) // always show the generic cta when there are more than 1 feature diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt index 90d10b1e53..756862d7cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt @@ -719,7 +719,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( _dialogState.update { it.copy(pinCTA = PinProCTA( overTheLimit = totalPins > maxPins, - proSubscription = proStatusManager.subscriptionState.value.type + proSubscription = proStatusManager.proDataState.value.type )) } } else { @@ -1237,8 +1237,8 @@ class ConversationSettingsViewModel @AssistedInject constructor( is Commands.ShowProBadgeCTA -> { _dialogState.update { it.copy( - proBadgeCTA = if(recipient?.isGroupV2Recipient == true) ProBadgeCTA.Group(proStatusManager.subscriptionState.value.type) - else ProBadgeCTA.Generic(proStatusManager.subscriptionState.value.type) + proBadgeCTA = if(recipient?.isGroupV2Recipient == true) ProBadgeCTA.Group(proStatusManager.proDataState.value.type) + else ProBadgeCTA.Generic(proStatusManager.proDataState.value.type) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index e1a0421c1a..8197d585fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.ManagerScope +import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.ui.theme.primaryGreen import org.thoughtcrime.securesms.ui.theme.primaryOrange import org.thoughtcrime.securesms.util.DateUtils @@ -127,5 +128,7 @@ data class DebugLogData( ) enum class DebugLogGroup(val label: String, val color: Color){ - AVATAR("Avatar", primaryOrange), PRO_SUBSCRIPTION("Pro Subscription", primaryGreen) + AVATAR("Avatar", primaryOrange), + PRO_SUBSCRIPTION("Pro Subscription", primaryGreen), + PRO_DATA("Pro Data", primaryBlue) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index 3249da0ef1..1212c0c5b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -504,6 +504,14 @@ fun DebugMenu( sendCommand(Copy07PrefixedBlindedPublicKey) } ) + + SlimFillButtonRect ( + text = "Copy Pro Master Key", + modifier = Modifier.fillMaxWidth(), + onClick = { + sendCommand(DebugMenuViewModel.Commands.CopyProMasterKey) + } + ) } Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index b1acc94ba5..e2fcd316fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -202,6 +202,21 @@ class DebugMenuViewModel @AssistedInject constructor( } } + is Commands.CopyProMasterKey -> { + val proKey = loginStateRepository.loggedInState.value?.seeded?.proMasterPrivateKey?.toHexString() + val clip = ClipData.newPlainText("Pro Master Key", proKey) + clipboardManager.setPrimaryClip(ClipData(clip)) + + // Show a toast if the version is below Android 13 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText( + context, + "Copied Pro Master Key to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } + is Commands.HideMessageRequest -> { textSecurePreferences.setHasHiddenMessageRequests(command.hide) _uiState.value = _uiState.value.copy(hideMessageRequests = command.hide) @@ -590,6 +605,7 @@ class DebugMenuViewModel @AssistedInject constructor( object ScheduleTokenNotification : Commands() object Copy07PrefixedBlindedPublicKey : Commands() object CopyAccountId : Commands() + object CopyProMasterKey : Commands() data class HideMessageRequest(val hide: Boolean) : Commands() data class HideNoteToSelf(val hide: Boolean) : Commands() data class ForceCurrentUserAsPro(val set: Boolean) : Commands() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 8c64aacf7c..416a0fb402 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -159,7 +159,7 @@ class HomeViewModel @Inject constructor( init { // observe subscription status viewModelScope.launch { - proStatusManager.subscriptionState.collect { subscription -> + proStatusManager.proDataState.collect { subscription -> // show a CTA (only once per install) when // - subscription is expiring in less than 7 days // - subscription expired less than 30 days ago @@ -262,7 +262,7 @@ class HomeViewModel @Inject constructor( it.copy( pinCTA = PinProCTA( overTheLimit = totalPins > maxPins, - proSubscription = proStatusManager.subscriptionState.value.type + proSubscription = proStatusManager.proDataState.value.type ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt index b7d6eeec87..891ba0a687 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt @@ -75,13 +75,29 @@ import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.TempAvatar import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.UserAvatar -import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.* +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ClearData +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideAnimatedProCTA +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideAvatarPickerOptions +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideClearDataDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideSimpleDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideUrlDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.HideUsernameDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.OnAvatarDialogDismissed +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.OnDonateClicked +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.RemoveAvatar +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.SaveAvatar +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.SetUsername +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ShowAnimatedProCTA +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ShowAvatarDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ShowClearDataDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ShowUrlDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.ShowUsernameDialog +import org.thoughtcrime.securesms.preferences.SettingsViewModel.Commands.UpdateUsername import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsActivity -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.ProDataState import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity import org.thoughtcrime.securesms.tokenpage.TokenPageActivity import org.thoughtcrime.securesms.ui.AccountIdHeader @@ -131,8 +147,6 @@ import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUIElement import org.thoughtcrime.securesms.util.State import org.thoughtcrime.securesms.util.push -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -266,11 +280,11 @@ fun Settings( }, text = uiState.username, iconSize = 53.sp to 24.sp, - content = if(uiState.subscriptionState.type !is ProStatus.NeverSubscribed){{ // if we are pro or expired + content = if(uiState.proDataState.type !is ProStatus.NeverSubscribed){{ // if we are pro or expired ProBadge( modifier = Modifier.padding(start = 4.dp) .qaTag(stringResource(R.string.qa_pro_badge_icon)), - colors = if(uiState.subscriptionState.type is ProStatus.Active) + colors = if(uiState.proDataState.type is ProStatus.Active) proBadgeColorStandard() else proBadgeColorDisabled() ) @@ -302,7 +316,7 @@ fun Settings( recoveryHidden = uiState.recoveryHidden, hasPaths = uiState.hasPath, postPro = uiState.isPostPro, - subscriptionState = uiState.subscriptionState, + proDataState = uiState.proDataState, sendCommand = sendCommand ) @@ -384,7 +398,7 @@ fun Settings( if(uiState.showAvatarDialog) { AvatarDialog( state = uiState.avatarDialogState, - isPro = uiState.subscriptionState.type is ProStatus.Active, + isPro = uiState.proDataState.type is ProStatus.Active, isPostPro = uiState.isPostPro, sendCommand = sendCommand, startAvatarSelection = startAvatarSelection @@ -394,7 +408,7 @@ fun Settings( // Animated avatar CTA if(uiState.showAnimatedProCTA){ AnimatedProCTA( - proSubscription = uiState.subscriptionState.type, + proSubscription = uiState.proDataState.type, sendCommand = sendCommand ) } @@ -481,7 +495,7 @@ fun Buttons( recoveryHidden: Boolean, hasPaths: Boolean, postPro: Boolean, - subscriptionState: SubscriptionState, + proDataState: ProDataState, sendCommand: (SettingsViewModel.Commands) -> Unit, ) { Column( @@ -526,7 +540,7 @@ fun Buttons( if(postPro){ ItemButton( text = annotatedStringResource( - when (subscriptionState.type) { + when (proDataState.type) { is ProStatus.Active -> Phrase.from( LocalContext.current, R.string.sessionProBeta @@ -1062,19 +1076,8 @@ private fun SettingsScreenPreview() { ) ), isPostPro = true, - subscriptionState = SubscriptionState( - type = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + proDataState = ProDataState( + type = previewAutoRenewingApple, refreshState = State.Success(Unit), showProBadge = true ), diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index 6c0400e3fd..bd491e3091 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -52,7 +52,7 @@ import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.ProDataState import org.thoughtcrime.securesms.pro.getDefaultSubscriptionStateData import org.thoughtcrime.securesms.reviews.InAppReviewManager import org.thoughtcrime.securesms.ui.SimpleDialogData @@ -96,7 +96,7 @@ class SettingsViewModel @Inject constructor( version = getVersionNumber(), recoveryHidden = prefs.getHidePassword(), isPostPro = proStatusManager.isPostPro(), - subscriptionState = getDefaultSubscriptionStateData(), + proDataState = getDefaultSubscriptionStateData(), )) val uiState: StateFlow get() = _uiState @@ -116,8 +116,8 @@ class SettingsViewModel @Inject constructor( // observe subscription status viewModelScope.launch { - proStatusManager.subscriptionState.collect { state -> - _uiState.update { it.copy(subscriptionState = state) } + proStatusManager.proDataState.collect { state -> + _uiState.update { it.copy(proDataState = state) } } } @@ -651,7 +651,7 @@ class SettingsViewModel @Inject constructor( val usernameDialog: UsernameDialogData? = null, val showSimpleDialog: SimpleDialogData? = null, val isPostPro: Boolean, - val subscriptionState: SubscriptionState, + val proDataState: ProDataState, ) sealed interface Commands { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt index e8f190426f..0cee7d8c4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanNonOriginating.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import com.squareup.phrase.Phrase import network.loki.messenger.R +import network.loki.messenger.libsession_util.protocol.PaymentProviderMetadata import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY @@ -17,7 +18,8 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_ACC import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog -import org.thoughtcrime.securesms.pro.SubscriptionDetails +import org.thoughtcrime.securesms.pro.getPlatformDisplayName +import org.thoughtcrime.securesms.pro.previewAppleMetaData import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors @@ -25,7 +27,7 @@ import org.thoughtcrime.securesms.ui.theme.ThemeColors @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable fun CancelPlanNonOriginating( - subscriptionDetails: SubscriptionDetails, + providerData: PaymentProviderMetadata, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ){ @@ -38,28 +40,28 @@ fun CancelPlanNonOriginating( .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), buttonText = Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, subscriptionDetails.getPlatformDisplayName()) + .put(PLATFORM_KEY, providerData.getPlatformDisplayName()) .format().toString(), dangerButton = true, onButtonClick = { - sendCommand(ShowOpenUrlDialog(subscriptionDetails.subscriptionUrl)) + sendCommand(ShowOpenUrlDialog(providerData.cancelSubscriptionUrl)) }, contentTitle = stringResource(R.string.proCancellation), contentDescription = Phrase.from(context.getText(R.string.proCancellationDescription)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(PLATFORM_ACCOUNT_KEY, subscriptionDetails.platformAccount) + .put(PLATFORM_ACCOUNT_KEY, providerData.platformAccount) .format(), linkCellsInfo = stringResource(R.string.proCancellationOptions), linkCells = listOf( NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onDevice)) - .put(DEVICE_TYPE_KEY, subscriptionDetails.device) + .put(DEVICE_TYPE_KEY, providerData.device) .format(), info = Phrase.from(context.getText(R.string.onDeviceCancelDescription)) .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) - .put(DEVICE_TYPE_KEY, subscriptionDetails.device) - .put(PLATFORM_ACCOUNT_KEY, subscriptionDetails.platformAccount) + .put(DEVICE_TYPE_KEY, providerData.device) + .put(PLATFORM_ACCOUNT_KEY, providerData.platformAccount) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), @@ -67,11 +69,11 @@ fun CancelPlanNonOriginating( ), NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onPlatformWebsite)) - .put(PLATFORM_KEY, subscriptionDetails.getPlatformDisplayName()) + .put(PLATFORM_KEY, providerData.getPlatformDisplayName()) .format(), info = Phrase.from(context.getText(R.string.requestRefundPlatformWebsite)) - .put(PLATFORM_KEY, subscriptionDetails.getPlatformDisplayName()) - .put(PLATFORM_ACCOUNT_KEY, subscriptionDetails.platformAccount) + .put(PLATFORM_KEY, providerData.getPlatformDisplayName()) + .put(PLATFORM_ACCOUNT_KEY, providerData.platformAccount) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), iconRes = R.drawable.ic_globe @@ -88,14 +90,7 @@ private fun PreviewUpdatePlan( PreviewTheme(colors) { val context = LocalContext.current CancelPlanNonOriginating ( - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ), + providerData = previewAppleMetaData, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanScreen.kt index 9a03c95a64..8e1349854a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/CancelPlanScreen.kt @@ -19,7 +19,8 @@ import network.loki.messenger.R import org.session.libsession.utilities.NonTranslatableStringConstants import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY -import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.OpenSubscriptionPage +import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.OpenCancelSubscriptionPage +import org.thoughtcrime.securesms.pro.isFromAnotherPlatform import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -48,10 +49,10 @@ fun CancelPlanScreen( when { // there is an active subscription but from a different platform or from the // same platform but a different account - activePlan.subscriptionDetails.isFromAnotherPlatform() + activePlan.providerData.isFromAnotherPlatform() || !planData.hasValidSubscription -> CancelPlanNonOriginating( - subscriptionDetails = activePlan.subscriptionDetails, + providerData = activePlan.providerData, sendCommand = viewModel::onCommand, onBack = onBack, ) @@ -81,7 +82,7 @@ fun CancelPlan( .format().toString(), dangerButton = true, onButtonClick = { - sendCommand(OpenSubscriptionPage) + sendCommand(OpenCancelSubscriptionPage) }, title = Phrase.from(context.getText(R.string.proCancelSorry)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt index 88514a2058..c8d596e956 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/PlanConfirmationScreen.kt @@ -37,10 +37,10 @@ import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NETWORK_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.ProDataState import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple +import org.thoughtcrime.securesms.pro.previewExpiredApple import org.thoughtcrime.securesms.ui.SessionProSettingsHeader import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.annotatedStringResource @@ -52,8 +52,6 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.util.State -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalSharedTransitionApi::class) @@ -115,7 +113,7 @@ fun PlanConfirmation( Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) - val description = when (proData.subscriptionState.type) { + val description = when (proData.proDataState.type) { is ProStatus.Active -> { Phrase.from(context.getText(R.string.proAllSetDescription)) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) @@ -150,7 +148,7 @@ fun PlanConfirmation( Spacer(Modifier.height(LocalDimensions.current.spacing)) - val buttonLabel = when (proData.subscriptionState.type) { + val buttonLabel = when (proData.proDataState.type) { is ProStatus.Active -> stringResource(R.string.theReturn) else -> { @@ -185,19 +183,8 @@ private fun PreviewPlanConfirmationActive( PlanConfirmation( proData = ProSettingsViewModel.ProSettingsState( subscriptionExpiryDate = "20th June 2026", - subscriptionState = SubscriptionState( - type = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + proDataState = ProDataState( + type = previewAutoRenewingApple, refreshState = State.Success(Unit), showProBadge = false, ), @@ -216,17 +203,8 @@ private fun PreviewPlanConfirmationExpired( PreviewTheme(colors) { PlanConfirmation( proData = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - )), + proDataState = ProDataState( + type = previewExpiredApple, refreshState = State.Success(Unit), showProBadge = true, ), @@ -245,7 +223,7 @@ private fun PreviewPlanConfirmationNeverSub( PreviewTheme(colors) { PlanConfirmation( proData = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( + proDataState = ProDataState( type = ProStatus.NeverSubscribed, refreshState = State.Success(Unit), showProBadge = true, 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 2742b8c4a1..73b74cbb39 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 @@ -61,11 +61,11 @@ import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.C import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.OnProStatsClicked import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.SetShowProBadge import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog -import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.ProDataState import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.ProStatusManager +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple +import org.thoughtcrime.securesms.pro.previewExpiredApple import org.thoughtcrime.securesms.ui.ActionRowItem import org.thoughtcrime.securesms.ui.CategoryCell import org.thoughtcrime.securesms.ui.Divider @@ -96,8 +96,6 @@ import org.thoughtcrime.securesms.ui.theme.primaryRed import org.thoughtcrime.securesms.ui.theme.primaryYellow import org.thoughtcrime.securesms.util.NumberUtil import org.thoughtcrime.securesms.util.State -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalSharedTransitionApi::class) @@ -125,7 +123,7 @@ fun ProSettingsHome( sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ) { - val subscriptionType = data.subscriptionState.type + val subscriptionType = data.proDataState.type val context = LocalContext.current val expiredInMainScreen = subscriptionType is ProStatus.Expired && !inSheet @@ -137,13 +135,13 @@ fun ProSettingsHome( onBack = onBack, onHeaderClick = { // add a click handling if the subscription state is loading or errored - if(data.subscriptionState.refreshState !is State.Success<*>){ + if(data.proDataState.refreshState !is State.Success<*>){ sendCommand(OnHeaderClicked(inSheet)) } else null }, extraHeaderContent = { // display extra content if the subscription state is loading or errored - when(data.subscriptionState.refreshState){ + when(data.proDataState.refreshState){ is State.Loading -> { Row( verticalAlignment = Alignment.CenterVertically, @@ -199,7 +197,7 @@ fun ProSettingsHome( ) { // Header for non-pro users or expired users in sheet mode if(subscriptionType is ProStatus.NeverSubscribed || expiredInSheet) { - if(data.subscriptionState.refreshState !is State.Success){ + if(data.proDataState.refreshState !is State.Success){ Spacer(Modifier.height(LocalDimensions.current.contentSpacing)) } @@ -219,7 +217,7 @@ fun ProSettingsHome( Spacer(Modifier.height(LocalDimensions.current.spacing)) Box { - val enableButon = data.subscriptionState.refreshState is State.Success + val enableButon = data.proDataState.refreshState is State.Success AccentFillButtonRect( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.theContinue), @@ -261,8 +259,8 @@ fun ProSettingsHome( if(subscriptionType is ProStatus.Active){ Spacer(Modifier.height(LocalDimensions.current.smallSpacing)) ProSettings( - showProBadge = data.subscriptionState.showProBadge, - subscriptionRefreshState = data.subscriptionState.refreshState, + showProBadge = data.proDataState.showProBadge, + subscriptionRefreshState = data.proDataState.refreshState, inSheet = inSheet, expiry = data.subscriptionExpiryLabel, sendCommand = sendCommand, @@ -274,7 +272,7 @@ fun ProSettingsHome( Spacer(Modifier.height(LocalDimensions.current.spacing)) ProManage( data = subscriptionType, - subscriptionRefreshState = data.subscriptionState.refreshState, + subscriptionRefreshState = data.proDataState.refreshState, inSheet = inSheet, sendCommand = sendCommand, ) @@ -292,7 +290,7 @@ fun ProSettingsHome( if(!inSheet){ ProSettingsFooter( proStatus = subscriptionType, - subscriptionRefreshState = data.subscriptionState.refreshState, + subscriptionRefreshState = data.proDataState.refreshState, inSheet = inSheet, sendCommand = sendCommand ) @@ -973,19 +971,8 @@ fun PreviewProSettingsPro( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + proDataState = ProDataState( + type = previewAutoRenewingApple, refreshState = State.Success(Unit), showProBadge = true, ), @@ -1005,19 +992,8 @@ fun PreviewProSettingsProLoading( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + proDataState = ProDataState( + type = previewAutoRenewingApple, refreshState = State.Loading, showProBadge = true, ), @@ -1037,19 +1013,8 @@ fun PreviewProSettingsProError( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + proDataState = ProDataState( + type = previewAutoRenewingApple, refreshState = State.Error(Exception()), showProBadge = true, ), @@ -1069,17 +1034,8 @@ fun PreviewProSettingsExpired( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - )), + proDataState = ProDataState( + type = previewExpiredApple, refreshState = State.Success(Unit), showProBadge = true, ) @@ -1099,17 +1055,8 @@ fun PreviewProSettingsExpiredInSheet( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - )), + proDataState = ProDataState( + type = previewExpiredApple, refreshState = State.Success(Unit), showProBadge = true, ) @@ -1129,17 +1076,8 @@ fun PreviewProSettingsExpiredLoading( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - )), + proDataState = ProDataState( + type = previewExpiredApple, refreshState = State.Loading, showProBadge = true, ) @@ -1159,17 +1097,8 @@ fun PreviewProSettingsExpiredError( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( - type = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - )), + proDataState = ProDataState( + type = previewExpiredApple, refreshState = State.Error(Exception()), showProBadge = true, ) @@ -1189,7 +1118,7 @@ fun PreviewProSettingsNonPro( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( + proDataState = ProDataState( type = ProStatus.NeverSubscribed, refreshState = State.Success(Unit), showProBadge = true, @@ -1210,7 +1139,7 @@ fun PreviewProSettingsNonProInSheet( PreviewTheme(colors) { ProSettingsHome( data = ProSettingsViewModel.ProSettingsState( - subscriptionState = SubscriptionState( + proDataState = ProDataState( type = ProStatus.NeverSubscribed, refreshState = State.Success(Unit), showProBadge = true, 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 a033fd2a18..2a254640cd 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 @@ -34,11 +34,13 @@ 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.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog -import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.pro.SubscriptionState +import org.thoughtcrime.securesms.pro.ProDataState import org.thoughtcrime.securesms.pro.ProStatus +import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.getDefaultSubscriptionStateData +import org.thoughtcrime.securesms.pro.isFromAnotherPlatform import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration import org.thoughtcrime.securesms.pro.subscription.SubscriptionCoordinator import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager @@ -58,7 +60,8 @@ class ProSettingsViewModel @AssistedInject constructor( @param:ApplicationContext private val context: Context, private val proStatusManager: ProStatusManager, private val subscriptionCoordinator: SubscriptionCoordinator, - private val dateUtils: DateUtils + private val dateUtils: DateUtils, + private val prefs: TextSecurePreferences, ) : ViewModel() { @AssistedFactory @@ -84,7 +87,7 @@ class ProSettingsViewModel @AssistedInject constructor( init { // observe subscription status viewModelScope.launch { - proStatusManager.subscriptionState.collect { + proStatusManager.proDataState.collect { generateState(it) } } @@ -120,7 +123,7 @@ class ProSettingsViewModel @AssistedInject constructor( // 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) { + when(_proSettingsUIState.value.proDataState.type) { is ProStatus.Active -> R.string.proUpdatingAction is ProStatus.Expired -> R.string.proRenewingAction else -> R.string.proUpgradingAction @@ -139,7 +142,8 @@ class ProSettingsViewModel @AssistedInject constructor( positiveStyleDanger = false, showXIcon = true, onPositive = { - getPlanFromProvider() // retry getting the plan from provider + //todo PRO I shouldn't get the plan from the provider again. I should really only try to redo the backend call + //getPlanFromProvider() // retry getting the plan from provider }, onNegative = { onCommand(ShowOpenUrlDialog(ProStatusManager.URL_PRO_SUPPORT)) @@ -157,14 +161,14 @@ class ProSettingsViewModel @AssistedInject constructor( } } - private fun generateState(subscriptionState: SubscriptionState){ + private fun generateState(proDataState: ProDataState){ //todo PRO need to properly calculate this - val subType = subscriptionState.type + val subType = proDataState.type _proSettingsUIState.update { ProSettingsState( - subscriptionState = subscriptionState, + proDataState = proDataState, subscriptionExpiryLabel = when(subType){ is ProStatus.Active.AutoRenewing -> Phrase.from(context, R.string.proAutoRenewTime) @@ -206,10 +210,10 @@ class ProSettingsViewModel @AssistedInject constructor( } is Commands.GoToChoosePlan -> { - when(_proSettingsUIState.value.subscriptionState.refreshState){ + when(_proSettingsUIState.value.proDataState.refreshState){ // if we are in a loading or refresh state we should show a dialog instead is State.Loading -> { - val state = _proSettingsUIState.value.subscriptionState.type + val state = _proSettingsUIState.value.proDataState.type val (title, message) = when{ state is ProStatus.Active -> Phrase.from(context.getText(R.string.proAccessLoading)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) @@ -245,7 +249,7 @@ class ProSettingsViewModel @AssistedInject constructor( } is State.Error -> { - val state = _proSettingsUIState.value.subscriptionState.type + val state = _proSettingsUIState.value.proDataState.type val (title, message) = when{ state is ProStatus.Active -> Phrase.from(context.getText(R.string.proAccessError)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) @@ -294,20 +298,22 @@ class ProSettingsViewModel @AssistedInject constructor( } Commands.GoToRefund -> { - val sub = _proSettingsUIState.value.subscriptionState.type + val sub = _proSettingsUIState.value.proDataState.type if(sub !is ProStatus.Active) return _refundPlanState.update { State.Loading } navigateTo(ProSettingsDestination.RefundSubscription) viewModelScope.launch { - val subManager = subscriptionCoordinator.getCurrentManager() _refundPlanState.update { + val isQuickRefund = if(prefs.getDebugIsWithinQuickRefund() && prefs.forceCurrentUserAsPro()) true // debug mode + else sub.isWithinQuickRefundWindow() + State.Success( RefundPlanState( proStatus = sub, - isQuickRefund = subManager.isWithinQuickRefundWindow(), - quickRefundUrl = subManager.quickRefundUrl + isQuickRefund = isQuickRefund, + quickRefundUrl = sub.providerData.refundUrl ) ) } @@ -315,7 +321,7 @@ class ProSettingsViewModel @AssistedInject constructor( } Commands.GoToCancel -> { - val sub = _proSettingsUIState.value.subscriptionState.type + val sub = _proSettingsUIState.value.proDataState.type if(sub !is ProStatus.Active) return // calculate state @@ -343,9 +349,9 @@ class ProSettingsViewModel @AssistedInject constructor( } } - Commands.OpenSubscriptionPage -> { - val subUrl = (_proSettingsUIState.value.subscriptionState.type as? ProStatus.Active) - ?.subscriptionDetails?.subscriptionUrl + Commands.OpenCancelSubscriptionPage -> { + val subUrl = (_proSettingsUIState.value.proDataState.type as? ProStatus.Active) + ?.providerData?.cancelSubscriptionUrl if(!subUrl.isNullOrEmpty()){ viewModelScope.launch { navigator.navigateToIntent( @@ -388,7 +394,7 @@ class ProSettingsViewModel @AssistedInject constructor( } Commands.GetProPlan -> { - val currentSubscription = _proSettingsUIState.value.subscriptionState.type + val currentSubscription = _proSettingsUIState.value.proDataState.type val selectedPlan = getSelectedPlan() ?: return if(currentSubscription is ProStatus.Active){ @@ -452,10 +458,10 @@ class ProSettingsViewModel @AssistedInject constructor( } is Commands.OnHeaderClicked -> { - when(_proSettingsUIState.value.subscriptionState.refreshState){ + when(_proSettingsUIState.value.proDataState.refreshState){ // if we are in a loading or refresh state we should show a dialog instead is State.Loading -> { - val state = _proSettingsUIState.value.subscriptionState.type + val state = _proSettingsUIState.value.proDataState.type val (title, message) = when{ state is ProStatus.Active -> Phrase.from(context.getText(R.string.proStatusLoading)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) @@ -491,7 +497,7 @@ class ProSettingsViewModel @AssistedInject constructor( is State.Error -> { _dialogState.update { - val state = _proSettingsUIState.value.subscriptionState.type + val state = _proSettingsUIState.value.proDataState.type val (title, message) = when{ state is ProStatus.Active -> Phrase.from(context.getText(R.string.proStatusError)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) @@ -578,7 +584,7 @@ class ProSettingsViewModel @AssistedInject constructor( // while the user is on the page we need to calculate the "choose plan" data viewModelScope.launch { - val subType = _proSettingsUIState.value.subscriptionState.type + val subType = _proSettingsUIState.value.proDataState.type // first check if the user has a valid subscription and billing val hasBillingCapacity = subscriptionCoordinator.getCurrentManager().supportsBilling.value @@ -589,7 +595,7 @@ class ProSettingsViewModel @AssistedInject constructor( // or the user is pro but non originating val noPriceNeeded = !hasBillingCapacity || (subType is ProStatus.Active && !hasValidSub) - || (subType is ProStatus.Active && subType.subscriptionDetails.isFromAnotherPlatform()) + || (subType is ProStatus.Active && subType.providerData.isFromAnotherPlatform()) val plans = if(noPriceNeeded) emptyList() else { @@ -770,7 +776,7 @@ class ProSettingsViewModel @AssistedInject constructor( object GoToCancel: Commands object OnPostPlanConfirmation: Commands - object OpenSubscriptionPage: Commands + object OpenCancelSubscriptionPage: Commands data class SetShowProBadge(val show: Boolean): Commands @@ -783,7 +789,7 @@ class ProSettingsViewModel @AssistedInject constructor( } data class ProSettingsState( - val subscriptionState: SubscriptionState = getDefaultSubscriptionStateData(), + val proDataState: ProDataState = getDefaultSubscriptionStateData(), val proStats: State = State.Loading, val subscriptionExpiryLabel: CharSequence = "", // eg: "Pro auto renewing in 3 days" val subscriptionExpiryDate: CharSequence = "", // eg: "May 21st, 2025" diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt index b95cc48f36..d2793ee74e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt @@ -19,13 +19,10 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_STO import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable @@ -41,41 +38,41 @@ fun RefundPlanNonOriginating( onBack = onBack, headerTitle = stringResource(R.string.proRefundDescription), buttonText = Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, subscription.subscriptionDetails.platform) + .put(PLATFORM_KEY, subscription.providerData.platform) .format().toString(), dangerButton = true, onButtonClick = { - sendCommand(ShowOpenUrlDialog(subscription.subscriptionDetails.refundUrl)) + sendCommand(ShowOpenUrlDialog(subscription.providerData.refundSupportUrl)) }, contentTitle = Phrase.from(context.getText(R.string.proRefunding)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), contentDescription = Phrase.from(context.getText(R.string.proPlanPlatformRefund)) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(PLATFORM_STORE_KEY, subscription.subscriptionDetails.store) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(PLATFORM_STORE_KEY, subscription.providerData.store) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .format(), linkCellsInfo = stringResource(R.string.refundRequestOptions), linkCells = listOf( NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onDevice)) - .put(DEVICE_TYPE_KEY, subscription.subscriptionDetails.device) + .put(DEVICE_TYPE_KEY, subscription.providerData.device) .format(), info = Phrase.from(context.getText(R.string.proRefundAccountDevice)) .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) - .put(DEVICE_TYPE_KEY, subscription.subscriptionDetails.device) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(DEVICE_TYPE_KEY, subscription.providerData.device) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) .format(), iconRes = R.drawable.ic_smartphone ), NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onPlatformWebsite)) - .put(PLATFORM_KEY, subscription.subscriptionDetails.platform) + .put(PLATFORM_KEY, subscription.providerData.platform) .format(), info = Phrase.from(context.getText(R.string.requestRefundPlatformWebsite)) - .put(PLATFORM_KEY, subscription.subscriptionDetails.platform) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(PLATFORM_KEY, subscription.providerData.platform) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), iconRes = R.drawable.ic_globe @@ -92,18 +89,7 @@ private fun PreviewUpdatePlan( PreviewTheme(colors) { val context = LocalContext.current RefundPlanNonOriginating ( - subscription = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + subscription = previewAutoRenewingApple, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt index 4011193e37..3738b73461 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt @@ -22,8 +22,8 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.isFromAnotherPlatform +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -32,8 +32,6 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.bold -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalSharedTransitionApi::class) @@ -53,7 +51,7 @@ fun RefundPlanScreen( // there are different UI depending on the state when { // there is an active subscription but from a different platform - activePlan.subscriptionDetails.isFromAnotherPlatform() -> + activePlan.providerData.isFromAnotherPlatform() -> RefundPlanNonOriginating( subscription = activePlan, sendCommand = viewModel::onCommand, @@ -87,7 +85,7 @@ fun RefundPlan( disabled = true, onBack = onBack, buttonText = if(isQuickRefund) Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, data.subscriptionDetails.platform) + .put(PLATFORM_KEY, data.providerData.platform) .format().toString() else stringResource(R.string.requestRefund), dangerButton = true, @@ -95,7 +93,7 @@ fun RefundPlan( if(isQuickRefund && !quickRefundUrl.isNullOrEmpty()){ sendCommand(ShowOpenUrlDialog(quickRefundUrl)) } else { - sendCommand(ShowOpenUrlDialog(data.subscriptionDetails.refundUrl)) + sendCommand(ShowOpenUrlDialog(data.providerData.refundSupportUrl)) } }, title = stringResource(R.string.proRefundDescription), @@ -115,7 +113,7 @@ fun RefundPlan( text = annotatedStringResource( if(isQuickRefund) Phrase.from(context.getText(R.string.proRefundRequestStorePolicies)) - .put(PLATFORM_KEY, data.subscriptionDetails.platform) + .put(PLATFORM_KEY, data.providerData.platform) .put(APP_NAME_KEY, context.getString(R.string.app_name)) .format() else Phrase.from(context.getText(R.string.proRefundRequestSessionSupport)) @@ -158,18 +156,7 @@ private fun PreviewRefundPlan( ) { PreviewTheme(colors) { RefundPlan( - data = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", - ) - ), + data = previewAutoRenewingApple, isQuickRefund = false, quickRefundUrl = "", sendCommand = {}, @@ -185,18 +172,7 @@ private fun PreviewQuickRefundPlan( ) { PreviewTheme(colors) { RefundPlan( - data = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", - ) - ), + data = previewAutoRenewingApple, isQuickRefund = true, quickRefundUrl = "", sendCommand = {}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanHomeScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanHomeScreen.kt index 30dd0ff6a8..4e8d1d3922 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanHomeScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanHomeScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue import org.thoughtcrime.securesms.preferences.prosettings.BaseStateProScreen import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel import org.thoughtcrime.securesms.pro.ProStatus +import org.thoughtcrime.securesms.pro.isFromAnotherPlatform @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -28,7 +29,7 @@ fun ChoosePlanHomeScreen( // there is an active subscription but from a different platform or from the // same platform but a different account // or we have no billing APIs - subscription.subscriptionDetails.isFromAnotherPlatform() + subscription.providerData.isFromAnotherPlatform() || !planData.hasValidSubscription || !planData.hasBillingCapacity -> ChoosePlanNonOriginating( diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt index 5c1f13f5c7..0c1a4be12b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNoBilling.kt @@ -24,15 +24,14 @@ import org.thoughtcrime.securesms.preferences.prosettings.BaseNonOriginatingProS import org.thoughtcrime.securesms.preferences.prosettings.NonOriginatingLinkCellData import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog -import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.pro.SubscriptionDetails import org.thoughtcrime.securesms.pro.ProStatus +import org.thoughtcrime.securesms.pro.ProStatusManager +import org.thoughtcrime.securesms.pro.getPlatformDisplayName +import org.thoughtcrime.securesms.pro.previewExpiredApple import org.thoughtcrime.securesms.ui.components.iconExternalLink import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable @@ -173,11 +172,11 @@ fun ChoosePlanNoBilling( add( NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onPlatformStoreWebsite)) - .put(PLATFORM_STORE_KEY, subscription.subscriptionDetails.getPlatformDisplayName()) + .put(PLATFORM_STORE_KEY, subscription.providerData.getPlatformDisplayName()) .format(), info = Phrase.from(context.getText(R.string.proAccessRenewPlatformWebsite)) - .put(PLATFORM_KEY, subscription.subscriptionDetails.getPlatformDisplayName()) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(PLATFORM_KEY, subscription.providerData.getPlatformDisplayName()) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), iconRes = R.drawable.ic_globe @@ -192,13 +191,13 @@ fun ChoosePlanNoBilling( onBack = onBack, headerTitle = headerTitle, buttonText = if(subscription is ProStatus.Expired) Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, subscription.subscriptionDetails.getPlatformDisplayName()) + .put(PLATFORM_KEY, subscription.providerData.getPlatformDisplayName()) .format().toString() else null, dangerButton = false, onButtonClick = { if(subscription is ProStatus.Expired) { - sendCommand(ShowOpenUrlDialog(subscription.subscriptionDetails.subscriptionUrl)) + sendCommand(ShowOpenUrlDialog(subscription.providerData.updateSubscriptionUrl)) } }, contentTitle = contentTitle, @@ -220,17 +219,7 @@ private fun PreviewNonOrigExpiredUpdatePlan( PreviewTheme(colors) { val context = LocalContext.current ChoosePlanNoBilling ( - subscription = ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + subscription = previewExpiredApple, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt index be88a097bf..e262b7a669 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanNonOriginating.kt @@ -24,15 +24,13 @@ import org.thoughtcrime.securesms.preferences.prosettings.NonOriginatingLinkCell import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.pro.SubscriptionDetails -import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import org.thoughtcrime.securesms.pro.getPlatformDisplayName +import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.pro.subscription.expiryFromNow import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.util.DateUtils -import java.time.Duration -import java.time.Instant @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable @@ -43,7 +41,7 @@ fun ChoosePlanNonOriginating( ){ val context = LocalContext.current - val platformOverride = subscription.subscriptionDetails.getPlatformDisplayName() + val platformOverride = subscription.providerData.getPlatformDisplayName() val headerTitle = when(subscription) { is ProStatus.Active.Expiring -> Phrase.from(context.getText(R.string.proAccessExpireDate)) @@ -73,15 +71,15 @@ fun ChoosePlanNonOriginating( .format().toString(), dangerButton = false, onButtonClick = { - sendCommand(ShowOpenUrlDialog(subscription.subscriptionDetails.subscriptionUrl)) + sendCommand(ShowOpenUrlDialog(subscription.providerData.updateSubscriptionUrl)) }, contentTitle = Phrase.from(LocalContext.current, R.string.updateAccess) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString(), contentDescription = Phrase.from(context.getText(R.string.proAccessSignUp)) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(PLATFORM_STORE_KEY, subscription.subscriptionDetails.store) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(PLATFORM_STORE_KEY, subscription.providerData.store) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), linkCellsInfo = Phrase.from(context.getText(R.string.updateAccessTwo)) @@ -90,12 +88,12 @@ fun ChoosePlanNonOriginating( linkCells = listOf( NonOriginatingLinkCellData( title = Phrase.from(context.getText(R.string.onDevice)) - .put(DEVICE_TYPE_KEY, subscription.subscriptionDetails.device) + .put(DEVICE_TYPE_KEY, subscription.providerData.device) .format(), info = Phrase.from(context.getText(R.string.onDeviceDescription)) .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) - .put(DEVICE_TYPE_KEY, subscription.subscriptionDetails.device) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(DEVICE_TYPE_KEY, subscription.providerData.device) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), @@ -106,7 +104,7 @@ fun ChoosePlanNonOriginating( .put(PLATFORM_KEY, platformOverride) .format(), info = Phrase.from(context.getText(R.string.viaStoreWebsiteDescription)) - .put(PLATFORM_ACCOUNT_KEY, subscription.subscriptionDetails.platformAccount) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .put(PLATFORM_STORE_KEY, platformOverride) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format(), @@ -124,18 +122,7 @@ private fun PreviewUpdatePlan( PreviewTheme(colors) { val context = LocalContext.current ChoosePlanNonOriginating ( - subscription = ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + subscription = previewAutoRenewingApple, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProDataMapper.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProDataMapper.kt new file mode 100644 index 0000000000..c7e16f622e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProDataMapper.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.pro + +import network.loki.messenger.libsession_util.pro.BackendRequests +import network.loki.messenger.libsession_util.pro.BackendRequests.PAYMENT_PROVIDER_APP_STORE +import network.loki.messenger.libsession_util.pro.BackendRequests.PAYMENT_PROVIDER_GOOGLE_PLAY +import network.loki.messenger.libsession_util.pro.PaymentProvider +import network.loki.messenger.libsession_util.protocol.PaymentProviderMetadata +import org.thoughtcrime.securesms.pro.api.ServerPlanDuration +import org.thoughtcrime.securesms.pro.api.ProDetails +import org.thoughtcrime.securesms.pro.api.ProDetails.Companion.SERVER_PLAN_DURATION_12_MONTH +import org.thoughtcrime.securesms.pro.api.ProDetails.Companion.SERVER_PLAN_DURATION_3_MONTH +import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration +import java.time.Duration +import java.time.Instant + +fun ProDetails.toProStatus(): ProStatus { + return when (status) { + ProDetails.DETAILS_STATUS_ACTIVE -> { + val paymentItem = paymentItems.first() + + if (autoRenewing == true) { + ProStatus.Active.AutoRenewing( + validUntil = expiry!!, + duration = paymentItem.planDuration.toSubscriptionDuration(), + providerData = paymentItem.paymentProvider.getMetadata(), + quickRefundExpiry = paymentItem.platformExpiry + ) + } else { + ProStatus.Active.Expiring( + validUntil = expiry!!, + duration = paymentItem.planDuration.toSubscriptionDuration(), + providerData = paymentItem.paymentProvider.getMetadata(), + quickRefundExpiry = paymentItem.platformExpiry + ) + } + } + + ProDetails.DETAILS_STATUS_EXPIRED -> ProStatus.Expired( + expiredAt = expiry!!, + providerData = paymentItems.first().paymentProvider.getMetadata() + ) + + else -> ProStatus.NeverSubscribed + } +} + +fun PaymentProvider.getMetadata(): PaymentProviderMetadata{ + return when(this){ + PAYMENT_PROVIDER_APP_STORE -> BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_APP_STORE)!! + else -> BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!! + } +} + +fun ServerPlanDuration.toSubscriptionDuration(): ProSubscriptionDuration { + return when(this){ + SERVER_PLAN_DURATION_12_MONTH -> ProSubscriptionDuration.TWELVE_MONTHS + SERVER_PLAN_DURATION_3_MONTH -> ProSubscriptionDuration.THREE_MONTHS + else -> ProSubscriptionDuration.ONE_MONTH + } +} + +fun PaymentProviderMetadata.isFromAnotherPlatform(): Boolean { + return platform.trim().lowercase() != "google" +} + +/** + * Some UI cases require a special display name for the platform. + */ +fun PaymentProviderMetadata.getPlatformDisplayName(): String { + return when(platform.trim().lowercase()){ + "google" -> store + else -> platform + } +} + + +/** + * Preview Data - Reusable data for composable previews + */ + +val previewAppleMetaData = PaymentProviderMetadata( + device = "iOS", + store = "Apple App Store", + platform = "Apple", + platformAccount = "Apple Account", + updateSubscriptionUrl = "https://www.apple.com/account/subscriptions", + cancelSubscriptionUrl = "https://www.apple.com/account/subscriptions", + refundUrl = "https://www.apple.com/account/subscriptions", + refundSupportUrl = "https://www.apple.com/account/subscriptions", + refundAfterPlatformDeadlineUrl = "https://www.apple.com/account/subscriptions" +) + +val previewAutoRenewingApple = ProStatus.Active.AutoRenewing( + validUntil = Instant.now() + Duration.ofDays(14), + duration = ProSubscriptionDuration.THREE_MONTHS, + providerData = previewAppleMetaData, + quickRefundExpiry = Instant.now() + Duration.ofDays(14) +) + +val previewExpiredApple = ProStatus.Expired( + expiredAt = Instant.now() - Duration.ofDays(14), + providerData = previewAppleMetaData +) + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatus.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatus.kt index 008edc44b9..5060e5e96a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatus.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatus.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.pro +import network.loki.messenger.libsession_util.protocol.PaymentProviderMetadata import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration import org.thoughtcrime.securesms.util.State import java.time.Instant @@ -10,58 +11,43 @@ sealed interface ProStatus{ sealed interface Active: ProStatus{ val validUntil: Instant val duration: ProSubscriptionDuration - val subscriptionDetails: SubscriptionDetails + val providerData: PaymentProviderMetadata + val quickRefundExpiry: Instant? data class AutoRenewing( override val validUntil: Instant, override val duration: ProSubscriptionDuration, - override val subscriptionDetails: SubscriptionDetails + override val providerData: PaymentProviderMetadata, + override val quickRefundExpiry: Instant? ): Active data class Expiring( override val validUntil: Instant, override val duration: ProSubscriptionDuration, - override val subscriptionDetails: SubscriptionDetails + override val providerData: PaymentProviderMetadata, + override val quickRefundExpiry: Instant? ): Active + fun isWithinQuickRefundWindow(): Boolean { + return quickRefundExpiry != null && quickRefundExpiry!!.isAfter(Instant.now()) + } + + } data class Expired( val expiredAt: Instant, - val subscriptionDetails: SubscriptionDetails + val providerData: PaymentProviderMetadata ): ProStatus } -data class SubscriptionState( +data class ProDataState( val type: ProStatus, val showProBadge: Boolean, val refreshState: State, ) -data class SubscriptionDetails( - val device: String, - val store: String, - val platform: String, - val platformAccount: String, - val subscriptionUrl: String, - val refundUrl: String, -){ - fun isFromAnotherPlatform(): Boolean { - return platform.trim().lowercase() != "google" - } - - /** - * Some UI cases require a special display name for the platform. - */ - fun getPlatformDisplayName(): String { - return when(platform.trim().lowercase()){ - "google" -> store - else -> platform - } - } -} - -fun getDefaultSubscriptionStateData() = SubscriptionState( +fun getDefaultSubscriptionStateData() = ProDataState( type = ProStatus.NeverSubscribed, refreshState = State.Loading, showProBadge = false 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 cc17f676f0..55c2cbb49f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -17,15 +18,26 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import network.loki.messenger.libsession_util.pro.BackendRequests +import network.loki.messenger.libsession_util.pro.BackendRequests.PAYMENT_PROVIDER_APP_STORE +import network.loki.messenger.libsession_util.pro.BackendRequests.PAYMENT_PROVIDER_GOOGLE_PLAY import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.LoginStateRepository import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.debugmenu.DebugLogGroup import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent +import org.thoughtcrime.securesms.pro.api.AddPaymentErrorStatus +import org.thoughtcrime.securesms.pro.api.AddProPaymentRequest +import org.thoughtcrime.securesms.pro.api.ProApiExecutor +import org.thoughtcrime.securesms.pro.api.ProApiResponse +import org.thoughtcrime.securesms.pro.db.ProDatabase import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager import org.thoughtcrime.securesms.util.State @@ -40,10 +52,13 @@ class ProStatusManager @Inject constructor( private val prefs: TextSecurePreferences, recipientRepository: RecipientRepository, @param:ManagerScope private val scope: CoroutineScope, - loginStateRepository: LoginStateRepository, + private val apiExecutor: ProApiExecutor, + private val loginState: LoginStateRepository, + private val proDatabase: ProDatabase, + private val snodeClock: SnodeClock, ) : OnAppStartupComponent { - val subscriptionState: StateFlow = combine( + val proDataState: StateFlow = combine( recipientRepository.observeSelf(), (TextSecurePreferences.events.filter { it == TextSecurePreferences.DEBUG_SUBSCRIPTION_STATUS } as Flow<*>) .onStart { emit(Unit) } @@ -55,128 +70,80 @@ class ProStatusManager @Inject constructor( .onStart { emit(Unit) } .map { prefs.forceCurrentUserAsPro() }, ){ selfRecipient, debugSubscription, debugProPlanStatus, forceCurrentUserAsPro -> - //todo PRO implement properly - - val subscriptionState = debugSubscription ?: DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE - val proDataStatus = when(debugProPlanStatus){ + val proDataRefreshState = when(debugProPlanStatus){ DebugMenuViewModel.DebugProPlanStatus.LOADING -> State.Loading DebugMenuViewModel.DebugProPlanStatus.ERROR -> State.Error(Exception()) else -> State.Success(Unit) } if(!forceCurrentUserAsPro){ + Log.d(DebugLogGroup.PRO_DATA.label, "ProStatusManager: Getting REAL Pro data state") //todo PRO this is where we should get the real state - SubscriptionState( + ProDataState( type = ProStatus.NeverSubscribed, showProBadge = selfRecipient.shouldShowProBadge, - refreshState = proDataStatus + refreshState = proDataRefreshState ) - } - else SubscriptionState( - type = when(subscriptionState){ - DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE -> ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.THREE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", + }// debug data + else { + Log.d(DebugLogGroup.PRO_DATA.label, "ProStatusManager: Getting DEBUG Pro data state") + val subscriptionState = debugSubscription ?: DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE + + ProDataState( + type = when(subscriptionState){ + DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE -> ProStatus.Active.AutoRenewing( + validUntil = Instant.now() + Duration.ofDays(14), + duration = ProSubscriptionDuration.THREE_MONTHS, + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!!, + quickRefundExpiry = Instant.now() + Duration.ofDays(7) ) - ) - - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE -> ProStatus.Active.Expiring( - validUntil = Instant.now() + Duration.ofDays(2), - duration = ProSubscriptionDuration.TWELVE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE -> ProStatus.Active.Expiring( + validUntil = Instant.now() + Duration.ofDays(2), + duration = ProSubscriptionDuration.TWELVE_MONTHS, + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!!, + quickRefundExpiry = Instant.now() + Duration.ofDays(7) ) - ) - - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE_LATER -> ProStatus.Active.Expiring( - validUntil = Instant.now() + Duration.ofDays(40), - duration = ProSubscriptionDuration.TWELVE_MONTHS, - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_GOOGLE_LATER -> ProStatus.Active.Expiring( + validUntil = Instant.now() + Duration.ofDays(40), + duration = ProSubscriptionDuration.TWELVE_MONTHS, + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!!, + quickRefundExpiry = Instant.now() + Duration.ofDays(7) ) - ) - - DebugMenuViewModel.DebugSubscriptionStatus.AUTO_APPLE -> ProStatus.Active.AutoRenewing( - validUntil = Instant.now() + Duration.ofDays(14), - duration = ProSubscriptionDuration.ONE_MONTH, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://support.apple.com/118223", + + DebugMenuViewModel.DebugSubscriptionStatus.AUTO_APPLE -> ProStatus.Active.AutoRenewing( + validUntil = Instant.now() + Duration.ofDays(14), + duration = ProSubscriptionDuration.ONE_MONTH, + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_APP_STORE)!!, + quickRefundExpiry = Instant.now() + Duration.ofDays(7) ) - ) - - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_APPLE -> ProStatus.Active.Expiring( - validUntil = Instant.now() + Duration.ofDays(2), - duration = ProSubscriptionDuration.ONE_MONTH, - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://support.apple.com/118223", + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRING_APPLE -> ProStatus.Active.Expiring( + validUntil = Instant.now() + Duration.ofDays(2), + duration = ProSubscriptionDuration.ONE_MONTH, + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_APP_STORE)!!, + quickRefundExpiry = Instant.now() + Duration.ofDays(7) ) - ) - - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED -> ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", + + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED -> ProStatus.Expired( + expiredAt = Instant.now() - Duration.ofDays(14), + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!! ) - ) - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_EARLIER -> ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(60), - subscriptionDetails = SubscriptionDetails( - device = "Android", - store = "Google Play Store", - platform = "Google", - platformAccount = "Google account", - subscriptionUrl = "https://play.google.com/store/account/subscriptions?package=network.loki.messenger&sku=SESSION_PRO_MONTHLY", - refundUrl = "https://getsession.org/android-refund", + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_EARLIER -> ProStatus.Expired( + expiredAt = Instant.now() - Duration.ofDays(60), + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_GOOGLE_PLAY)!! ) - ) - DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_APPLE -> ProStatus.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - subscriptionDetails = SubscriptionDetails( - device = "iOS", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://support.apple.com/118223", + DebugMenuViewModel.DebugSubscriptionStatus.EXPIRED_APPLE -> ProStatus.Expired( + expiredAt = Instant.now() - Duration.ofDays(14), + providerData = BackendRequests.getPaymentProviderMetadata(PAYMENT_PROVIDER_APP_STORE)!! ) - ) - }, + }, - refreshState = proDataStatus, - showProBadge = selfRecipient.shouldShowProBadge, - ) + refreshState = proDataRefreshState, + showProBadge = selfRecipient.shouldShowProBadge, + ) + } }.stateIn(scope, SharingStarted.Eagerly, initialValue = getDefaultSubscriptionStateData() @@ -267,43 +234,64 @@ class ProStatusManager @Inject constructor( return emptySet() } - suspend fun appProPaymentToBackend() { + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun appProPaymentToBackend(orderId: String, paymentId: String) { // max 3 attempts as per PRD val maxAttempts = 3 + // no point in going further if we have no key data + val keyData = loginState.loggedInState.value ?: throw Exception() + 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, - */ - - } + // 5s timeout as per PRD + val paymentResponse = withTimeout(5_000L) { + apiExecutor.executeRequest( + request = AddProPaymentRequest( + googlePaymentToken = paymentId, + googleOrderId = orderId, + masterPrivateKey = keyData.seeded.proMasterPrivateKey, + rotatingPrivateKey = proDatabase.ensureValidRotatingKeys(snodeClock.currentTime()).ed25519PrivKey + ) + ) + } + + when (paymentResponse) { + is ProApiResponse.Success -> { + Log.d(DebugLogGroup.PRO_SUBSCRIPTION.label, "Backend 'add pro payment' successful") + // Payment was successfully claimed - save it to the database + proDatabase.updateCurrentProProof(paymentResponse.data) + } + + is ProApiResponse.Failure -> { + // Handle payment failure + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Backend 'add pro payment' failure: $paymentResponse") + when (paymentResponse.status) { + // unknown payment is retryable - throw a generic exception here to go through our retries + AddPaymentErrorStatus.UnknownPayment -> { + throw Exception() + } + + // nothing to do if already redeemed + AddPaymentErrorStatus.AlreadyRedeemed -> { + return + } + + // non retryable error - throw our custom exception + AddPaymentErrorStatus.GenericError -> { + throw SubscriptionManager.PaymentServerException() + } + } + } + } } catch (e: CancellationException) { throw e - } catch (_: Exception) { + } catch (e: SubscriptionManager.PaymentServerException){ + // rethrow this error directly without retrying + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Backend 'add pro payment' PaymentServerException caught and rethrown") + throw e + }catch (e: Exception) { + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Backend 'add pro payment' exception", e) // If not the last attempt, backoff a little and retry if (attempt < maxAttempts) { // small incremental backoff before retry @@ -314,6 +302,7 @@ class ProStatusManager @Inject constructor( } // All attempts failed - throw our custom exception + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Backend 'add pro payment' - Al retries attempted, throwing our custom `PaymentServerException`") throw SubscriptionManager.PaymentServerException() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/AddProPayment.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/AddProPayment.kt index f50cf3440b..9918ee7f35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/AddProPayment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/AddProPayment.kt @@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.pro.api import kotlinx.serialization.DeserializationStrategy import network.loki.messenger.libsession_util.pro.BackendRequests import network.loki.messenger.libsession_util.pro.ProProof +import org.session.libsignal.utilities.Log class AddProPaymentRequest( private val googlePaymentToken: String, private val googleOrderId: String, private val masterPrivateKey: ByteArray, private val rotatingPrivateKey: ByteArray, -) : ApiRequest { +) : ApiRequest { override val endpoint: String get() = "add_pro_payment" @@ -24,9 +25,10 @@ class AddProPaymentRequest( ) } - override fun convertStatus(status: Int): AddPaymentStatus { - return AddPaymentStatus.entries.firstOrNull { it.apiValue == status } - ?: AddPaymentStatus.GenericError + override fun convertErrorStatus(status: Int): AddPaymentErrorStatus { + Log.w("", "AddProPayment: convertErrorStatus: $status") + return AddPaymentErrorStatus.entries.firstOrNull { it.apiValue == status } + ?: AddPaymentErrorStatus.GenericError } override val responseDeserializer: DeserializationStrategy @@ -34,9 +36,8 @@ class AddProPaymentRequest( } -enum class AddPaymentStatus(val apiValue: Int) { - Success(0), +enum class AddPaymentErrorStatus(val apiValue: Int) { GenericError(1), - AlreadyRedeemed(2), - UnknownPayment(3), + AlreadyRedeemed(100), + UnknownPayment(101), } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/ApiRequest.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/ApiRequest.kt index 28bd00428c..a6ffb030b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/ApiRequest.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/ApiRequest.kt @@ -1,26 +1,14 @@ package org.thoughtcrime.securesms.pro.api import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.decodeFromStream -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.utilities.await /** * Represents a generic API request to the Pro backend. * - * @param Status The type of the status returned by the API. + * @param ErrorStatus The type of error status returned by the API. * @param Res The type of the expected response. */ -interface ApiRequest { +interface ApiRequest { /** * The endpoint (path) for this API request, e.g. "v1/pro/payments" */ @@ -28,7 +16,7 @@ interface ApiRequest { val responseDeserializer: DeserializationStrategy - fun convertStatus(status: Int): Status + fun convertErrorStatus(status: Int): ErrorStatus fun buildJsonBody(): String } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GenerateProProof.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GenerateProProof.kt index 4b0eadb32a..399353c082 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GenerateProProof.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GenerateProProof.kt @@ -29,7 +29,7 @@ class GenerateProProofRequest @AssistedInject constructor( override val responseDeserializer: DeserializationStrategy get() = ProProof.serializer() - override fun convertStatus(status: Int): GetProProofStatus = status + override fun convertErrorStatus(status: Int): GetProProofStatus = status @AssistedFactory interface Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProDetails.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProDetails.kt index 10f7f5bb02..df386213dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProDetails.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProDetails.kt @@ -31,7 +31,7 @@ class GetProDetailsRequest @AssistedInject constructor( override val responseDeserializer: DeserializationStrategy get() = ProDetails.serializer() - override fun convertStatus(status: Int): Int = status + override fun convertErrorStatus(status: Int): Int = status @AssistedFactory interface Factory { @@ -39,11 +39,12 @@ class GetProDetailsRequest @AssistedInject constructor( } } -typealias ProDetailsStatus = Int +typealias ServerProDetailsStatus = Int +typealias ServerPlanDuration = Int @Serializable class ProDetails( - val status: ProDetailsStatus, + val status: ServerProDetailsStatus, @SerialName("auto_renewing") val autoRenewing: Boolean? = null, @@ -66,10 +67,17 @@ class ProDetails( val version: Int, ) { + init { + check((status != DETAILS_STATUS_ACTIVE && status != DETAILS_STATUS_EXPIRED) || expiry != null) { "Expiry must not be null for state other than 'never subscribed'" } + check((status != DETAILS_STATUS_ACTIVE && status != DETAILS_STATUS_EXPIRED) || paymentItems.isNotEmpty()) { "Can't have no payment items for state other than 'never subscribed'" } + } @Serializable data class Item( - val status: ProDetailsStatus, + @SerialName("plan") + val planDuration: ServerPlanDuration, + + val status: Int, // Payment status [Redeemed, Revoked, Expired] - we do not use this status in the clients @SerialName("payment_provider") val paymentProvider: PaymentProvider, @@ -79,7 +87,7 @@ class ProDetails( val expiry: Instant? = null, @SerialName("grace_duration_ms") - val graceDurationMs: Long, + val graceDurationMs: Long? = null, @SerialName("platform_refund_expiry_unix_ts_ms") @Serializable(with = InstantAsMillisSerializer::class) @@ -114,8 +122,12 @@ class ProDetails( ) companion object { - const val DETAILS_STATUS_NEVER_BEEN_PRO: ProDetailsStatus = 0 - const val DETAILS_STATUS_ACTIVE: ProDetailsStatus = 1 - const val DETAILS_STATUS_EXPIRED: ProDetailsStatus = 2 + const val DETAILS_STATUS_NEVER_BEEN_PRO: ServerProDetailsStatus = 0 + const val DETAILS_STATUS_ACTIVE: ServerProDetailsStatus = 1 + const val DETAILS_STATUS_EXPIRED: ServerProDetailsStatus = 2 + + const val SERVER_PLAN_DURATION_1_MONTH: ServerPlanDuration = 1 + const val SERVER_PLAN_DURATION_3_MONTH: ServerPlanDuration = 2 + const val SERVER_PLAN_DURATION_12_MONTH: ServerPlanDuration = 3 } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProRevocations.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProRevocations.kt index 2616e51258..a800df5fd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProRevocations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/GetProRevocations.kt @@ -17,7 +17,7 @@ class GetProRevocationRequest @AssistedInject constructor( override val responseDeserializer: DeserializationStrategy get() = ProRevocations.serializer() - override fun convertStatus(status: Int): Int = status + override fun convertErrorStatus(status: Int): Int = status override val endpoint: String get() = "get_pro_revocations" diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/api/ProApiExecutor.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/api/ProApiExecutor.kt index 3056715138..c0560ac600 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/api/ProApiExecutor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/api/ProApiExecutor.kt @@ -80,7 +80,7 @@ class ProApiExecutor @Inject constructor( ProApiResponse.Success(data) } else { ProApiResponse.Failure( - status = request.convertStatus(rawResp.status), + status = request.convertErrorStatus(rawResp.status), errors = rawResp.errors.orEmpty() ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/NoOpSubscriptionManager.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/NoOpSubscriptionManager.kt index 91de7bf5f0..7c91139d3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/NoOpSubscriptionManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/subscription/NoOpSubscriptionManager.kt @@ -25,8 +25,6 @@ class NoOpSubscriptionManager @Inject constructor( override val supportsBilling = MutableStateFlow(false) - override val quickRefundUrl = null - override suspend fun purchasePlan(subscriptionDuration: ProSubscriptionDuration): Result { return Result.success(Unit) } @@ -37,10 +35,6 @@ class NoOpSubscriptionManager @Inject constructor( return false } - override suspend fun isWithinQuickRefundWindow(): Boolean { - return false - } - override suspend fun getSubscriptionPrices(): List { return emptyList() } 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 f365e1ca3d..ea0d54469d 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 @@ -26,9 +26,6 @@ abstract class SubscriptionManager( abstract val supportsBilling: StateFlow - // Optional. Some store can have a platform specific refund window and url - abstract val quickRefundUrl: String? - abstract val availablePlans: List sealed interface PurchaseEvent { @@ -50,10 +47,6 @@ abstract class SubscriptionManager( abstract suspend fun purchasePlan(subscriptionDuration: ProSubscriptionDuration): Result - /** - * Returns true if a provider has a quick refunds and the current time since purchase is within that window - */ - abstract suspend fun isWithinQuickRefundWindow(): Boolean /** * Checks whether there is a valid subscription for the current user within this subscriber's billing API @@ -70,11 +63,11 @@ abstract class SubscriptionManager( /** * Function called when a purchased has been made successfully from the subscription api */ - protected fun onPurchaseSuccessful(){ + protected fun onPurchaseSuccessful(orderId: String, paymentId: String){ // we need to tie our purchase with the back end scope.launch { try { - proStatusManager.appProPaymentToBackend() + proStatusManager.appProPaymentToBackend(orderId, paymentId) _purchaseEvents.emit(PurchaseEvent.Success) } catch (e: Exception) { when (e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UserProfileUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UserProfileUtils.kt index f388053c21..c4df000fbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/UserProfileUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/UserProfileUtils.kt @@ -143,7 +143,7 @@ class UserProfileUtils @AssistedInject constructor( UserProfileModalCommands.ShowProCTA -> { _userProfileModalData.update { _userProfileModalData.value?.copy( - showProCTA = GenericCTAData(proStatusManager.subscriptionState.value.type) + showProCTA = GenericCTAData(proStatusManager.proDataState.value.type) ) } } 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 c2a2a7eb27..89f202b9f2 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 @@ -68,8 +68,6 @@ class PlayStoreSubscriptionManager @Inject constructor( } .stateIn(scope, SharingStarted.Eagerly, false) - override val quickRefundUrl = "https://support.google.com/googleplay/workflow/9813244" - private val billingClient by lazy { BillingClient.newBuilder(application) .setListener { result, purchases -> @@ -80,7 +78,10 @@ class PlayStoreSubscriptionManager @Inject constructor( Log.d(DebugLogGroup.PRO_SUBSCRIPTION.label, "Billing callback. We have a purchase [${it.orderId}]. Acknowledged? ${it.isAcknowledged}") - onPurchaseSuccessful() + onPurchaseSuccessful( + orderId = it.orderId ?: "", + paymentId = it.purchaseToken + ) } } else { Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Purchase failed or cancelled: $result") @@ -264,21 +265,6 @@ class PlayStoreSubscriptionManager @Inject constructor( else getExistingSubscription() != null } - override suspend fun isWithinQuickRefundWindow(): Boolean { - if(prefs.getDebugIsWithinQuickRefund() && prefs.forceCurrentUserAsPro()) return true // debug mode - - val purchaseTimeMillis = getExistingSubscription()?.purchaseTime ?: return false - - val now = Instant.now() - val purchaseInstant = Instant.ofEpochMilli(purchaseTimeMillis) - - // Google Play allows refunds within 48 hours of purchase - val refundWindowHours = 48 - val refundDeadline = purchaseInstant.plus(refundWindowHours.toLong(), ChronoUnit.HOURS) - - return now.isBefore(refundDeadline) - } - @Throws(Exception::class) override suspend fun getSubscriptionPrices(): List { val result = getProductDetails() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37ee66cb47..6cb0f65f91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ kotlinVersion = "2.2.21" kryoVersion = "5.6.2" kspVersion = "2.3.0" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.0.9-14-g2c548d4" +libsessionUtilAndroidVersion = "1.0.9-15-g01bceb9" media3ExoplayerVersion = "1.8.0" mockitoCoreVersion = "5.20.0" navVersion = "2.9.5"