diff --git a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 4dfe9bcb29..da8d6afb36 100644 --- a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -218,6 +218,8 @@ interface TextSecurePreferences { fun setDebugSubscriptionType(status: DebugMenuViewModel.DebugSubscriptionStatus?) fun getDebugProPlanStatus(): DebugMenuViewModel.DebugProPlanStatus? fun setDebugProPlanStatus(status: DebugMenuViewModel.DebugProPlanStatus?) + fun getDebugForceNoBilling(): Boolean + fun setDebugForceNoBilling(hasBilling: Boolean) fun setSubscriptionProvider(provider: String) fun getSubscriptionProvider(): String? @@ -380,6 +382,7 @@ interface TextSecurePreferences { const val DEBUG_MESSAGE_FEATURES = "debug_message_features" const val DEBUG_SUBSCRIPTION_STATUS = "debug_subscription_status" const val DEBUG_PRO_PLAN_STATUS = "debug_pro_plan_status" + const val DEBUG_FORCE_NO_BILLING = "debug_pro_has_billing" const val SUBSCRIPTION_PROVIDER = "session_subscription_provider" @@ -1780,6 +1783,14 @@ class AppTextSecurePreferences @Inject constructor( _events.tryEmit(TextSecurePreferences.DEBUG_PRO_PLAN_STATUS) } + override fun getDebugForceNoBilling(): Boolean { + return getBooleanPreference(TextSecurePreferences.DEBUG_FORCE_NO_BILLING, false) + } + + override fun setDebugForceNoBilling(hasBilling: Boolean) { + setBooleanPreference(TextSecurePreferences.DEBUG_FORCE_NO_BILLING, hasBilling) + } + override fun getSubscriptionProvider(): String? { return getStringPreference(TextSecurePreferences.SUBSCRIPTION_PROVIDER, null) } 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 c874e1df2d..ad8a6100e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -300,6 +300,15 @@ fun DebugMenu( } } + Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) + DebugSwitchRow( + text = "Force \"No Billing\" APIs", + checked = uiState.forceNoBilling, + onCheckedChange = { + sendCommand(DebugMenuViewModel.Commands.ForceNoBilling(it)) + } + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) DebugSwitchRow( text = "Set all incoming messages as Pro", @@ -773,6 +782,7 @@ fun PreviewDebugMenu() { debugProPlanStatus = setOf(DebugMenuViewModel.DebugProPlanStatus.NORMAL), selectedDebugProPlanStatus = DebugMenuViewModel.DebugProPlanStatus.NORMAL, debugProPlans = emptyList(), + forceNoBilling = false ), sendCommand = {}, onClose = {} 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 5dc37e077a..5fc01a9cdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -102,6 +102,7 @@ class DebugMenuViewModel @Inject constructor( debugProPlans = subscriptionManagers.asSequence() .flatMap { it.availablePlans.asSequence().map { plan -> DebugProPlan(it, plan) } } .toList(), + forceNoBilling = textSecurePreferences.getDebugForceNoBilling(), ) ) val uiState: StateFlow @@ -268,6 +269,13 @@ class DebugMenuViewModel @Inject constructor( } } + is Commands.ForceNoBilling -> { + textSecurePreferences.setDebugForceNoBilling(command.set) + _uiState.update { + it.copy(forceNoBilling = command.set) + } + } + is Commands.ForcePostPro -> { textSecurePreferences.setForcePostPro(command.set) _uiState.update { @@ -426,6 +434,7 @@ class DebugMenuViewModel @Inject constructor( val debugProPlanStatus: Set, val selectedDebugProPlanStatus: DebugProPlanStatus, val debugProPlans: List, + val forceNoBilling: Boolean, ) enum class DatabaseInspectorState { @@ -461,6 +470,7 @@ class DebugMenuViewModel @Inject constructor( data class ForceCurrentUserAsPro(val set: Boolean) : Commands() data class ForceOtherUsersAsPro(val set: Boolean) : Commands() data class ForceIncomingMessagesAsPro(val set: Boolean) : Commands() + data class ForceNoBilling(val set: Boolean) : Commands() data class ForcePostPro(val set: Boolean) : Commands() data class ForceShortTTl(val set: Boolean) : Commands() data class SetMessageProFeature(val feature: ProStatusManager.MessageProFeature, val set: Boolean) : Commands() 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 00a14c9ad2..6cd615cbf8 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 @@ -40,6 +40,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -66,6 +67,7 @@ import org.thoughtcrime.securesms.ui.IconActionRowItem import org.thoughtcrime.securesms.ui.ProBadgeText import org.thoughtcrime.securesms.ui.SpeechBubbleTooltip import org.thoughtcrime.securesms.ui.SwitchActionRowItem +import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.ExtraSmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.components.annotatedStringResource @@ -74,6 +76,7 @@ import org.thoughtcrime.securesms.ui.components.inlineContentMap import org.thoughtcrime.securesms.ui.proBadgeColorDisabled import org.thoughtcrime.securesms.ui.proBadgeColorStandard import org.thoughtcrime.securesms.ui.qaTag +import org.thoughtcrime.securesms.ui.shimmerOverlay import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -184,6 +187,26 @@ fun ProSettingsHome( } } ) { + // Header for non-pro users + if(subscriptionType is SubscriptionType.NeverSubscribed) { + Text( + text = Phrase.from(context.getText(R.string.proFullestPotential)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format().toString(), + style = LocalType.current.base, + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(LocalDimensions.current.spacing)) + + AccentFillButtonRect( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.theContinue), + onClick = { sendCommand(GoToChoosePlan) } + ) + } + // Pro Stats if(subscriptionType is SubscriptionType.Active){ Spacer(Modifier.height(LocalDimensions.current.spacing)) @@ -652,7 +675,7 @@ fun ProFeatures( // More... ProFeatureItem( - title = stringResource(R.string.proFeatureListLoadsMore), + title = stringResource(R.string.plusLoadsMore), subtitle = annotatedStringResource( text = Phrase.from(LocalContext.current.getText(R.string.plusLoadsMoreDescription)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) 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 1ddf166c35..bc16f1a34d 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 @@ -43,8 +43,6 @@ fun ChoosePlanNoBilling( ){ val context = LocalContext.current - //todo PRO cater for NEVER SUBSCRIBED here - val defaultGoogleStore = ProStatusManager.DEFAULT_GOOGLE_STORE val defaultAppleStore = ProStatusManager.DEFAULT_APPLE_STORE @@ -54,6 +52,10 @@ fun ChoosePlanNoBilling( .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) .format() + is SubscriptionType.NeverSubscribed -> Phrase.from(context.getText(R.string.proUpgradeAccess)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format() + else -> "" } @@ -61,6 +63,11 @@ fun ChoosePlanNoBilling( is SubscriptionType.Expired -> Phrase.from(context.getText(R.string.renewingPro)) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString() + + is SubscriptionType.NeverSubscribed-> Phrase.from(context.getText(R.string.proUpgradingTo)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format().toString() + else -> "" } @@ -82,11 +89,63 @@ fun ChoosePlanNoBilling( .put(ICON_KEY, iconExternalLink) .format() + is SubscriptionType.NeverSubscribed -> Phrase.from(context.getText(R.string.proUpgradeNoAccessBilling)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .put(PLATFORM_STORE_KEY, defaultGoogleStore) + .put(PLATFORM_STORE2_KEY, defaultAppleStore) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(BUILD_VARIANT_KEY, when (BuildConfig.FLAVOR) { + "fdroid" -> "F-Droid Store" + "huawei" -> "Huawei App Gallery" + else -> "APK" + }) + .put(ICON_KEY, iconExternalLink) + .format() + else -> "" } val cellsInfo = when(subscription) { is SubscriptionType.Expired -> stringResource(R.string.proOptionsRenewalSubtitle) + is SubscriptionType.NeverSubscribed -> stringResource(R.string.proUpgradeOptionsTwo) + else -> "" + } + + val cell1Text: CharSequence = when(subscription) { + is SubscriptionType.Expired -> Phrase.from(context.getText(R.string.proRenewDesktopLinked)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(PLATFORM_STORE_KEY, defaultGoogleStore) + .put(PLATFORM_STORE2_KEY, defaultAppleStore) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format() + + is SubscriptionType.NeverSubscribed -> Phrase.from(context.getText(R.string.proUpgradeDesktopLinked)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(PLATFORM_STORE_KEY, defaultGoogleStore) + .put(PLATFORM_STORE2_KEY, defaultAppleStore) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .format() + + else -> "" + } + + val cell2Text: CharSequence = when(subscription) { + is SubscriptionType.Expired -> Phrase.from(context.getText(R.string.proNewInstallationDescription)) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(PLATFORM_STORE_KEY, defaultGoogleStore) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format() + + is SubscriptionType.NeverSubscribed -> Phrase.from(context.getText(R.string.proNewInstallationUpgrade)) + .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) + .put(PLATFORM_STORE_KEY, defaultGoogleStore) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format() + else -> "" } @@ -95,13 +154,7 @@ fun ChoosePlanNoBilling( add( NonOriginatingLinkCellData( title = stringResource(R.string.onLinkedDevice), - info = Phrase.from(context.getText(R.string.proRenewDesktopLinked)) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) - .put(PLATFORM_STORE_KEY, defaultGoogleStore) - .put(PLATFORM_STORE2_KEY, defaultAppleStore) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .format(), + info = cell1Text, iconRes = R.drawable.ic_link ) ) @@ -110,12 +163,7 @@ fun ChoosePlanNoBilling( add( NonOriginatingLinkCellData( title = stringResource(R.string.proNewInstallation), - info = Phrase.from(context.getText(R.string.proNewInstallationDescription)) - .put(APP_NAME_KEY, NonTranslatableStringConstants.APP_NAME) - .put(PLATFORM_STORE_KEY, defaultGoogleStore) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(PRO_KEY, NonTranslatableStringConstants.PRO) - .format(), + info = cell2Text, iconRes = R.drawable.ic_smartphone ) ) @@ -194,17 +242,7 @@ private fun PreviewNoBiilingBrandNewPlan( PreviewTheme(colors) { val context = LocalContext.current ChoosePlanNoBilling ( - subscription = SubscriptionType.Expired( - expiredAt = Instant.now() - Duration.ofDays(14), - SubscriptionDetails( - device = "iPhone", - store = "Apple App Store", - platform = "Apple", - platformAccount = "Apple Account", - subscriptionUrl = "https://www.apple.com/account/subscriptions", - refundUrl = "https://www.apple.com/account/subscriptions", - ) - ), + subscription = SubscriptionType.NeverSubscribed, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanScreen.kt index 47c24508df..e128a5abc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/chooseplan/ChoosePlanScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -125,8 +126,10 @@ fun ChoosePlan( .put(DATE_KEY, planData.subscriptionType.duration.expiryFromNow()) .format() - //todo PRO cater for brand new subscription in here - else -> "" + is SubscriptionType.NeverSubscribed -> + Phrase.from(context.getText(R.string.proChooseAccess)) + .put(PRO_KEY, NonTranslatableStringConstants.PRO) + .format() } Text( @@ -171,6 +174,7 @@ fun ChoosePlan( is SubscriptionType.Active.Expiring -> Phrase.from(LocalContext.current, R.string.updateAccess) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString() + is SubscriptionType.NeverSubscribed -> stringResource(R.string.upgrade) else -> Phrase.from(LocalContext.current, R.string.updateAccess) .put(PRO_KEY, NonTranslatableStringConstants.PRO) .format().toString() @@ -188,6 +192,25 @@ fun ChoosePlan( Spacer(Modifier.height(LocalDimensions.current.xxsSpacing)) + val footer = when (planData.subscriptionType) { + is SubscriptionType.Expired -> + Phrase.from(LocalContext.current.getText(R.string.proRenewTosPrivacy)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(ICON_KEY, iconExternalLink) + .format() + + is SubscriptionType.Active -> Phrase.from(LocalContext.current.getText(R.string.proTosPrivacy)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(ICON_KEY, iconExternalLink) + .format() + + is SubscriptionType.NeverSubscribed -> + Phrase.from(LocalContext.current.getText(R.string.proUpgradingTosPrivacy)) + .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) + .put(ICON_KEY, iconExternalLink) + .format() + } + Text( modifier = Modifier.fillMaxWidth() .clickable( @@ -200,13 +223,7 @@ fun ChoosePlan( vertical = LocalDimensions.current.xxsSpacing ) .clip(MaterialTheme.shapes.extraSmall), - text = annotatedStringResource( - Phrase.from(LocalContext.current.getText(R.string.proTosPrivacy)) - .put(APP_PRO_KEY, NonTranslatableStringConstants.APP_PRO) - .put(ICON_KEY, iconExternalLink) - .put(ICON_KEY, iconExternalLink) - .format() - ), + text = annotatedStringResource(footer), textAlign = TextAlign.Center, style = LocalType.current.small, color = LocalColors.current.text, 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 f6a01687a2..ba363f5d9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProStatusManager.kt @@ -50,13 +50,19 @@ class ProStatusManager @Inject constructor( //todo PRO implement properly val subscriptionState = debugSubscription ?: DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE - val proStatus = when(debugProPlanStatus){ + val proDataStatus = when(debugProPlanStatus){ DebugMenuViewModel.DebugProPlanStatus.LOADING -> State.Loading DebugMenuViewModel.DebugProPlanStatus.ERROR -> State.Error(Exception()) else -> State.Success(Unit) } - SubscriptionState( + if(selfRecipient.proStatus is ProStatus.None){ + SubscriptionState( + type = SubscriptionType.NeverSubscribed, + refreshState = proDataStatus + ) + } + else SubscriptionState( type = when(subscriptionState){ DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE -> SubscriptionType.Active.AutoRenewing( proStatus = ProStatus.Pro( @@ -146,7 +152,7 @@ class ProStatusManager @Inject constructor( ) }, - refreshState = proStatus, + refreshState = proDataStatus, ) }.stateIn(GlobalScope, SharingStarted.Eagerly, 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 e4023db77a..54e7bd96c1 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 @@ -14,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.util.CurrentActivityObserver @@ -27,13 +28,15 @@ class PlayStoreSubscriptionManager @Inject constructor( private val application: Application, @param:ManagerScope private val scope: CoroutineScope, private val currentActivityObserver: CurrentActivityObserver, + private val prefs: TextSecurePreferences ) : SubscriptionManager { override val id = "google_play_store" override val name = "Google Play Store" override val description = "" override val iconRes = null - override val supportsBilling: Boolean = false + override val supportsBilling: Boolean + get() = !prefs.getDebugForceNoBilling() override val quickRefundExpiry: Instant = Instant.now() //todo PRO implement properly override val quickRefundUrl = "https://support.google.com/googleplay/workflow/9813244"