Skip to content

Commit ffe9b5a

Browse files
authored
capture featurepage when redirecting to subscription flows (#6465)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1149059203486286/task/1210858657604808?focus=true ### Description Capture feature page query param when redirecting to subscription flows ### Steps to test this PR _Feature 1_ - [x] change debug package so it doesn't include `.debug` (to make subscriptions work) - [x] install the branch - [x] Visit https://duckduckgo.com/pro?something=true&featurePage=duckai - [x] Ensure subscription flow opens loading (https://duckduckgo.com/subscriptions?something=true&featurePage=duckai) - [x] Ensure `m_privacy-pro_offer_screen_impression` pixel is sent ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent 55ec984 commit ffe9b5a

File tree

7 files changed

+117
-16
lines changed

7 files changed

+117
-16
lines changed

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/ui/SubscriptionsHandler.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class SubscriptionsHandler @Inject constructor(
3636
private val dispatcherProvider: DispatcherProvider,
3737
) {
3838

39+
private val defaultDuckAiSubscriptionPurchase = SubscriptionPurchase(featurePage = DUCK_AI_FEATURE_PAGE)
40+
3941
fun handleSubscriptionsFeature(
4042
featureName: String,
4143
method: String,
@@ -68,11 +70,12 @@ class SubscriptionsHandler @Inject constructor(
6870

6971
METHOD_OPEN_SUBSCRIPTION_PURCHASE -> {
7072
val subscriptionParams = runCatching {
71-
data?.getString(MESSAGE_PARAM_ORIGIN_KEY).takeUnless { it.isNullOrBlank() }
73+
data?.getString(MESSAGE_PARAM_ORIGIN_KEY)
74+
.takeUnless { it.isNullOrBlank() }
7275
?.let { nonEmptyOrigin ->
73-
SubscriptionPurchase(nonEmptyOrigin)
74-
} ?: SubscriptionPurchase()
75-
}.getOrDefault(SubscriptionPurchase())
76+
defaultDuckAiSubscriptionPurchase.copy(origin = nonEmptyOrigin)
77+
} ?: defaultDuckAiSubscriptionPurchase
78+
}.getOrDefault(defaultDuckAiSubscriptionPurchase)
7679

7780
withContext(dispatcherProvider.main()) {
7881
globalActivityStarter.start(context, subscriptionParams)
@@ -89,5 +92,6 @@ class SubscriptionsHandler @Inject constructor(
8992
private const val METHOD_OPEN_SUBSCRIPTION_ACTIVATION = "openSubscriptionActivation"
9093
private const val METHOD_OPEN_SUBSCRIPTION_PURCHASE = "openSubscriptionPurchase"
9194
private const val MESSAGE_PARAM_ORIGIN_KEY = "origin"
95+
private const val DUCK_AI_FEATURE_PAGE = "duckai"
9296
}
9397
}

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/SubscriptionsHandlerTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class SubscriptionsHandlerTest {
185185
contentScopeScripts,
186186
)
187187

188-
verify(globalActivityStarter).start(context, SubscriptionPurchase())
188+
verify(globalActivityStarter).start(context, SubscriptionPurchase(featurePage = "duckai"))
189189
}
190190

191191
@Test
@@ -208,7 +208,7 @@ class SubscriptionsHandlerTest {
208208
contentScopeScripts,
209209
)
210210

211-
verify(globalActivityStarter).start(context, SubscriptionPurchase("duckai_chat"))
211+
verify(globalActivityStarter).start(context, SubscriptionPurchase(origin = "duckai_chat", featurePage = "duckai"))
212212
}
213213

214214
@Test
@@ -231,7 +231,7 @@ class SubscriptionsHandlerTest {
231231
contentScopeScripts,
232232
)
233233

234-
verify(globalActivityStarter).start(context, SubscriptionPurchase())
234+
verify(globalActivityStarter).start(context, SubscriptionPurchase(featurePage = "duckai"))
235235
}
236236

237237
@Test
@@ -254,7 +254,7 @@ class SubscriptionsHandlerTest {
254254
contentScopeScripts,
255255
)
256256

