Skip to content

Commit 8953ab8

Browse files
authored
Adds support for getFeatureConfig and getAuthAccessToken in subscriptions flows (#6334)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1210705948833249?focus=true ### Description - Adds support for getFeatureConfig and getAuthAccessToken - Preparation work to fully migrate subscriptions flows to v2 ([Migrate Subscriptions flows to v2](https://app.asana.com/1/137249556945/task/1210705424261806?focus=true)) - Include FF for rollout and kill switch if necessary - New messaging FF will be enabled - V2 flows FF will be disabled until fully migrated ### Steps to test this PR _Feature 1_ - [x] checkout + apply staging patch - [x] install changes - [x] Try to purchase a subscription - [x] ensure no issues - [x] Try to activate a subscription - [x] ensure no issues (add any smoke testing to ensure subscription flow is not impacted) ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent 48f561b commit 8953ab8

File tree

3 files changed

+260
-0
lines changed

3 files changed

+260
-0
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,23 @@ interface PrivacyProFeature {
167167

168168
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
169169
fun privacyProFreeTrial(): Toggle
170+
171+
/**
172+
* Android supports v2 token, but still relies on old v1 subscription messaging.
173+
* We are introducing new JS messaging. Use this flag as kill-switch if necessary.
174+
* It doesn't control which version of messaging FE uses.
175+
*/
176+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
177+
fun enableNewSubscriptionMessages(): Toggle
178+
179+
/**
180+
* When enabled, we signal FE if v2 is available, enabling v2 messaging
181+
* When disabled, FE works with old messaging (v1)
182+
* This flag will be used to select FE subscription messaging mode.
183+
* The value is added into GetFeatureConfig to allow FE to select the mode.
184+
*/
185+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
186+
fun enableSubscriptionFlowsV2(): Toggle
170187
}
171188

172189
@ContributesBinding(AppScope::class)

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.duckduckgo.js.messaging.api.SubscriptionEventData
3535
import com.duckduckgo.subscriptions.impl.AccessTokenResult
3636
import com.duckduckgo.subscriptions.impl.AuthTokenResult
3737
import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
38+
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
3839
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
3940
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
4041
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
@@ -57,6 +58,7 @@ class SubscriptionMessagingInterface @Inject constructor(
5758
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
5859
pixelSender: SubscriptionPixelSender,
5960
subscriptionsChecker: SubscriptionsChecker,
61+
private val privacyProFeature: PrivacyProFeature,
6062
) : JsMessaging {
6163
private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()
6264

@@ -69,6 +71,8 @@ class SubscriptionMessagingInterface @Inject constructor(
6971
SetSubscriptionMessage(subscriptionsManager, appCoroutineScope, dispatcherProvider, pixelSender, subscriptionsChecker),
7072
InformationalEventsMessage(subscriptionsManager, appCoroutineScope, pixelSender),
7173
GetAccessTokenMessage(subscriptionsManager),
74+
GetAuthAccessTokenMessage(subscriptionsManager),
75+
GetFeatureConfigMessage(privacyProFeature),
7276
)
7377

7478
@JavascriptInterface
@@ -311,4 +315,76 @@ class SubscriptionMessagingInterface @Inject constructor(
311315
override val featureName: String = "useSubscription"
312316
override val methods: List<String> = listOf("getAccessToken")
313317
}
318+
319+
private inner class GetAuthAccessTokenMessage(
320+
private val subscriptionsManager: SubscriptionsManager,
321+
) : JsMessageHandler {
322+
323+
override fun process(
324+
jsMessage: JsMessage,
325+
secret: String,
326+
jsMessageCallback: JsMessageCallback?,
327+
) {
328+
val jsMessageId = jsMessage.id ?: return
329+
330+
val pat: AccessTokenResult = runBlocking {
331+
subscriptionsManager.getAccessToken()
332+
}
333+
334+
val resultJson = when (pat) {
335+
is AccessTokenResult.Success -> JSONObject().apply {
336+
put("accessToken", pat.accessToken)
337+
}
338+
339+
is AccessTokenResult.Failure -> JSONObject()
340+
}
341+
342+
val response = JsRequestResponse.Success(
343+
context = jsMessage.context,
344+
featureName = featureName,
345+
method = jsMessage.method,
346+
id = jsMessageId,
347+
result = resultJson,
348+
)
349+
350+
jsMessageHelper.sendJsResponse(response, callbackName, secret, webView)
351+
}
352+
353+
override val allowedDomains: List<String> = emptyList()
354+
override val featureName: String = "useSubscription"
355+
override val methods: List<String> = listOf("getAuthAccessToken")
356+
}
357+
358+
private inner class GetFeatureConfigMessage(
359+
private val privacyProFeature: PrivacyProFeature,
360+
) : JsMessageHandler {
361+
override fun process(
362+
jsMessage: JsMessage,
363+
secret: String,
364+
jsMessageCallback: JsMessageCallback?,
365+
) {
366+
val jsMessageId = jsMessage.id ?: return
367+
368+
if (privacyProFeature.enableNewSubscriptionMessages().isEnabled().not()) return
369+
370+
val authV2Enabled = privacyProFeature.enableSubscriptionFlowsV2().isEnabled()
371+
val resultJson = JSONObject().apply {
372+
put("useSubscriptionsAuthV2", authV2Enabled)
373+
}
374+
375+
val response = JsRequestResponse.Success(
376+
context = jsMessage.context,
377+
featureName = featureName,
378+
method = jsMessage.method,
379+
id = jsMessageId,
380+
result = resultJson,
381+
)
382+
383+
jsMessageHelper.sendJsResponse(response, callbackName, secret, webView)
384+
}
385+
386+
override val allowedDomains: List<String> = emptyList()
387+
override val featureName: String = "useSubscription"
388+
override val methods: List<String> = listOf("getFeatureConfig")
389+
}
314390
}

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

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.duckduckgo.js.messaging.api.JsRequestResponse
99
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
1010
import com.duckduckgo.subscriptions.impl.AccessTokenResult
1111
import com.duckduckgo.subscriptions.impl.AuthTokenResult
12+
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
1213
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
1314
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
1415
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
@@ -40,13 +41,15 @@ class SubscriptionMessagingInterfaceTest {
4041
private val subscriptionsManager: SubscriptionsManager = mock()
4142
private val pixelSender: SubscriptionPixelSender = mock()
4243
private val subscriptionsChecker: SubscriptionsChecker = mock()
44+
private val privacyProFeature: PrivacyProFeature = mock()
4345
private val messagingInterface = SubscriptionMessagingInterface(
4446
subscriptionsManager,
4547
jsMessageHelper,
4648
coroutineRule.testDispatcherProvider,
4749
coroutineRule.testScope,
4850
pixelSender,
4951
subscriptionsChecker,
52+
privacyProFeature,
5053
)
5154

5255
private val callback = object : JsMessageCallback() {
@@ -714,6 +717,158 @@ class SubscriptionMessagingInterfaceTest {
714717
verify(subscriptionsManager, never()).refreshAccessToken()
715718
}
716719

720+
@Test
721+
fun `when process and get auth access token then return response`() = runTest {
722+
givenInterfaceIsRegistered()
723+
givenAccessTokenIsSuccess()
724+
725+
val expected = JsRequestResponse.Success(
726+
context = "subscriptionPages",
727+
featureName = "useSubscription",
728+
method = "getAuthAccessToken",
729+
id = "myId",
730+
result = JSONObject("""{"accessToken":"accessToken"}"""),
731+
)
732+
733+
val message = """
734+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getAuthAccessToken","id":"myId","params":{}}
735+
""".trimIndent()
736+
737+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
738+
739+
val captor = argumentCaptor<JsRequestResponse>()
740+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
741+
val jsMessage = captor.firstValue
742+
743+
assertTrue(jsMessage is JsRequestResponse.Success)
744+
checkEquals(expected, jsMessage)
745+
}
746+
747+
@Test
748+
fun `when process and get auth access token message error then return response`() = runTest {
749+
givenInterfaceIsRegistered()
750+
givenAccessTokenIsFailure()
751+
752+
val expected = JsRequestResponse.Success(
753+
context = "subscriptionPages",
754+
featureName = "useSubscription",
755+
method = "getAuthAccessToken",
756+
id = "myId",
757+
result = JSONObject("""{}"""),
758+
)
759+
760+
val message = """
761+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getAuthAccessToken","id":"myId","params":{}}
762+
""".trimIndent()
763+
764+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
765+
766+
val captor = argumentCaptor<JsRequestResponse>()
767+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
768+
val jsMessage = captor.firstValue
769+
770+
assertTrue(jsMessage is JsRequestResponse.Success)
771+
checkEquals(expected, jsMessage)
772+
}
773+
774+
@Test
775+
fun `when process and get auth access token if no id do nothing`() = runTest {
776+
givenInterfaceIsRegistered()
777+
givenAccessTokenIsSuccess()
778+
779+
val message = """
780+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getAuthAccessToken","params":{}}
781+
""".trimIndent()
782+
783+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
784+
785+
verifyNoInteractions(jsMessageHelper)
786+
}
787+
788+
@Test
789+
fun `when process and get feature config and messaging enabled then return response with feature flags`() = runTest {
790+
givenInterfaceIsRegistered()
791+
givenSubscriptionMessaging(enabled = true)
792+
givenAuthV2(enabled = true)
793+
794+
val expected = JsRequestResponse.Success(
795+
context = "subscriptionPages",
796+
featureName = "useSubscription",
797+
method = "getFeatureConfig",
798+
id = "myId",
799+
result = JSONObject("""{"useSubscriptionsAuthV2":true}"""),
800+
)
801+
802+
val message = """
803+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
804+
""".trimIndent()
805+
806+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
807+
808+
val captor = argumentCaptor<JsRequestResponse>()
809+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
810+
val jsMessage = captor.firstValue
811+
812+
assertTrue(jsMessage is JsRequestResponse.Success)
813+
checkEquals(expected, jsMessage)
814+
}
815+
816+
@Test
817+
fun `when process and get feature config and messaging enabled but auth v2 disabled then return response with auth v2 false`() = runTest {
818+
givenInterfaceIsRegistered()
819+
givenSubscriptionMessaging(enabled = true)
820+
givenAuthV2(enabled = false)
821+
822+
val expected = JsRequestResponse.Success(
823+
context = "subscriptionPages",
824+
featureName = "useSubscription",
825+
method = "getFeatureConfig",
826+
id = "myId",
827+
result = JSONObject("""{"useSubscriptionsAuthV2":false}"""),
828+
)
829+
830+
val message = """
831+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
832+
""".trimIndent()
833+
834+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
835+
836+
val captor = argumentCaptor<JsRequestResponse>()
837+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
838+
val jsMessage = captor.firstValue
839+
840+
assertTrue(jsMessage is JsRequestResponse.Success)
841+
checkEquals(expected, jsMessage)
842+
}
843+
844+
@Test
845+
fun `when process and get feature config and messaging disabled then do nothing`() = runTest {
846+
givenInterfaceIsRegistered()
847+
givenSubscriptionMessaging(enabled = false)
848+
849+
val message = """
850+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
851+
""".trimIndent()
852+
853+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
854+
855+
verifyNoInteractions(jsMessageHelper)
856+
}
857+
858+
@Test
859+
fun `when process and get feature config if no id do nothing`() = runTest {
860+
givenInterfaceIsRegistered()
861+
givenSubscriptionMessaging(enabled = true)
862+
863+
val message = """
864+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","params":{}}
865+
""".trimIndent()
866+
867+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
868+
869+
verifyNoInteractions(jsMessageHelper)
870+
}
871+
717872
private fun givenInterfaceIsRegistered() {
718873
messagingInterface.register(webView, callback)
719874
whenever(webView.url).thenReturn("https://duckduckgo.com/test")
@@ -753,6 +908,18 @@ class SubscriptionMessagingInterfaceTest {
753908
whenever(subscriptionsManager.getAccessToken()).thenReturn(AccessTokenResult.Failure(message = "something happened"))
754909
}
755910

911+
private fun givenSubscriptionMessaging(enabled: Boolean) {
912+
val subscriptionMessagingToggle = mock<com.duckduckgo.feature.toggles.api.Toggle>()
913+
whenever(subscriptionMessagingToggle.isEnabled()).thenReturn(enabled)
914+
whenever(privacyProFeature.enableNewSubscriptionMessages()).thenReturn(subscriptionMessagingToggle)
915+
}
916+
917+
private fun givenAuthV2(enabled: Boolean) {
918+
val v2SubscriptionFlow = mock<com.duckduckgo.feature.toggles.api.Toggle>()
919+
whenever(v2SubscriptionFlow.isEnabled()).thenReturn(enabled)
920+
whenever(privacyProFeature.enableSubscriptionFlowsV2()).thenReturn(v2SubscriptionFlow)
921+
}
922+
756923
private fun checkEquals(expected: JsRequestResponse, actual: JsRequestResponse) {
757924
if (expected is JsRequestResponse.Success && actual is JsRequestResponse.Success) {
758925
assertEquals(expected.id, actual.id)

0 commit comments

Comments
 (0)