Skip to content

Update subscription flow welcome page #6260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions subscriptions/subscriptions-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
implementation project(':survey-api')
implementation project(':vpn-api')
implementation project(':content-scope-scripts-api')
implementation project(':duckchat-api')

implementation AndroidX.appCompat
implementation KotlinX.coroutines.core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ interface PrivacyProFeature {
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
fun privacyProFreeTrial(): Toggle

/**
* Enables/Disables duckAi for subscribers (advanced models)
* This flag is used to hide the feature in the native client and FE.
* It will be used for the feature rollout and kill-switch if necessary.
*/
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
fun duckAiPlus(): Toggle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ object SubscriptionsConstants {
const val ITR = "Identity Theft Restoration"
const val ROW_ITR = "Global Identity Theft Restoration"
const val PIR = "Data Broker Protection"
const val DUCK_AI = "Duck.ai"

// Platform
const val PLATFORM = "android"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,10 @@ class SubscriptionMessagingInterface @Inject constructor(
if (privacyProFeature.enableNewSubscriptionMessages().isEnabled().not()) return

val authV2Enabled = privacyProFeature.enableSubscriptionFlowsV2().isEnabled()
val duckAiSubscriberModelsEnabled = privacyProFeature.duckAiPlus().isEnabled()
val resultJson = JSONObject().apply {
put("useSubscriptionsAuthV2", authV2Enabled)
put("usePaidDuckAi", duckAiSubscriberModelsEnabled)
}

val response = JsRequestResponse.Success(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ enum class SubscriptionPixel(
type = Unique(),
includedParameters = setOf(ATB, APP_VERSION),
),
ONBOARDING_DUCK_AI_CLICK(
baseName = "m_privacy-pro_welcome_paid-ai-chat_click",
type = Unique(),
includedParameters = setOf(ATB, APP_VERSION),
),
SUBSCRIPTION_SETTINGS_SHOWN(
baseName = "m_privacy-pro_settings_screen_impression",
type = Count,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_ADD_DEVICE_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_DUCK_AI_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_IDTR_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_PIR_CLICK
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_VPN_CLICK
Expand Down Expand Up @@ -89,6 +90,7 @@ interface SubscriptionPixelSender {
fun reportOnboardingVpnClick()
fun reportOnboardingPirClick()
fun reportOnboardingIdtrClick()
fun reportOnboardingDuckAiClick()
fun reportSubscriptionSettingsShown()
fun reportAppSettingsPirClick()
fun reportAppSettingsIdtrClick()
Expand Down Expand Up @@ -197,6 +199,9 @@ class SubscriptionPixelSenderImpl @Inject constructor(
override fun reportOnboardingIdtrClick() =
fire(ONBOARDING_IDTR_CLICK)

override fun reportOnboardingDuckAiClick() =
fire(ONBOARDING_DUCK_AI_CLICK)

override fun reportSubscriptionSettingsShown() =
fire(SUBSCRIPTION_SETTINGS_SHOWN)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
import com.duckduckgo.subscriptions.impl.SubscriptionOffer
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.DUCK_AI
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_ITR
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_NETP
Expand Down Expand Up @@ -193,13 +194,15 @@ class SubscriptionWebViewViewModel @Inject constructor(
NETP, LEGACY_FE_NETP -> networkProtectionAccessState.getScreenForCurrentState()?.let { GoToNetP(it) }
ITR, LEGACY_FE_ITR, ROW_ITR -> GoToITR
PIR, LEGACY_FE_PIR -> GoToPIR
DUCK_AI -> GoToDuckAI
else -> null
}
if (hasPurchasedSubscription()) {
when (commandToSend) {
GoToITR -> pixelSender.reportOnboardingIdtrClick()
is GoToNetP -> pixelSender.reportOnboardingVpnClick()
GoToPIR -> pixelSender.reportOnboardingPirClick()
GoToDuckAI -> pixelSender.reportOnboardingDuckAiClick()
else -> {} // no-op
}
}
Expand Down Expand Up @@ -428,6 +431,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
data object GoToITR : Command()
data object GoToPIR : Command()
data class GoToNetP(val activityParams: ActivityParams) : Command()
data object GoToDuckAI : Command()
data object Reload : Command()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.DownloadsFileActions
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
Expand All @@ -78,6 +79,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettings
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettingsActivateSuccess
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToDuckAI
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToITR
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToNetP
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToPIR
Expand Down Expand Up @@ -161,6 +163,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
@Inject
lateinit var pixelSender: SubscriptionPixelSender

@Inject
lateinit var duckChat: DuckChat

private val viewModel: SubscriptionWebViewViewModel by bindViewModel()

private val binding: ActivitySubscriptionsWebviewBinding by viewBinding()
Expand Down Expand Up @@ -422,6 +427,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
is GoToITR -> goToITR()
is GoToPIR -> goToPIR()
is GoToNetP -> goToNetP(command.activityParams)
is GoToDuckAI -> goToDuckAI()
Reload -> binding.webview.reload()
}
}
Expand All @@ -447,6 +453,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
globalActivityStarter.start(this, params)
}

private fun goToDuckAI() {
duckChat.openDuckChat()
}

private fun renderPurchaseState(purchaseState: PurchaseStateView) {
when (purchaseState) {
is PurchaseStateView.InProgress, PurchaseStateView.Inactive -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,13 +790,14 @@ class SubscriptionMessagingInterfaceTest {
givenInterfaceIsRegistered()
givenSubscriptionMessaging(enabled = true)
givenAuthV2(enabled = true)
givenDuckAiPlus(enabled = true)

val expected = JsRequestResponse.Success(
context = "subscriptionPages",
featureName = "useSubscription",
method = "getFeatureConfig",
id = "myId",
result = JSONObject("""{"useSubscriptionsAuthV2":true}"""),
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":true}"""),
)

val message = """
Expand All @@ -818,13 +819,43 @@ class SubscriptionMessagingInterfaceTest {
givenInterfaceIsRegistered()
givenSubscriptionMessaging(enabled = true)
givenAuthV2(enabled = false)
givenDuckAiPlus(enabled = true)

val expected = JsRequestResponse.Success(
context = "subscriptionPages",
featureName = "useSubscription",
method = "getFeatureConfig",
id = "myId",
result = JSONObject("""{"useSubscriptionsAuthV2":false}"""),
result = JSONObject("""{"useSubscriptionsAuthV2":false,"usePaidDuckAi":true}"""),
)

val message = """
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
""".trimIndent()

messagingInterface.process(message, "duckduckgo-android-messaging-secret")

val captor = argumentCaptor<JsRequestResponse>()
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
val jsMessage = captor.firstValue

assertTrue(jsMessage is JsRequestResponse.Success)
checkEquals(expected, jsMessage)
}

@Test
fun `when process and get feature config and messaging enabled but duck ai plus disabled then return response with duck ai false`() = runTest {
givenInterfaceIsRegistered()
givenSubscriptionMessaging(enabled = true)
givenAuthV2(enabled = true)
givenDuckAiPlus(enabled = false)

val expected = JsRequestResponse.Success(
context = "subscriptionPages",
featureName = "useSubscription",
method = "getFeatureConfig",
id = "myId",
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":false}"""),
)

val message = """
Expand Down Expand Up @@ -920,6 +951,12 @@ class SubscriptionMessagingInterfaceTest {
whenever(privacyProFeature.enableSubscriptionFlowsV2()).thenReturn(v2SubscriptionFlow)
}

private fun givenDuckAiPlus(enabled: Boolean) {
val duckAiPlusToggle = mock<com.duckduckgo.feature.toggles.api.Toggle>()
whenever(duckAiPlusToggle.isEnabled()).thenReturn(enabled)
whenever(privacyProFeature.duckAiPlus()).thenReturn(duckAiPlusToggle)
}

private fun checkEquals(expected: JsRequestResponse, actual: JsRequestResponse) {
if (expected is JsRequestResponse.Success && actual is JsRequestResponse.Success) {
assertEquals(expected.id, actual.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,20 @@ class SubscriptionWebViewViewModelTest {
}
}

@Test
fun whenFeatureSelectedAndFeatureIsDuckAiThenCommandSent() = runTest {
givenSubscriptionStatus(EXPIRED)
viewModel.commands().test {
viewModel.processJsCallbackMessage(
"test",
"featureSelected",
null,
JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
)
assertTrue(awaitItem() is Command.GoToDuckAI)
}
}

@Test
fun whenSubscriptionSelectedThenPixelIsSent() = runTest {
viewModel.processJsCallbackMessage(
Expand Down Expand Up @@ -622,6 +636,34 @@ class SubscriptionWebViewViewModelTest {
verifyNoInteractions(pixelSender)
}

@Test
fun whenFeatureSelectedAndFeatureIsDuckAiAndInPurchaseFlowThenPixelIsSent() = runTest {
givenSubscriptionStatus(AUTO_RENEWABLE)
whenever(subscriptionsManager.currentPurchaseState).thenReturn(flowOf(CurrentPurchase.Success))
viewModel.start()

viewModel.processJsCallbackMessage(
featureName = "test",
method = "featureSelected",
id = null,
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
)
verify(pixelSender).reportOnboardingDuckAiClick()
}

@Test
fun whenFeatureSelectedAndFeatureIsDuckAiAndNotInPurchaseFlowThenPixelIsNotSent() = runTest {
givenSubscriptionStatus(AUTO_RENEWABLE)

viewModel.processJsCallbackMessage(
featureName = "test",
method = "featureSelected",
id = null,
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
)
verifyNoInteractions(pixelSender)
}

@Test
fun whenSubscriptionsWelcomeFaqClickedAndInPurchaseFlowThenPixelIsSent() = runTest {
givenSubscriptionStatus(AUTO_RENEWABLE)
Expand Down
Loading