257-
verify(globalActivityStarter).start(context, SubscriptionPurchase())
257+
verify(globalActivityStarter).start(context, SubscriptionPurchase(featurePage = "duckai"))
258258
}
259259

260260
@Test
@@ -302,6 +302,6 @@ class SubscriptionsHandlerTest {
302302
)
303303

304304
verify(subscriptionsJSHelper).processJsCallbackMessage(featureName, method, id, data)
305-
verify(globalActivityStarter).start(context, SubscriptionPurchase())
305+
verify(globalActivityStarter).start(context, SubscriptionPurchase(featurePage = "duckai"))
306306
}
307307
}

subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/SubscriptionScreens.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
2121
sealed class SubscriptionScreens {
2222
data object SubscriptionsSettingsScreenWithEmptyParams : ActivityParams
2323
data class RestoreSubscriptionScreenWithParams(val isOriginWeb: Boolean = true) : ActivityParams
24-
data class SubscriptionPurchase(val origin: String? = null) : ActivityParams
24+
data class SubscriptionPurchase(val origin: String? = null, val featurePage: String? = null) : ActivityParams
2525
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter
3838
import com.duckduckgo.subscriptions.api.Product
3939
import com.duckduckgo.subscriptions.api.SubscriptionStatus
4040
import com.duckduckgo.subscriptions.api.Subscriptions
41+
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL
4142
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.PRIVACY_PRO_ETLD
4243
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.PRIVACY_PRO_PATH
4344
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.PRIVACY_SUBSCRIPTIONS_PATH
@@ -102,7 +103,7 @@ class RealSubscriptions @Inject constructor(
102103
val privacyPro = globalActivityStarter.startIntent(
103104
context,
104105
SubscriptionsWebViewActivityWithParams(
105-
url = SubscriptionsConstants.BUY_URL,
106+
url = buildSubscriptionUrl(uri),
106107
origin = origin,
107108
),
108109
) ?: return
@@ -135,6 +136,15 @@ class RealSubscriptions @Inject constructor(
135136
override suspend fun isFreeTrialEligible(): Boolean {
136137
return subscriptionsManager.isFreeTrialEligible()
137138
}
139+
140+
private fun buildSubscriptionUrl(uri: Uri?): String {
141+
val queryParams = uri?.query
142+
return if (!queryParams.isNullOrBlank()) {
143+
"$BUY_URL?$queryParams"
144+
} else {
145+
BUY_URL
146+
}
147+
}
138148
}
139149

140150
@ContributesRemoteFeature(

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
@@ -61,6 +61,7 @@ object SubscriptionsConstants {
6161
const val ITR_URL = "https://duckduckgo.com/identity-theft-restoration"
6262
const val FAQS_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/"
6363
const val PRIVACY_PRO_ETLD = "duckduckgo.com"
64+
const val FEATURE_PAGE_QUERY_PARAM_KEY = "featurePage"
6465
const val PRIVACY_PRO_PATH = "pro"
6566
const val PRIVACY_SUBSCRIPTIONS_PATH = "subscriptions"
6667
}

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
3434
import androidx.annotation.AnyThread
3535
import androidx.appcompat.widget.Toolbar
3636
import androidx.core.content.ContextCompat
37+
import androidx.core.net.toUri
3738
import androidx.fragment.app.DialogFragment
3839
import androidx.lifecycle.Lifecycle
3940
import androidx.lifecycle.flowWithLifecycle
@@ -70,10 +71,12 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
7071
import com.duckduckgo.navigation.api.getActivityParams
7172
import com.duckduckgo.subscriptions.api.SubscriptionScreens.RestoreSubscriptionScreenWithParams
7273
import com.duckduckgo.subscriptions.api.SubscriptionScreens.SubscriptionPurchase
74+
import com.duckduckgo.subscriptions.api.Subscriptions
7375
import com.duckduckgo.subscriptions.impl.R.string
7476
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants
7577
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ACTIVATE_URL
7678
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL
79+
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.FEATURE_PAGE_QUERY_PARAM_KEY
7780
import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionsWebviewBinding
7881
import com.duckduckgo.subscriptions.impl.pir.PirActivity.Companion.PirScreenWithEmptyParams
7982
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
@@ -105,6 +108,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
105108
import kotlinx.coroutines.flow.launchIn
106109
import kotlinx.coroutines.flow.onEach
107110
import kotlinx.coroutines.launch
111+
import logcat.logcat
108112
import org.json.JSONObject
109113

110114
data class SubscriptionsWebViewActivityWithParams(
@@ -168,6 +172,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
168172
@Inject
169173
lateinit var duckChat: DuckChat
170174

175+
@Inject
176+
lateinit var subscriptions: Subscriptions
177+
171178
private val viewModel: SubscriptionWebViewViewModel by bindViewModel()
172179

173180
private val binding: ActivitySubscriptionsWebviewBinding by viewBinding()
@@ -184,7 +191,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
184191
override fun onCreate(savedInstanceState: Bundle?) {
185192
super.onCreate(savedInstanceState)
186193

187-
params = convertIntoSubscriptionWebViewActivityParams(intent)
194+
params = convertIntoSubscriptionWebViewActivityParams(intent).also {
195+
logcat { "Subscription Flow: entering with params $it" }
196+
}
188197

189198
setContentView(binding.root)
190199

@@ -266,7 +275,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
266275
renderPurchaseState(it.purchaseState)
267276
}.launchIn(lifecycleScope)
268277

269-
if (savedInstanceState == null && params.url == BUY_URL) {
278+
val isPrivacyProUrl by lazy {
279+
runCatching { subscriptions.isPrivacyProUrl(params.url.toUri()) }.getOrDefault(false)
280+
}
281+
if (savedInstanceState == null && isPrivacyProUrl) {
270282
viewModel.paywallShown()
271283
}
272284
}
@@ -280,11 +292,26 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
280292
}
281293

282294
private fun convertIntoSubscriptionWebViewActivityParams(intent: Intent): SubscriptionsWebViewActivityWithParams {
283-
intent.getActivityParams(SubscriptionPurchase::class.java)?.let {
295+
intent.getActivityParams(SubscriptionPurchase::class.java)?.let { subscriptionPurchaseActivityParams ->
284296
return SubscriptionsWebViewActivityWithParams(
285297
url = BUY_URL,
286-
origin = it.origin,
287-
)
298+
origin = subscriptionPurchaseActivityParams.origin,
299+
).let { webViewActivityWithParams ->
300+
if (subscriptionPurchaseActivityParams.featurePage.isNullOrBlank().not()) {
301+
val urlWithParams = kotlin.runCatching {
302+
BUY_URL.toUri()
303+
.buildUpon()
304+
.appendQueryParameter(FEATURE_PAGE_QUERY_PARAM_KEY, subscriptionPurchaseActivityParams.featurePage)
305+
.build()
306+
.toString()
307+
}.getOrDefault(BUY_URL)
308+
webViewActivityWithParams.copy(
309+
url = urlWithParams,
310+
)
311+
} else {
312+
webViewActivityWithParams
313+
}
314+
}
288315
}
289316

290317
return intent.getActivityParams(SubscriptionsWebViewActivityWithParams::class.java)

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.duckduckgo.subscriptions.api.Product.NetP
3030
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
3131
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
3232
import com.duckduckgo.subscriptions.api.SubscriptionStatus.WAITING
33+
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL
3334
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
3435
import com.duckduckgo.subscriptions.impl.ui.SubscriptionsWebViewActivityWithParams
3536
import kotlinx.coroutines.flow.flowOf
@@ -197,6 +198,64 @@ class RealSubscriptionsTest {
197198
assertEquals("test", (captor.lastValue as SubscriptionsWebViewActivityWithParams).origin)
198199
}
199200

201+
@Test
202+
fun whenLaunchProUrlWithFeaturePageThenIncludeInSubscriptionURLToActivity() = runTest {
203+
whenever(globalActivityStarter.startIntent(any(), any<SettingsScreenNoParams>())).thenReturn(fakeIntent())
204+
whenever(globalActivityStarter.startIntent(any(), any<SubscriptionsWebViewActivityWithParams>())).thenReturn(fakeIntent())
205+
206+
val captor = argumentCaptor<ActivityParams>()
207+
subscriptions.launchPrivacyPro(context, "https://duckduckgo.com/pro?featurePage=duckai".toUri())
208+
209+
verify(globalActivityStarter, times(2)).startIntent(eq(context), captor.capture())
210+
assertEquals("$BUY_URL?featurePage=duckai", (captor.lastValue as SubscriptionsWebViewActivityWithParams).url)
211+
}
212+
213+
@Test
214+
fun whenLaunchProWithMultipleQueryParametersThenTheyAreIncludedInSubscriptionURLToActivity() = runTest {
215+
whenever(globalActivityStarter.startIntent(any(), any<SettingsScreenNoParams>())).thenReturn(fakeIntent())
216+
whenever(globalActivityStarter.startIntent(any(), any<SubscriptionsWebViewActivityWithParams>())).thenReturn(fakeIntent())
217+
218+
val captor = argumentCaptor<ActivityParams>()
219+
subscriptions.launchPrivacyPro(context, "https://duckduckgo.com/pro?usePaidDuckAi=true&featurePage=duckai".toUri())
220+
221+
verify(globalActivityStarter, times(2)).startIntent(eq(context), captor.capture())
222+
assertEquals("$BUY_URL?usePaidDuckAi=true&featurePage=duckai", (captor.lastValue as SubscriptionsWebViewActivityWithParams).url)
223+
}
224+
225+
@Test
226+
fun whenLaunchSubscriptionUrlWithFeaturePageThenIncludeInSubscriptionURLToActivity() = runTest {
227+
whenever(globalActivityStarter.startIntent(any(), any<SettingsScreenNoParams>())).thenReturn(fakeIntent())
228+
whenever(globalActivityStarter.startIntent(any(), any<SubscriptionsWebViewActivityWithParams>())).thenReturn(fakeIntent())
229+
230+
val captor = argumentCaptor<ActivityParams>()
231+
subscriptions.launchPrivacyPro(context, "https://duckduckgo.com/subscriptions?featurePage=duckai".toUri())
232+
233+
verify(globalActivityStarter, times(2)).startIntent(eq(context), captor.capture())
234+
assertEquals("$BUY_URL?featurePage=duckai", (captor.lastValue as SubscriptionsWebViewActivityWithParams).url)
235+
}
236+
237+
@Test
238+
fun whenLaunchSubscriptionWithMultipleQueryParametersThenTheyAreIncludedInSubscriptionURLToActivity() = runTest {
239+
whenever(globalActivityStarter.startIntent(any(), any<SettingsScreenNoParams>())).thenReturn(fakeIntent())
240+
whenever(globalActivityStarter.startIntent(any(), any<SubscriptionsWebViewActivityWithParams>())).thenReturn(fakeIntent())
241+
242+
val captor = argumentCaptor<ActivityParams>()
243+
subscriptions.launchPrivacyPro(context, "https://duckduckgo.com/subscriptions?usePaidDuckAi=true&featurePage=duckai".toUri())
244+
245+
verify(globalActivityStarter, times(2)).startIntent(eq(context), captor.capture())
246+
assertEquals("$BUY_URL?usePaidDuckAi=true&featurePage=duckai", (captor.lastValue as SubscriptionsWebViewActivityWithParams).url)
247+
}
248+
249+
@Test
250+
fun whenSubscriptionWithMultipleQueryParametersThenIsPrivacyProUrlReturnsTrue() = runTest {
251+
assertTrue(subscriptions.isPrivacyProUrl("https://duckduckgo.com/subscriptions?usePaidDuckAi=true&featurePage=duckai".toUri()))
252+
}
253+
254+
@Test
255+
fun whenSubscriptionUrlButNotRootPathThenIsPrivacyProUrlReturnsFalse() = runTest {
256+
assertFalse(subscriptions.isPrivacyProUrl("https://duckduckgo.com/subscriptions/welcome?usePaidDuckAi=true&featurePage=duckai".toUri()))
257+
}
258+
200259
@Test
201260
fun whenLaunchPrivacyProWithNoOriginThenDoNotPassTheOriginToActivity() = runTest {
202261
whenever(globalActivityStarter.startIntent(any(), any<SettingsScreenNoParams>())).thenReturn(fakeIntent())

0 commit comments

Comments
 (0)