Skip to content

Commit fbf374c

Browse files
committed
Update subscription flow welcome page (#6260)
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1210461583262419?focus=true ### Description Updates JS messaging in subscription flows welcome pages. Touched methods: - ~Adds getAuthAccessToken~ - Adds getFeatureConfig - Adds Duck.ai support in featureSelected Feature flagging: - ~privacyProFeature.subscriptionMessaging() -> enables getFeatureConfig~ - privacyProFeature.duckAiPlus() -> signales FE Duck.ai is enabled - Token is not affected by FF ### Steps to test this PR _Feature 1_ - [x] apply patch attached in the Asana task (that will target staging and specific FE sandbox) - [x] install the branch - [x] Ensure you don't have a subscription - [x] Start purchase a subscription - [x] In the landing page, scroll to the bottom. - [x] Ensure Duck.ai appears in the comparison chart. - [x] Purchase a subscription - [x] After purchase, welcome page should list Duck.ai as a card - [x] Click on the Card - [x] Ensure pixel `m_privacy-pro_welcome_paid-ai-chat_click` emits - [x] Ensure you navigate to Duck chat (this branch doesn't have the code to signal the token so you won't see the subscriber mode) _Feature 2_ - [x] Go to feature flags and disable privacypro - duckAiPlus - [x] Cancel and delete your subscription - [x] Repeat the process again - [x] Ensure in Landing page comparison chart, Duck.ai doesn't show. - [x] Ensure when subscription is purchased, there's no Duck.ai card _Feature 3_ - [x] Remove all files changed in previous patch - [x] Apply standard patch from https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true (no FE sandbox) - [x] install the branch - [x] Ensure you don't have a subscription - [x] Purchase a subscription - [x] ensure purchase succeeds - [x] ensure no duck.ai is present ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent cfbf664 commit fbf374c

File tree

10 files changed

+114
-2
lines changed

10 files changed

+114
-2
lines changed

subscriptions/subscriptions-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060
implementation project(':survey-api')
6161
implementation project(':vpn-api')
6262
implementation project(':content-scope-scripts-api')
63+
implementation project(':duckchat-api')
6364

6465
implementation AndroidX.appCompat
6566
implementation KotlinX.coroutines.core

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ interface PrivacyProFeature {
168168
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
169169
fun privacyProFreeTrial(): Toggle
170170

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

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ object SubscriptionsConstants {
4545
const val ITR = "Identity Theft Restoration"
4646
const val ROW_ITR = "Global Identity Theft Restoration"
4747
const val PIR = "Data Broker Protection"
48+
const val DUCK_AI = "Duck.ai"
4849

4950
// Platform
5051
const val PLATFORM = "android"

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,10 @@ class SubscriptionMessagingInterface @Inject constructor(
406406
if (privacyProFeature.enableNewSubscriptionMessages().isEnabled().not()) return
407407

408408
val authV2Enabled = privacyProFeature.enableSubscriptionFlowsV2().isEnabled()
409+
val duckAiSubscriberModelsEnabled = privacyProFeature.duckAiPlus().isEnabled()
409410
val resultJson = JSONObject().apply {
410411
put("useSubscriptionsAuthV2", authV2Enabled)
412+
put("usePaidDuckAi", duckAiSubscriberModelsEnabled)
411413
}
412414

413415
val response = JsRequestResponse.Success(

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ enum class SubscriptionPixel(
141141
type = Unique(),
142142
includedParameters = setOf(ATB, APP_VERSION),
143143
),
144+
ONBOARDING_DUCK_AI_CLICK(
145+
baseName = "m_privacy-pro_welcome_paid-ai-chat_click",
146+
type = Unique(),
147+
includedParameters = setOf(ATB, APP_VERSION),
148+
),
144149
SUBSCRIPTION_SETTINGS_SHOWN(
145150
baseName = "m_privacy-pro_settings_screen_impression",
146151
type = Count,

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_
3939
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN
4040
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK
4141
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_ADD_DEVICE_CLICK
42+
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_DUCK_AI_CLICK
4243
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_IDTR_CLICK
4344
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_PIR_CLICK
4445
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_VPN_CLICK
@@ -89,6 +90,7 @@ interface SubscriptionPixelSender {
8990
fun reportOnboardingVpnClick()
9091
fun reportOnboardingPirClick()
9192
fun reportOnboardingIdtrClick()
93+
fun reportOnboardingDuckAiClick()
9294
fun reportSubscriptionSettingsShown()
9395
fun reportAppSettingsPirClick()
9496
fun reportAppSettingsIdtrClick()
@@ -197,6 +199,9 @@ class SubscriptionPixelSenderImpl @Inject constructor(
197199
override fun reportOnboardingIdtrClick() =
198200
fire(ONBOARDING_IDTR_CLICK)
199201

202+
override fun reportOnboardingDuckAiClick() =
203+
fire(ONBOARDING_DUCK_AI_CLICK)
204+
200205
override fun reportSubscriptionSettingsShown() =
201206
fire(SUBSCRIPTION_SETTINGS_SHOWN)
202207

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
3232
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
3333
import com.duckduckgo.subscriptions.impl.SubscriptionOffer
3434
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
35+
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.DUCK_AI
3536
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR
3637
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_ITR
3738
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_NETP
@@ -193,13 +194,15 @@ class SubscriptionWebViewViewModel @Inject constructor(
193194
NETP, LEGACY_FE_NETP -> networkProtectionAccessState.getScreenForCurrentState()?.let { GoToNetP(it) }
194195
ITR, LEGACY_FE_ITR, ROW_ITR -> GoToITR
195196
PIR, LEGACY_FE_PIR -> GoToPIR
197+
DUCK_AI -> GoToDuckAI
196198
else -> null
197199
}
198200
if (hasPurchasedSubscription()) {
199201
when (commandToSend) {
200202
GoToITR -> pixelSender.reportOnboardingIdtrClick()
201203
is GoToNetP -> pixelSender.reportOnboardingVpnClick()
202204
GoToPIR -> pixelSender.reportOnboardingPirClick()
205+
GoToDuckAI -> pixelSender.reportOnboardingDuckAiClick()
203206
else -> {} // no-op
204207
}
205208
}
@@ -428,6 +431,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
428431
data object GoToITR : Command()
429432
data object GoToPIR : Command()
430433
data class GoToNetP(val activityParams: ActivityParams) : Command()
434+
data object GoToDuckAI : Command()
431435
data object Reload : Command()
432436
}
433437

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import com.duckduckgo.downloads.api.DownloadStateListener
5858
import com.duckduckgo.downloads.api.DownloadsFileActions
5959
import com.duckduckgo.downloads.api.FileDownloader
6060
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
61+
import com.duckduckgo.duckchat.api.DuckChat
6162
import com.duckduckgo.js.messaging.api.JsCallbackData
6263
import com.duckduckgo.js.messaging.api.JsMessageCallback
6364
import com.duckduckgo.js.messaging.api.JsMessaging
@@ -78,6 +79,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
7879
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command
7980
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettings
8081
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettingsActivateSuccess
82+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToDuckAI
8183
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToITR
8284
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToNetP
8385
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToPIR
@@ -161,6 +163,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
161163
@Inject
162164
lateinit var pixelSender: SubscriptionPixelSender
163165

166+
@Inject
167+
lateinit var duckChat: DuckChat
168+
164169
private val viewModel: SubscriptionWebViewViewModel by bindViewModel()
165170

166171
private val binding: ActivitySubscriptionsWebviewBinding by viewBinding()
@@ -421,6 +426,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
421426
is GoToITR -> goToITR()
422427
is GoToPIR -> goToPIR()
423428
is GoToNetP -> goToNetP(command.activityParams)
429+
is GoToDuckAI -> goToDuckAI()
424430
Reload -> binding.webview.reload()
425431
}
426432
}
@@ -446,6 +452,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
446452
globalActivityStarter.start(this, params)
447453
}
448454

455+
private fun goToDuckAI() {
456+
duckChat.openDuckChat()
457+
}
458+
449459
private fun renderPurchaseState(purchaseState: PurchaseStateView) {
450460
when (purchaseState) {
451461
is PurchaseStateView.InProgress, PurchaseStateView.Inactive -> {

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,13 +821,14 @@ class SubscriptionMessagingInterfaceTest {
821821
givenInterfaceIsRegistered()
822822
givenSubscriptionMessaging(enabled = true)
823823
givenAuthV2(enabled = true)
824+
givenDuckAiPlus(enabled = true)
824825

825826
val expected = JsRequestResponse.Success(
826827
context = "subscriptionPages",
827828
featureName = "useSubscription",
828829
method = "getFeatureConfig",
829830
id = "myId",
830-
result = JSONObject("""{"useSubscriptionsAuthV2":true}"""),
831+
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":true}"""),
831832
)
832833

833834
val message = """
@@ -849,13 +850,43 @@ class SubscriptionMessagingInterfaceTest {
849850
givenInterfaceIsRegistered()
850851
givenSubscriptionMessaging(enabled = true)
851852
givenAuthV2(enabled = false)
853+
givenDuckAiPlus(enabled = true)
852854

853855
val expected = JsRequestResponse.Success(
854856
context = "subscriptionPages",
855857
featureName = "useSubscription",
856858
method = "getFeatureConfig",
857859
id = "myId",
858-
result = JSONObject("""{"useSubscriptionsAuthV2":false}"""),
860+
result = JSONObject("""{"useSubscriptionsAuthV2":false,"usePaidDuckAi":true}"""),
861+
)
862+
863+
val message = """
864+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
865+
""".trimIndent()
866+
867+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
868+
869+
val captor = argumentCaptor<JsRequestResponse>()
870+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
871+
val jsMessage = captor.firstValue
872+
873+
assertTrue(jsMessage is JsRequestResponse.Success)
874+
checkEquals(expected, jsMessage)
875+
}
876+
877+
@Test
878+
fun `when process and get feature config and messaging enabled but duck ai plus disabled then return response with duck ai false`() = runTest {
879+
givenInterfaceIsRegistered()
880+
givenSubscriptionMessaging(enabled = true)
881+
givenAuthV2(enabled = true)
882+
givenDuckAiPlus(enabled = false)
883+
884+
val expected = JsRequestResponse.Success(
885+
context = "subscriptionPages",
886+
featureName = "useSubscription",
887+
method = "getFeatureConfig",
888+
id = "myId",
889+
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":false}"""),
859890
)
860891

861892
val message = """
@@ -951,6 +982,12 @@ class SubscriptionMessagingInterfaceTest {
951982
whenever(privacyProFeature.enableSubscriptionFlowsV2()).thenReturn(v2SubscriptionFlow)
952983
}
953984

985+
private fun givenDuckAiPlus(enabled: Boolean) {
986+
val duckAiPlusToggle = mock<com.duckduckgo.feature.toggles.api.Toggle>()
987+
whenever(duckAiPlusToggle.isEnabled()).thenReturn(enabled)
988+
whenever(privacyProFeature.duckAiPlus()).thenReturn(duckAiPlusToggle)
989+
}
990+
954991
private fun checkEquals(expected: JsRequestResponse, actual: JsRequestResponse) {
955992
if (expected is JsRequestResponse.Success && actual is JsRequestResponse.Success) {
956993
assertEquals(expected.id, actual.id)

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,20 @@ class SubscriptionWebViewViewModelTest {
487487
}
488488
}
489489

490+
@Test
491+
fun whenFeatureSelectedAndFeatureIsDuckAiThenCommandSent() = runTest {
492+
givenSubscriptionStatus(EXPIRED)
493+
viewModel.commands().test {
494+
viewModel.processJsCallbackMessage(
495+
"test",
496+
"featureSelected",
497+
null,
498+
JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
499+
)
500+
assertTrue(awaitItem() is Command.GoToDuckAI)
501+
}
502+
}
503+
490504
@Test
491505
fun whenSubscriptionSelectedThenPixelIsSent() = runTest {
492506
viewModel.processJsCallbackMessage(
@@ -622,6 +636,34 @@ class SubscriptionWebViewViewModelTest {
622636
verifyNoInteractions(pixelSender)
623637
}
624638

639+
@Test
640+
fun whenFeatureSelectedAndFeatureIsDuckAiAndInPurchaseFlowThenPixelIsSent() = runTest {
641+
givenSubscriptionStatus(AUTO_RENEWABLE)
642+
whenever(subscriptionsManager.currentPurchaseState).thenReturn(flowOf(CurrentPurchase.Success))
643+
viewModel.start()
644+
645+
viewModel.processJsCallbackMessage(
646+
featureName = "test",
647+
method = "featureSelected",
648+
id = null,
649+
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
650+
)
651+
verify(pixelSender).reportOnboardingDuckAiClick()
652+
}
653+
654+
@Test
655+
fun whenFeatureSelectedAndFeatureIsDuckAiAndNotInPurchaseFlowThenPixelIsNotSent() = runTest {
656+
givenSubscriptionStatus(AUTO_RENEWABLE)
657+
658+
viewModel.processJsCallbackMessage(
659+
featureName = "test",
660+
method = "featureSelected",
661+
id = null,
662+
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
663+
)
664+
verifyNoInteractions(pixelSender)
665+
}
666+
625667
@Test
626668
fun whenSubscriptionsWelcomeFaqClickedAndInPurchaseFlowThenPixelIsSent() = runTest {
627669
givenSubscriptionStatus(AUTO_RENEWABLE)

0 commit comments

Comments
 (0)