diff --git a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt index e8fee1710a3c..502f55a41d66 100644 --- a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt +++ b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt @@ -94,3 +94,7 @@ enum class SubscriptionStatus(val statusName: String) { UNKNOWN("Unknown"), WAITING("Waiting"), } + +enum class ActiveOfferType { + TRIAL, UNKNOWN +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt index 93ffac1c18a5..e14d1832bedf 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt @@ -34,6 +34,7 @@ import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.InternalAlwaysEnabled import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.subscriptions.api.Product import com.duckduckgo.subscriptions.api.SubscriptionStatus @@ -160,6 +161,14 @@ interface PrivacyProFeature { // Kill switch @Toggle.DefaultValue(true) fun featuresApi(): Toggle + + @Toggle.DefaultValue(false) + fun privacyProFreeTrialJan25(): Toggle + + enum class Cohorts(override val cohortName: String) : CohortName { + CONTROL("control"), + TREATMENT("treatment"), + } } @ContributesBinding(AppScope::class) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt index 252d093aca5c..36f7d87a4479 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt @@ -34,8 +34,8 @@ object SubscriptionsConstants { const val MONTHLY_PLAN_ROW = "ddg-privacy-pro-monthly-renews-row" // List of offers - const val MONTHLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-sandbox-freetrial-monthly-renews-us" - const val YEARLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-sandbox-freetrial-yearly-renews-us" + const val MONTHLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-freetrial-monthly-renews-us" + const val YEARLY_FREE_TRIAL_OFFER_US = "ddg-privacy-pro-freetrial-yearly-renews-us" // List of features const val LEGACY_FE_NETP = "vpn" diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index dc438f697bb9..bf76a1d40a2f 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -24,6 +24,7 @@ import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.api.ActiveOfferType import com.duckduckgo.subscriptions.api.Product import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE @@ -110,7 +111,7 @@ interface SubscriptionsManager { suspend fun purchase( activity: Activity, planId: String, - offerId: String? = null, + offerId: String?, ) /** @@ -214,6 +215,13 @@ interface SubscriptionsManager { suspend fun getPortalUrl(): String? suspend fun canSupportEncryption(): Boolean + + /** + * Checks whether the user has previously used a trial. + * + * @return [Boolean] indicating if the user has had a trial before. + */ + suspend fun hadTrial(): Boolean } @SingleInstanceIn(AppScope::class) @@ -327,6 +335,14 @@ class RealSubscriptionsManager @Inject constructor( override suspend fun canSupportEncryption(): Boolean = authRepository.canSupportEncryption() + override suspend fun hadTrial(): Boolean { + return try { + return subscriptionsService.offerStatus().hadTrial + } catch (e: Exception) { + false + } + } + override suspend fun getAccount(): Account? = authRepository.getAccount() override suspend fun getPortalUrl(): String? { @@ -406,11 +422,18 @@ class RealSubscriptionsManager @Inject constructor( packageName: String, purchaseToken: String, ): Boolean { + var experimentName: String? = null + val cohort: String? = privacyProFeature.get().privacyProFreeTrialJan25().getCohort()?.name + if (cohort != null) { + experimentName = "privacyProFreeTrialJan25" + } return try { val confirmationResponse = subscriptionsService.confirm( ConfirmationBody( packageName = packageName, purchaseToken = purchaseToken, + experimentName = experimentName, + experimentCohort = cohort, ), ) @@ -420,6 +443,7 @@ class RealSubscriptionsManager @Inject constructor( expiresOrRenewsAt = confirmationResponse.subscription.expiresOrRenewsAt, status = confirmationResponse.subscription.status.toStatus(), platform = confirmationResponse.subscription.platform, + activeOffers = confirmationResponse.subscription.activeOffers.map { it.type.toActiveOfferType() }, ) authRepository.setSubscription(subscription) @@ -437,6 +461,12 @@ class RealSubscriptionsManager @Inject constructor( } if (subscription.isActive()) { + // Free Trial experiment metrics + if (confirmationResponse.subscription.productId.contains("monthly-renews-us")) { + pixelSender.reportFreeTrialOnSubscriptionStartedMonthly() + } else if (confirmationResponse.subscription.productId.contains("yearly-renews-us")) { + pixelSender.reportFreeTrialOnSubscriptionStartedYearly() + } pixelSender.reportPurchaseSuccess() pixelSender.reportSubscriptionActivated() emitEntitlementsValues() @@ -517,6 +547,7 @@ class RealSubscriptionsManager @Inject constructor( expiresOrRenewsAt = subscription.expiresOrRenewsAt, status = subscription.status.toStatus(), platform = subscription.platform, + activeOffers = subscription.activeOffers.map { it.type.toActiveOfferType() }, ), ) authRepository.setEntitlements(accountData.entitlements.toEntitlements()) @@ -582,6 +613,7 @@ class RealSubscriptionsManager @Inject constructor( expiresOrRenewsAt = subscription.expiresOrRenewsAt, status = subscription.status.toStatus(), platform = subscription.platform, + activeOffers = subscription.activeOffers.map { it.type.toActiveOfferType() }, ), ) @@ -814,6 +846,7 @@ class RealSubscriptionsManager @Inject constructor( activity = activity, planId = planId, externalId = authRepository.getAccount()!!.externalId, + offerId = offerId, ) } catch (e: Exception) { val error = extractError(e) @@ -1009,6 +1042,13 @@ fun String.toStatus(): SubscriptionStatus { } } +fun String.toActiveOfferType(): ActiveOfferType { + return when (this) { + "Trial" -> ActiveOfferType.TRIAL + else -> ActiveOfferType.UNKNOWN + } +} + sealed class CurrentPurchase { data object PreFlowInProgress : CurrentPurchase() data object PreFlowFinished : CurrentPurchase() diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/PlayBillingManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/PlayBillingManager.kt index 97200bcfc110..a5ba9c729468 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/PlayBillingManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/PlayBillingManager.kt @@ -75,7 +75,7 @@ interface PlayBillingManager { activity: Activity, planId: String, externalId: String, - offerId: String? = null, + offerId: String?, ) } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialExperimentDataStore.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialExperimentDataStore.kt new file mode 100644 index 000000000000..b8ba4edd6764 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialExperimentDataStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.subscriptions.impl.freetrial + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.data.store.api.SharedPreferencesProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.PixelDefinition +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.withContext + +interface FreeTrialExperimentDataStore { + /** + * @return number of times paywall has been displayed to user + */ + var paywallImpressions: Int + + /** + * Increases the count of paywall impressions + */ + suspend fun increaseMetricForPaywallImpressions() + + /** + * Returns the value [String] for the given pixel [definition] + */ + suspend fun getMetricForPixelDefinition(definition: PixelDefinition): String? + + /** + * Increases the count of paywall impressions for the given [definition] + */ + suspend fun increaseMetricForPixelDefinition( + definition: PixelDefinition, + value: String, + ): String? +} + +@ContributesBinding(AppScope::class) +class FreeTrialExperimentDataStoreImpl @Inject constructor( + private val sharedPreferencesProvider: SharedPreferencesProvider, + private val dispatcherProvider: DispatcherProvider, +) : FreeTrialExperimentDataStore { + + private val preferences: SharedPreferences by lazy { + sharedPreferencesProvider.getSharedPreferences( + FILENAME, + ) + } + + override var paywallImpressions: Int + get() = preferences.getInt(KEY_PAYWALL_IMPRESSIONS, 0) + set(impressions) = preferences.edit { putInt(KEY_PAYWALL_IMPRESSIONS, impressions) } + + override suspend fun increaseMetricForPaywallImpressions() { + withContext(dispatcherProvider.io()) { + paywallImpressions += 1 + } + } + + override suspend fun getMetricForPixelDefinition(definition: PixelDefinition): String? { + val tag = "$definition" + return withContext(dispatcherProvider.io()) { + preferences.getString(tag, null) + } + } + + override suspend fun increaseMetricForPixelDefinition( + definition: PixelDefinition, + value: String, + ): String? = + withContext(dispatcherProvider.io()) { + val tag = "$definition" + preferences.edit(commit = true) { + putString(tag, value) + } + preferences.getString(tag, null) + } + + companion object { + private const val FILENAME = "com.duckduckgo.subscriptions.freetrial.store" + private const val KEY_PAYWALL_IMPRESSIONS = "PAYWALL_IMPRESSIONS" + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProMetricsPixelPlugin.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProMetricsPixelPlugin.kt new file mode 100644 index 000000000000..494c0ac728f1 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProMetricsPixelPlugin.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.subscriptions.impl.freetrial + +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.ConversionWindow +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.feature.toggles.api.MetricsPixelPlugin +import com.duckduckgo.feature.toggles.api.PixelDefinition +import com.duckduckgo.subscriptions.impl.PrivacyProFeature +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.Lazy +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject + +internal suspend fun FreeTrialPrivacyProPixelsPlugin.onPaywallImpression() { + val metricPixel = this.getMetrics().firstOrNull { it.metric == "paywallImpressions" } + this.firePixelFor(metricPixel) +} + +internal suspend fun FreeTrialPrivacyProPixelsPlugin.onStartClickedMonthly() { + val metricPixel = this.getMetrics().firstOrNull { it.metric == "startClickedMonthly" } + this.firePixelFor(metricPixel) +} + +internal suspend fun FreeTrialPrivacyProPixelsPlugin.onStartClickedYearly() { + val metricPixel = this.getMetrics().firstOrNull { it.metric == "startClickedYearly" } + this.firePixelFor(metricPixel) +} + +internal suspend fun FreeTrialPrivacyProPixelsPlugin.onSubscriptionStartedMonthly() { + val metricPixel = this.getMetrics().firstOrNull { it.metric == "subscriptionStartedMonthly" } + this.firePixelFor(metricPixel) +} + +internal suspend fun FreeTrialPrivacyProPixelsPlugin.onSubscriptionStartedYearly() { + val metricPixel = this.getMetrics().firstOrNull { it.metric == "subscriptionStartedYearly" } + this.firePixelFor(metricPixel) +} + +@ContributesMultibinding(AppScope::class) +class FreeTrialPrivacyProPixelsPlugin @Inject constructor( + private val toggle: Lazy, + private val freeTrialExperimentDataStore: FreeTrialExperimentDataStore, + private val pixel: Pixel, +) : MetricsPixelPlugin { + + override suspend fun getMetrics(): List { + return listOf( + MetricsPixel( + metric = "paywallImpressions", + value = getMetricsPixelValue(freeTrialExperimentDataStore.paywallImpressions), + toggle = toggle.get().privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ), + MetricsPixel( + metric = "startClickedMonthly", + value = getMetricsPixelValue(freeTrialExperimentDataStore.paywallImpressions), + toggle = toggle.get().privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ), + MetricsPixel( + metric = "startClickedYearly", + value = getMetricsPixelValue(freeTrialExperimentDataStore.paywallImpressions), + toggle = toggle.get().privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ), + MetricsPixel( + metric = "subscriptionStartedMonthly", + value = getMetricsPixelValue(freeTrialExperimentDataStore.paywallImpressions), + toggle = toggle.get().privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ), + MetricsPixel( + metric = "subscriptionStartedYearly", + value = getMetricsPixelValue(freeTrialExperimentDataStore.paywallImpressions), + toggle = toggle.get().privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ), + ) + } + + internal fun getMetricsPixelValue(paywallImpressions: Int): String { + return when (paywallImpressions) { + 1, 2, 3, 4, 5 -> paywallImpressions.toString() + in 6..10 -> "6-10" + in 11..50 -> "11-50" + in 50..Int.MAX_VALUE -> "51+" + else -> "0" + } + } + + internal suspend fun firePixelFor(metricsPixel: MetricsPixel?) { + metricsPixel?.let { metric -> + metric.getPixelDefinitions().forEach { definition -> + val hasMetricValueChanged = freeTrialExperimentDataStore.getMetricForPixelDefinition(definition) != metric.value + if (definition.isInConversionWindow() && hasMetricValueChanged) { + freeTrialExperimentDataStore.increaseMetricForPixelDefinition(definition, metric.value) + pixel.fire(definition.pixelName, definition.params) + } + } + } + } +} + +private fun PixelDefinition.isInConversionWindow(): Boolean { + val enrollmentDate = this.params["enrollmentDate"] ?: return false + val lowerWindow = this.params["conversionWindowDays"]?.split("-")?.first()?.toInt() ?: return false + val upperWindow = this.params["conversionWindowDays"]?.split("-")?.last()?.toInt() ?: return false + val daysDiff = enrollmentDate.daysUntilToday() + + return (daysDiff in lowerWindow..upperWindow) +} + +private fun String.daysUntilToday(): Long { + val today = ZonedDateTime.now(ZoneId.of("America/New_York")) + val localDate = LocalDate.parse(this) + val zoneDateTime: ZonedDateTime = localDate.atStartOfDay(ZoneId.of("America/New_York")) + return ChronoUnit.DAYS.between(zoneDateTime, today) +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt index 0d8a8b6b4291..4c67a60844af 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt @@ -230,8 +230,14 @@ class SubscriptionMessagingInterface @Inject constructor( ) { appCoroutineScope.launch { when (jsMessage.method) { - "subscriptionsMonthlyPriceClicked" -> pixelSender.reportMonthlyPriceClick() - "subscriptionsYearlyPriceClicked" -> pixelSender.reportYearlyPriceClick() + "subscriptionsMonthlyPriceClicked" -> { + pixelSender.reportMonthlyPriceClick() + pixelSender.reportFreeTrialOnStartClickedMonthly() + } + "subscriptionsYearlyPriceClicked" -> { + pixelSender.reportYearlyPriceClick() + pixelSender.reportFreeTrialOnStartClickedYearly() + } "subscriptionsAddEmailSuccess" -> { pixelSender.reportAddEmailSuccess() subscriptionsManager.tryRefreshAccessToken() diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt index 28f4fc0efd0a..ba2b9d6f4a6c 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt @@ -20,6 +20,12 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.extensions.toSanitizedLanguageTag import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.impl.freetrial.FreeTrialPrivacyProPixelsPlugin +import com.duckduckgo.subscriptions.impl.freetrial.onPaywallImpression +import com.duckduckgo.subscriptions.impl.freetrial.onStartClickedMonthly +import com.duckduckgo.subscriptions.impl.freetrial.onStartClickedYearly +import com.duckduckgo.subscriptions.impl.freetrial.onSubscriptionStartedMonthly +import com.duckduckgo.subscriptions.impl.freetrial.onSubscriptionStartedYearly import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSCRIPTION_ENTER_EMAIL_CLICK import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSCRIPTION_RESTORE_PURCHASE_CLICK import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_IDTR_CLICK @@ -106,12 +112,18 @@ interface SubscriptionPixelSender { fun reportAuthV2MigrationFailureOther() fun reportAuthV2TokenValidationError() fun reportAuthV2TokenStoreError() + suspend fun reportFreeTrialExperimentOnPaywallImpression() + suspend fun reportFreeTrialOnStartClickedMonthly() + suspend fun reportFreeTrialOnStartClickedYearly() + suspend fun reportFreeTrialOnSubscriptionStartedMonthly() + suspend fun reportFreeTrialOnSubscriptionStartedYearly() } @ContributesBinding(AppScope::class) class SubscriptionPixelSenderImpl @Inject constructor( private val pixelSender: Pixel, private val appBuildConfig: AppBuildConfig, + private val freeTrialPrivacyProPixelsPlugin: FreeTrialPrivacyProPixelsPlugin, ) : SubscriptionPixelSender { override fun reportSubscriptionActive() = @@ -252,6 +264,26 @@ class SubscriptionPixelSenderImpl @Inject constructor( fire(AUTH_V2_TOKEN_STORE_ERROR) } + override suspend fun reportFreeTrialExperimentOnPaywallImpression() { + freeTrialPrivacyProPixelsPlugin.onPaywallImpression() + } + + override suspend fun reportFreeTrialOnStartClickedMonthly() { + freeTrialPrivacyProPixelsPlugin.onStartClickedMonthly() + } + + override suspend fun reportFreeTrialOnStartClickedYearly() { + freeTrialPrivacyProPixelsPlugin.onStartClickedYearly() + } + + override suspend fun reportFreeTrialOnSubscriptionStartedMonthly() { + freeTrialPrivacyProPixelsPlugin.onSubscriptionStartedMonthly() + } + + override suspend fun reportFreeTrialOnSubscriptionStartedYearly() { + freeTrialPrivacyProPixelsPlugin.onSubscriptionStartedYearly() + } + private fun fire(pixel: SubscriptionPixel, params: Map = emptyMap()) { pixel.getPixelNames().forEach { (pixelType, pixelName) -> pixelSender.fire(pixelName = pixelName, type = pixelType, parameters = params) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/repository/AuthRepository.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/repository/AuthRepository.kt index ea94da63795b..6202b409e48f 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/repository/AuthRepository.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/repository/AuthRepository.kt @@ -19,6 +19,7 @@ package com.duckduckgo.subscriptions.impl.repository import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.data.store.api.SharedPreferencesProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.api.ActiveOfferType import com.duckduckgo.subscriptions.api.Product import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE @@ -166,6 +167,7 @@ internal class RealAuthRepository constructor( startedAt = subscription?.startedAt expiresOrRenewsAt = subscription?.expiresOrRenewsAt status = subscription?.status?.statusName + freeTrialActive = subscription?.activeOffers?.contains(ActiveOfferType.TRIAL) ?: false } } @@ -175,12 +177,14 @@ internal class RealAuthRepository constructor( val startedAt = subscriptionsDataStore.startedAt ?: return@withContext null val expiresOrRenewsAt = subscriptionsDataStore.expiresOrRenewsAt ?: return@withContext null val status = subscriptionsDataStore.status?.toStatus() ?: return@withContext null + val activeOffers = if (subscriptionsDataStore.freeTrialActive) listOf(ActiveOfferType.TRIAL) else listOf() Subscription( productId = productId, platform = platform, startedAt = startedAt, expiresOrRenewsAt = expiresOrRenewsAt, status = status, + activeOffers = activeOffers, ) } @@ -249,6 +253,7 @@ data class Subscription( val expiresOrRenewsAt: Long, val status: SubscriptionStatus, val platform: String, + val activeOffers: List, ) { fun isActive(): Boolean = status.isActive() } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt index e133741d11ac..6a068b598226 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt @@ -35,6 +35,10 @@ interface SubscriptionsService { @GET("https://subscriptions.duckduckgo.com/api/checkout/portal") suspend fun portal(): PortalResponse + @AuthRequired + @GET("https://subscriptions.duckduckgo.com/api/v1/offer-status") + suspend fun offerStatus(): OfferStatusResponse + @AuthRequired @POST("https://subscriptions.duckduckgo.com/api/purchase/confirm/google") suspend fun confirm( @@ -59,11 +63,18 @@ data class SubscriptionResponse( val expiresOrRenewsAt: Long, val platform: String, val status: String, + val activeOffers: List, +) + +data class ActiveOfferResponse( + val type: String, ) data class ConfirmationBody( val packageName: String, val purchaseToken: String, + val experimentName: String?, + val experimentCohort: String?, ) data class ConfirmationResponse( @@ -100,3 +111,7 @@ data class FeedbackResponse( data class FeaturesResponse( val features: List, ) + +data class OfferStatusResponse( + val hadTrial: Boolean, +) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/store/SubscriptionsDataStore.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/store/SubscriptionsDataStore.kt index 130dc0ba2a66..a3cf30a8f83e 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/store/SubscriptionsDataStore.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/store/SubscriptionsDataStore.kt @@ -41,6 +41,7 @@ interface SubscriptionsDataStore { var status: String? var entitlements: String? var productId: String? + var freeTrialActive: Boolean var subscriptionFeatures: String? @@ -123,6 +124,14 @@ internal class SubscriptionsEncryptedDataStore constructor( } } + override var freeTrialActive: Boolean + get() = encryptedPreferences?.getBoolean(KEY_FREE_TRIAL_ACTIVE, false) ?: false + set(value) { + encryptedPreferences?.edit(commit = true) { + putBoolean(KEY_FREE_TRIAL_ACTIVE, value) + } + } + override var accessToken: String? get() = encryptedPreferences?.getString(KEY_ACCESS_TOKEN, null) set(value) { @@ -217,5 +226,6 @@ internal class SubscriptionsEncryptedDataStore constructor( const val KEY_STATUS = "KEY_STATUS" const val KEY_PRODUCT_ID = "KEY_PRODUCT_ID" const val KEY_SUBSCRIPTION_FEATURES = "KEY_SUBSCRIPTION_FEATURES" + const val KEY_FREE_TRIAL_ACTIVE = "KEY_FREE_TRIAL_ACTIVE" } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt index 3f3393241bad..7ab87fba1865 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt @@ -36,6 +36,7 @@ import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.subscriptions.api.ActiveOfferType import com.duckduckgo.subscriptions.api.PrivacyProFeedbackScreens.PrivacyProFeedbackScreenWithParams import com.duckduckgo.subscriptions.api.PrivacyProUnifiedFeedback.PrivacyProFeedbackSource.SUBSCRIPTION_SETTINGS import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE @@ -176,17 +177,35 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { binding.subscriptionActiveStatusContainer.isVisible = true binding.subscriptionExpiredStatusContainer.isVisible = false - val status = when (viewState.status) { - AUTO_RENEWABLE -> getString(string.renews) - else -> getString(string.expires) - } + // Free Trial active + if (viewState.activeOffers.contains(ActiveOfferType.TRIAL)) { + binding.subscriptionActiveStatusTextView.text = getString(string.subscriptionStatusFreeTrial) - val subscriptionsDataStringResId = when (viewState.duration) { - Monthly -> string.subscriptionsDataMonthly - Yearly -> string.subscriptionsDataYearly - } + val subscriptionRenewalDetailsRes = when { + viewState.status == AUTO_RENEWABLE && viewState.duration == Monthly -> + getString(string.freeTrialActiveSubscriptionsData, viewState.date, getString(string.monthly)) + viewState.status == AUTO_RENEWABLE && viewState.duration == Yearly -> + getString(string.freeTrialActiveSubscriptionsData, viewState.date, getString(string.yearly)) + else -> getString(string.freeTrialCancelledSubscriptionsData, viewState.date) + } + binding.changePlan.setSecondaryText(subscriptionRenewalDetailsRes) - binding.changePlan.setSecondaryText(getString(subscriptionsDataStringResId, status, viewState.date)) + // Active status without a Free Trial + } else { + binding.subscriptionActiveStatusTextView.text = getString(string.subscriptionStatusSubscribed) + + val status = when (viewState.status) { + AUTO_RENEWABLE -> getString(string.renews) + else -> getString(string.expires) + } + + val subscriptionsDataStringResId = when (viewState.duration) { + Monthly -> string.subscriptionsDataMonthly + Yearly -> string.subscriptionsDataYearly + } + + binding.changePlan.setSecondaryText(getString(subscriptionsDataStringResId, status, viewState.date)) + } when (viewState.platform.lowercase()) { "apple", "ios" -> diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt index aeec41bb9839..01b3d22613a7 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.subscriptions.api.ActiveOfferType import com.duckduckgo.subscriptions.api.PrivacyProUnifiedFeedback import com.duckduckgo.subscriptions.api.PrivacyProUnifiedFeedback.PrivacyProFeedbackSource.SUBSCRIPTION_SETTINGS import com.duckduckgo.subscriptions.api.SubscriptionStatus @@ -98,6 +99,7 @@ class SubscriptionSettingsViewModel @Inject constructor( platform = subscription.platform, email = account.email?.takeUnless { it.isBlank() }, showFeedback = privacyProUnifiedFeedback.shouldUseUnifiedFeedback(source = SUBSCRIPTION_SETTINGS), + activeOffers = subscription.activeOffers, ), ) } @@ -152,6 +154,7 @@ class SubscriptionSettingsViewModel @Inject constructor( val platform: String, val email: String?, val showFeedback: Boolean = false, + val activeOffers: List, ) : ViewState() } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt index e2906cf82b12..3056c453457e 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt @@ -30,6 +30,7 @@ import com.duckduckgo.subscriptions.api.SubscriptionStatus import com.duckduckgo.subscriptions.impl.CurrentPurchase import com.duckduckgo.subscriptions.impl.JSONObjectAdapter import com.duckduckgo.subscriptions.impl.PrivacyProFeature +import com.duckduckgo.subscriptions.impl.PrivacyProFeature.Cohorts import com.duckduckgo.subscriptions.impl.SubscriptionOffer import com.duckduckgo.subscriptions.impl.SubscriptionsChecker import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR @@ -49,6 +50,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_FREE_TRIA import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_ROW import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.freetrial.FreeTrialExperimentDataStore import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.isActive import com.duckduckgo.subscriptions.impl.repository.isExpired @@ -82,6 +84,7 @@ class SubscriptionWebViewViewModel @Inject constructor( private val networkProtectionAccessState: NetworkProtectionAccessState, private val pixelSender: SubscriptionPixelSender, private val privacyProFeature: PrivacyProFeature, + private val freeTrialExperimentDataStore: FreeTrialExperimentDataStore, ) : ViewModel() { private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() @@ -220,18 +223,19 @@ class SubscriptionWebViewViewModel @Inject constructor( viewModelScope.launch(dispatcherProvider.io()) { val id = runCatching { data?.getString("id") }.getOrNull() + val offerId = runCatching { data?.getString("offerId") }.getOrNull() if (id.isNullOrBlank()) { pixelSender.reportPurchaseFailureOther() _currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = Failure)) } else { - command.send(SubscriptionSelected(id)) + command.send(SubscriptionSelected(id, offerId)) } } } - fun purchaseSubscription(activity: Activity, planId: String) { + fun purchaseSubscription(activity: Activity, planId: String, offerId: String?) { viewModelScope.launch(dispatcherProvider.io()) { - subscriptionsManager.purchase(activity, planId) + subscriptionsManager.purchase(activity, planId, offerId) } } @@ -255,6 +259,13 @@ class SubscriptionWebViewViewModel @Inject constructor( val subscriptionOptions = if (privacyProFeature.allowPurchase().isEnabled()) { val subscriptionOffers = subscriptionsManager.getSubscriptionOffer().associateBy { it.offerId ?: it.planId } when { + subscriptionOffers.keys.containsAll(listOf(MONTHLY_FREE_TRIAL_OFFER_US, YEARLY_FREE_TRIAL_OFFER_US)) && isFreeTrialEligible() -> { + createSubscriptionOptions( + monthlyOffer = subscriptionOffers.getValue(MONTHLY_FREE_TRIAL_OFFER_US), + yearlyOffer = subscriptionOffers.getValue(YEARLY_FREE_TRIAL_OFFER_US), + ) + } + subscriptionOffers.keys.containsAll(listOf(MONTHLY_PLAN_US, YEARLY_PLAN_US)) -> { createSubscriptionOptions( monthlyOffer = subscriptionOffers.getValue(MONTHLY_PLAN_US), @@ -276,10 +287,15 @@ class SubscriptionWebViewViewModel @Inject constructor( } sendOptionJson(subscriptionOptions) + pixelSender.reportFreeTrialExperimentOnPaywallImpression() // move to paywallShown() if needed after experiment } } - private fun createSubscriptionOptions( + private fun isFreeTrialEligible(): Boolean { + return privacyProFeature.privacyProFreeTrialJan25().isEnabled(Cohorts.TREATMENT) + } + + private suspend fun createSubscriptionOptions( monthlyOffer: SubscriptionOffer, yearlyOffer: SubscriptionOffer, ): SubscriptionOptionsJson { @@ -292,7 +308,7 @@ class SubscriptionWebViewViewModel @Inject constructor( ) } - private fun createOptionsJson(offer: SubscriptionOffer, recurrence: String): OptionsJson { + private suspend fun createOptionsJson(offer: SubscriptionOffer, recurrence: String): OptionsJson { val offerDisplayPrice: String = offer.offerId?.let { offer.pricingPhases.getOrNull(1)?.formattedPrice ?: offer.pricingPhases.first().formattedPrice } ?: offer.pricingPhases.first().formattedPrice @@ -304,7 +320,7 @@ class SubscriptionWebViewViewModel @Inject constructor( ) } - private fun getOfferJson(offer: SubscriptionOffer): OfferJson? { + private suspend fun getOfferJson(offer: SubscriptionOffer): OfferJson? { return offer.offerId?.let { val offerType = when (offer.offerId) { MONTHLY_FREE_TRIAL_OFFER_US, YEARLY_FREE_TRIAL_OFFER_US -> OfferType.FREE_TRIAL @@ -315,7 +331,7 @@ class SubscriptionWebViewViewModel @Inject constructor( type = offerType.type, id = it, durationInDays = offer.pricingPhases.first().getBillingPeriodInDays(), - isUserEligible = true, // TODO Noelia: Need to check if they already had a free trial before to return false + isUserEligible = !subscriptionsManager.hadTrial(), ) } } @@ -340,6 +356,13 @@ class SubscriptionWebViewViewModel @Inject constructor( } } + fun paywallShown() { + pixelSender.reportOfferScreenShown() + viewModelScope.launch { + freeTrialExperimentDataStore.increaseMetricForPaywallImpressions() + } + } + data class SubscriptionOptionsJson( val platform: String = PLATFORM, val options: List, @@ -385,7 +408,10 @@ class SubscriptionWebViewViewModel @Inject constructor( data object BackToSettingsActivateSuccess : Command() data class SendJsEvent(val event: SubscriptionEventData) : Command() data class SendResponseToJs(val data: JsCallbackData) : Command() - data class SubscriptionSelected(val id: String) : Command() + data class SubscriptionSelected( + val id: String, + val offerId: String?, + ) : Command() data object RestoreSubscription : Command() data object GoToITR : Command() data object GoToPIR : Command() diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt index d52da434d493..f07c20ed77bf 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt @@ -259,7 +259,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD }.launchIn(lifecycleScope) if (savedInstanceState == null && params.url == BUY_URL) { - pixelSender.reportOfferScreenShown() + viewModel.paywallShown() } } @@ -417,7 +417,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD is BackToSettings, BackToSettingsActivateSuccess -> backToSettings() is SendJsEvent -> sendJsEvent(command.event) is SendResponseToJs -> sendResponseToJs(command.data) - is SubscriptionSelected -> selectSubscription(command.id) + is SubscriptionSelected -> selectSubscription(command.id, command.offerId) is RestoreSubscription -> restoreSubscription() is GoToITR -> goToITR() is GoToPIR -> goToPIR() @@ -517,8 +517,8 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD .show() } - private fun selectSubscription(id: String) { - viewModel.purchaseSubscription(this, id) + private fun selectSubscription(id: String, offerId: String?) { + viewModel.purchaseSubscription(this, id, offerId) } private fun sendResponseToJs(data: JsCallbackData) { diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/activity_subscription_settings.xml b/subscriptions/subscriptions-impl/src/main/res/layout/activity_subscription_settings.xml index 20eaa691eb66..7454933e2fbc 100644 --- a/subscriptions/subscriptions-impl/src/main/res/layout/activity_subscription_settings.xml +++ b/subscriptions/subscriptions-impl/src/main/res/layout/activity_subscription_settings.xml @@ -15,34 +15,34 @@ --> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context="com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsActivity"> + android:id="@+id/includeToolbar" + layout="@layout/include_default_toolbar" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="@dimen/keyline_5"> + android:src="@drawable/ic_privacy_pro_settings_hero" /> @@ -97,11 +97,12 @@ android:src="@drawable/ic_dot_green" /> + app:typography="body2" /> @@ -111,8 +112,8 @@ android:layout_marginTop="@dimen/keyline_4" /> + android:id="@+id/removeDevice" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:primaryText="@string/subscriptionSettingRemoveFromDevice" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:primaryText="@string/subscriptionSettingSectionHelpAndSupport" /> + android:id="@+id/faq" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:primaryText="@string/privacyProFaq" + app:secondaryText="@string/privacyProFaqSecondary" /> + + + + Free Trial Active + monthly + yearly + Your free trial ends on %1$s & automatically converts to a %2$s paid subscription on that day. + Your free trial ends on %1$s & will not convert to a paid subscription. + \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index 07e9311a07f3..80d720ebdf35 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -316,7 +316,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenUserIsNotSignedIn() givenCreateAccountSucceeds() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) if (authApiV2Enabled) { verify(authClient).authorize(any()) @@ -334,7 +334,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { whenever(emailManager.getToken()).thenReturn("emailToken") givenUserIsNotSignedIn() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) verify(authService).createAccount("Bearer emailToken") } @@ -345,7 +345,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenCreateAccountFails() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) assertTrue(awaitItem() is CurrentPurchase.Failure) cancelAndConsumeRemainingEvents() @@ -359,7 +359,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenValidateTokenSucceedsNoEntitlements() givenAccessTokenSucceeds() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) verify(playBillingManager).launchBillingFlow(any(), any(), externalId = eq("1234"), isNull()) } @@ -372,7 +372,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenSubscriptionSucceedsWithoutEntitlements(status = "Expired") givenAccessTokenSucceeds() - subscriptionsManager.purchase(mock(), "") + subscriptionsManager.purchase(mock(), "", null) verify(playBillingManager).launchBillingFlow(any(), any(), externalId = eq("1234"), isNull()) } @@ -386,7 +386,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenAccessTokenSucceeds() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) verify(playBillingManager, never()).launchBillingFlow(any(), any(), any(), isNull()) assertTrue(awaitItem() is CurrentPurchase.Recovered) @@ -402,7 +402,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenStoreLoginFails() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) assertTrue(awaitItem() is CurrentPurchase.Failure) cancelAndConsumeRemainingEvents() @@ -415,7 +415,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenUserIsSignedIn() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) verify(authService).validateToken(any()) } @@ -426,7 +426,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenSubscriptionSucceedsWithoutEntitlements(status = "Expired") subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) verify(playBillingManager).launchBillingFlow(any(), any(), externalId = eq("1234"), isNull()) assertTrue(awaitItem() is CurrentPurchase.PreFlowFinished) @@ -440,7 +440,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenCreateAccountFails() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) assertTrue(awaitItem() is CurrentPurchase.Failure) cancelAndConsumeRemainingEvents() @@ -452,7 +452,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenUserIsSignedIn() givenValidateTokenFails("failure") - subscriptionsManager.purchase(mock(), "") + subscriptionsManager.purchase(mock(), "", null) verify(authService, never()).createAccount(any()) } @@ -464,7 +464,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenSubscriptionSucceedsWithoutEntitlements() givenAccessTokenSucceeds() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) if (authApiV2Enabled) { assertEquals(FAKE_ACCESS_TOKEN_V2, authDataStore.accessTokenV2) assertEquals(FAKE_REFRESH_TOKEN_V2, authDataStore.refreshTokenV2) @@ -486,7 +486,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenSubscriptionSucceedsWithoutEntitlements() givenAccessTokenSucceeds() - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) subscriptionsManager.isSignedIn.test { assertTrue(awaitItem()) if (authApiV2Enabled) { @@ -509,7 +509,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenUserIsSignedIn() givenSubscriptionFails(httpResponseCode = 400) - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) verify(playBillingManager).launchBillingFlow(any(), any(), any(), isNull()) } @@ -519,7 +519,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenUserIsSignedIn() givenSubscriptionFails(httpResponseCode = 404) - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) verify(playBillingManager).launchBillingFlow(any(), any(), any(), isNull()) } @@ -1100,7 +1100,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenAccessTokenSucceeds() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) assertTrue(awaitItem() is CurrentPurchase.Recovered) @@ -1137,7 +1137,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { givenCreateAccountFails() subscriptionsManager.currentPurchaseState.test { - subscriptionsManager.purchase(mock(), planId = "") + subscriptionsManager.purchase(mock(), planId = "", offerId = null) assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) assertTrue(awaitItem() is CurrentPurchase.Failure) @@ -1274,6 +1274,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { expiresOrRenewsAt = 1234, platform = "android", status = "AUTO_RENEWABLE", + activeOffers = listOf(), ) } @@ -1355,6 +1356,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { expiresOrRenewsAt = 1234, platform = "android", status = status, + activeOffers = listOf(), ), ) } @@ -1368,6 +1370,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { expiresOrRenewsAt = 1234, platform = "android", status = status, + activeOffers = listOf(), ), ) } @@ -1564,6 +1567,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { status = "Auto-Renewable", startedAt = 1000000L, expiresOrRenewsAt = 1000000L, + activeOffers = listOf(), ), ), ) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/billing/RealPlayBillingManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/billing/RealPlayBillingManagerTest.kt index 323efd7f92d1..cd67223e8c3d 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/billing/RealPlayBillingManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/billing/RealPlayBillingManagerTest.kt @@ -105,7 +105,7 @@ class RealPlayBillingManagerTest { subject.purchaseState.test { expectNoEvents() - subject.launchBillingFlow(activity = mock(), planId = MONTHLY_PLAN_US, externalId) + subject.launchBillingFlow(activity = mock(), planId = MONTHLY_PLAN_US, externalId, null) assertEquals(InProgress, awaitItem()) } @@ -126,7 +126,7 @@ class RealPlayBillingManagerTest { subject.purchaseState.test { expectNoEvents() - subject.launchBillingFlow(activity = mock(), planId = MONTHLY_PLAN_US, externalId) + subject.launchBillingFlow(activity = mock(), planId = MONTHLY_PLAN_US, externalId, null) assertEquals(Canceled, awaitItem()) } @@ -175,7 +175,27 @@ class RealPlayBillingManagerTest { subject.purchaseState.test { expectNoEvents() - subject.launchBillingFlow(activity = mock(), planId = offerDetails.basePlanId, externalId) + subject.launchBillingFlow(activity = mock(), planId = offerDetails.basePlanId, externalId, null) + + assertEquals(InProgress, awaitItem()) + } + + billingClientAdapter.verifyLaunchBillingFlowInvoked(productDetails, offerToken = offerDetails.offerToken, externalId) + } + + @Test + fun `when launch billing flow then retrieves ProductDetails for provided plan id and offer id`() = runTest { + processLifecycleOwner.currentState = RESUMED + billingClientAdapter.launchBillingFlowResult = LaunchBillingFlowResult.Success + + val productDetails: ProductDetails = subject.products.single() + val offerDetails = productDetails.subscriptionOfferDetails!!.first() + val externalId = "external_id" + + subject.purchaseState.test { + expectNoEvents() + + subject.launchBillingFlow(activity = mock(), planId = offerDetails.basePlanId, externalId = externalId, offerId = offerDetails.offerId) assertEquals(InProgress, awaitItem()) } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProPixelsPluginTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProPixelsPluginTest.kt new file mode 100644 index 000000000000..4b798b046d18 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/freetrial/FreeTrialPrivacyProPixelsPluginTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.subscriptions.impl.freetrial + +import android.annotation.SuppressLint +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.ConversionWindow +import com.duckduckgo.feature.toggles.api.FakeToggleStore +import com.duckduckgo.feature.toggles.api.FeatureToggles +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.feature.toggles.api.PixelDefinition +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.subscriptions.impl.PrivacyProFeature +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +@SuppressLint("DenyListedApi") +class FreeTrialPrivacyProPixelsPluginTest { + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() + + private val mockPixel: Pixel = mock() + private val mockFreeTrialExperimentDataStore: FreeTrialExperimentDataStore = mock() + + private lateinit var testFeature: PrivacyProFeature + private lateinit var testee: FreeTrialPrivacyProPixelsPlugin + + @Before + fun setup() { + testFeature = FeatureToggles.Builder( + FakeToggleStore(), + featureName = "testFeature", + ).build().create(PrivacyProFeature::class.java) + + testFeature.privacyProFreeTrialJan25().setRawStoredState(state = State(enable = true)) + + testee = FreeTrialPrivacyProPixelsPlugin( + toggle = { testFeature }, + freeTrialExperimentDataStore = mockFreeTrialExperimentDataStore, + pixel = mockPixel, + ) + } + + @Test + fun whenPaywallImpressionsIsZeroThenSetMetricsPixelValueAs0String() = runTest { + val value = testee.getMetricsPixelValue(0) + + assertEquals(value, "0") + } + + @Test + fun whenPaywallImpressionsIsThreeThenSetMetricsPixelValueAs3String() = runTest { + val value = testee.getMetricsPixelValue(3) + + assertEquals(value, "3") + } + + @Test + fun whenPaywallImpressionsIsFiveThenSetMetricsPixelValueAs5String() = runTest { + val value = testee.getMetricsPixelValue(5) + + assertEquals(value, "5") + } + + @Test + fun whenPaywallImpressionsIsSixThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(6) + + assertEquals(value, "6-10") + } + + @Test + fun whenPaywallImpressionsIsEightThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(8) + + assertEquals(value, "6-10") + } + + @Test + fun whenPaywallImpressionsIsTenThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(10) + + assertEquals(value, "6-10") + } + + @Test + fun whenPaywallImpressionsIsElevenThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(11) + + assertEquals(value, "11-50") + } + + @Test + fun whenPaywallImpressionsIsThirtyThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(30) + + assertEquals(value, "11-50") + } + + @Test + fun whenPaywallImpressionsIsFiftyThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(50) + + assertEquals(value, "11-50") + } + + @Test + fun whenPaywallImpressionsIsGreaterThanFiftyThenSetMetricsPixelValueWithCorrectValue() = runTest { + val value = testee.getMetricsPixelValue(60) + + assertEquals(value, "51+") + } + + @Test + fun givenMetricValueAlreadyFiredWhenFirePixelRequestedThenDoNotFireAgain() = runTest { + val mockPixelDefinition: PixelDefinition = mock() + whenever(mockFreeTrialExperimentDataStore.getMetricForPixelDefinition(mockPixelDefinition)).thenReturn("6-10") + + testee.firePixelFor(getMetricPixel("6-10")) + verifyNoInteractions(mockPixel) + verifyNoInteractions(mockFreeTrialExperimentDataStore) + } + + private fun getMetricPixel(value: String): MetricsPixel { + return MetricsPixel( + metric = "metric", + value = value, + toggle = testFeature.privacyProFreeTrialJan25(), + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = 3)), + ) + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt index f541b5fbc82e..098c80656436 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt @@ -15,7 +15,9 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.Subscription import kotlinx.coroutines.test.runTest import org.json.JSONObject -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -567,7 +569,7 @@ class SubscriptionMessagingInterfaceTest { messagingInterface.process(message, "duckduckgo-android-messaging-secret") verify(pixelSender).reportMonthlyPriceClick() - verifyNoMoreInteractions(pixelSender) + // verifyNoMoreInteractions(pixelSender) Add it back when Free Trials experiment is removed } @Test @@ -581,7 +583,33 @@ class SubscriptionMessagingInterfaceTest { messagingInterface.process(message, "duckduckgo-android-messaging-secret") verify(pixelSender).reportYearlyPriceClick() - verifyNoMoreInteractions(pixelSender) + // verifyNoMoreInteractions(pixelSender) Add it back when Free Trials experiment is removed + } + + @Test + fun `when process and monthly price clicked then experiment pixel sent`() = runTest { + givenInterfaceIsRegistered() + + val message = """ + {"context":"subscriptionPages","featureName":"useSubscription","method":"subscriptionsMonthlyPriceClicked","id":"myId","params":{}} + """.trimIndent() + + messagingInterface.process(message, "duckduckgo-android-messaging-secret") + + verify(pixelSender).reportFreeTrialOnStartClickedMonthly() + } + + @Test + fun `when process and yearly price clicked then experiment pixel sent`() = runTest { + givenInterfaceIsRegistered() + + val message = """ + {"context":"subscriptionPages","featureName":"useSubscription","method":"subscriptionsYearlyPriceClicked","id":"myId","params":{}} + """.trimIndent() + + messagingInterface.process(message, "duckduckgo-android-messaging-secret") + + verify(pixelSender).reportFreeTrialOnStartClickedYearly() } @Test @@ -725,6 +753,7 @@ class SubscriptionMessagingInterfaceTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/repository/FakeSubscriptionsDataStore.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/repository/FakeSubscriptionsDataStore.kt index 93cfd3ad6859..6587f975f8f7 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/repository/FakeSubscriptionsDataStore.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/repository/FakeSubscriptionsDataStore.kt @@ -47,6 +47,7 @@ class FakeSubscriptionsDataStore( override var status: String? = null override var entitlements: String? = null override var productId: String? = null + override var freeTrialActive: Boolean = false override fun canUseEncryption(): Boolean = supportEncryption override var subscriptionFeatures: String? = null } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProBillingPeriodMatchingAttributeTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProBillingPeriodMatchingAttributeTest.kt index 6ebce71c69e8..1b67ea3a3b8f 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProBillingPeriodMatchingAttributeTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProBillingPeriodMatchingAttributeTest.kt @@ -24,6 +24,7 @@ class RMFPProBillingPeriodMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "Google", + activeOffers = listOf(), ) @Before diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysSinceSubscribedMatchingAttributeTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysSinceSubscribedMatchingAttributeTest.kt index e44fefea2e2d..eab5d35369c4 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysSinceSubscribedMatchingAttributeTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysSinceSubscribedMatchingAttributeTest.kt @@ -85,6 +85,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute()) @@ -114,6 +115,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(value = 15, max = 13, min = 10)) @@ -133,6 +135,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(max = 15)) @@ -152,6 +155,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(max = 15)) @@ -171,6 +175,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(min = 5)) @@ -190,6 +195,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(min = 5)) @@ -209,6 +215,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(min = 5, max = 10)) @@ -228,6 +235,7 @@ class RMFPProDaysSinceSubscribedMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProDaysSinceSubscribedMatchingAttribute(min = 5, max = 10)) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysUntilExpiryRenewalMatchingAttributeTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysUntilExpiryRenewalMatchingAttributeTest.kt index 1bbe3e5a24ee..9245db92ed26 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysUntilExpiryRenewalMatchingAttributeTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProDaysUntilExpiryRenewalMatchingAttributeTest.kt @@ -35,6 +35,7 @@ class RMFPProDaysUntilExpiryRenewalMatchingAttributeTest { expiresOrRenewsAt = DAYS.toMillis(10), status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ) @Before diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProPurchasePlatformMatchingAttributeTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProPurchasePlatformMatchingAttributeTest.kt index 74638e4744b3..0528457a8432 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProPurchasePlatformMatchingAttributeTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProPurchasePlatformMatchingAttributeTest.kt @@ -72,6 +72,7 @@ class RMFPProPurchasePlatformMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "Google", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProPurchasePlatformMatchingAttribute(listOf("google", "ios"))) @@ -91,6 +92,7 @@ class RMFPProPurchasePlatformMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "iOS", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProPurchasePlatformMatchingAttribute(listOf("android"))) @@ -121,6 +123,7 @@ class RMFPProPurchasePlatformMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "", + activeOffers = listOf(), ), ) val result = matcher.evaluate(PProPurchasePlatformMatchingAttribute(listOf("android"))) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProSubscriptionStatusMatchingAttributeTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProSubscriptionStatusMatchingAttributeTest.kt index 354ce9231fae..0a4eadfef75f 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProSubscriptionStatusMatchingAttributeTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/rmf/RMFPProSubscriptionStatusMatchingAttributeTest.kt @@ -30,6 +30,7 @@ class RMFPProSubscriptionStatusMatchingAttributeTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "Google", + activeOffers = listOf(), ) @Before diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/survey/PproSurveyParameterPluginsTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/survey/PproSurveyParameterPluginsTest.kt index 5e764fc9e966..36510bb2a1b1 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/survey/PproSurveyParameterPluginsTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/survey/PproSurveyParameterPluginsTest.kt @@ -29,6 +29,7 @@ class PproSurveyParameterPluginTest { expiresOrRenewsAt = 1719525600000, // June 27 UTC status = AUTO_RENEWABLE, platform = "android", + activeOffers = listOf(), ) @Before diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt index 9441379d3e0f..03088781b6af 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt @@ -197,6 +197,7 @@ class RestoreSubscriptionViewModelTest { expiresOrRenewsAt = 10000L, status = AUTO_RENEWABLE, platform = "google", + activeOffers = listOf(), ) } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt index 89d7953d7043..8dd2a5196ed5 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt @@ -66,6 +66,7 @@ class SubscriptionSettingsViewModelTest { expiresOrRenewsAt = 1701694623000, status = AUTO_RENEWABLE, platform = "android", + activeOffers = listOf(), ), ) @@ -93,6 +94,7 @@ class SubscriptionSettingsViewModelTest { expiresOrRenewsAt = 1701694623000, status = AUTO_RENEWABLE, platform = "android", + activeOffers = listOf(), ), ) @@ -120,6 +122,7 @@ class SubscriptionSettingsViewModelTest { expiresOrRenewsAt = 1701694623000, status = AUTO_RENEWABLE, platform = "android", + activeOffers = listOf(), ), ) @@ -147,6 +150,7 @@ class SubscriptionSettingsViewModelTest { expiresOrRenewsAt = 1701694623000, status = AUTO_RENEWABLE, platform = "android", + activeOffers = listOf(), ), ) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt index 68c1c2ad47f3..6e88a3de5e37 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt @@ -23,6 +23,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN_US import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.freetrial.FreeTrialExperimentDataStore import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettings @@ -38,7 +39,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.json.JSONObject -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -60,6 +63,7 @@ class SubscriptionWebViewViewModelTest { private val subscriptionsChecker: SubscriptionsChecker = mock() private val pixelSender: SubscriptionPixelSender = mock() private val privacyProFeature = FakeFeatureToggleFactory.create(PrivacyProFeature::class.java, FakeToggleStore()) + private val freeTrialExperimentDataStore: FreeTrialExperimentDataStore = mock() private lateinit var viewModel: SubscriptionWebViewViewModel @@ -73,6 +77,7 @@ class SubscriptionWebViewViewModelTest { networkProtectionAccessState, pixelSender, privacyProFeature, + freeTrialExperimentDataStore, ) givenSubscriptionStatus(UNKNOWN) }