Skip to content

Commit

Permalink
Add welcome screen
Browse files Browse the repository at this point in the history
  • Loading branch information
marcosholgado committed Nov 24, 2023
1 parent 45547f7 commit 8a05388
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import com.duckduckgo.js.messaging.api.JsMessageHandler
import com.duckduckgo.js.messaging.api.JsMessageHelper
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.JsRequestResponse
import com.duckduckgo.js.messaging.api.SubscriptionEvent
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.moshi.Moshi
import java.util.*
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -86,8 +87,14 @@ class ContentScopeScriptsJsMessaging @Inject constructor(
this.webView.addJavascriptInterface(this, coreContentScopeScripts.javascriptInterface)
}

override fun sendSubscriptionEvent() {
// NOOP
override fun sendSubscriptionEvent(subscriptionEventData: SubscriptionEventData) {
val subscriptionEvent = SubscriptionEvent(
context,
subscriptionEventData.featureName,
subscriptionEventData.subscriptionName,
subscriptionEventData.params,
)
jsMessageHelper.sendSubscriptionEvent(subscriptionEvent, callbackName, secret, webView)
}

override fun onResponse(response: JsCallbackData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface JsMessaging {
/**
* Method to send a subscription event
*/
fun sendSubscriptionEvent()
fun sendSubscriptionEvent(subscriptionEventData: SubscriptionEventData)

/**
* Context name
Expand Down Expand Up @@ -139,4 +139,5 @@ sealed class JsRequestResponse {
) : JsRequestResponse()
}

data class SubscriptionEventData(val featureName: String, val subscriptionName: String, val params: JSONObject?)
data class SubscriptionEvent(val context: String, val featureName: String, val subscriptionName: String, val params: JSONObject?)
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.squareup.moshi.Moshi
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -124,6 +125,7 @@ class RealSubscriptionsManager @Inject constructor(
private val _hasSubscription = MutableStateFlow(false)
override val hasSubscription = _hasSubscription.asStateFlow().onSubscription { emitHasSubscriptionsValues() }

private var purchaseStateJob: Job? = null
private fun isUserAuthenticated(): Boolean = !authDataStore.accessToken.isNullOrBlank() && !authDataStore.authToken.isNullOrBlank()

private suspend fun emitHasSubscriptionsValues() {
Expand All @@ -133,7 +135,8 @@ class RealSubscriptionsManager @Inject constructor(
}

private suspend fun emitCurrentPurchaseValues() {
coroutineScope.launch(dispatcherProvider.io()) {
purchaseStateJob?.cancel()
purchaseStateJob = coroutineScope.launch(dispatcherProvider.io()) {
billingClientWrapper.purchaseState.collect {
when (it) {
is PurchaseState.Purchased -> checkPurchase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.duckduckgo.js.messaging.api.JsMessageHandler
import com.duckduckgo.js.messaging.api.JsMessageHelper
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.JsRequestResponse
import com.duckduckgo.js.messaging.api.SubscriptionEvent
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.subscriptions.impl.AuthToken
import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
Expand Down Expand Up @@ -88,8 +90,14 @@ class SubscriptionMessagingInterface @Inject constructor(
this.webView.addJavascriptInterface(this, context)
}

override fun sendSubscriptionEvent() {
// NOOP
override fun sendSubscriptionEvent(subscriptionEventData: SubscriptionEventData) {
val subscriptionEvent = SubscriptionEvent(
context,
subscriptionEventData.featureName,
subscriptionEventData.subscriptionName,
subscriptionEventData.params,
)
jsMessageHelper.sendSubscriptionEvent(subscriptionEvent, callbackName, secret, webView)
}

override fun onResponse(response: JsCallbackData) {
Expand All @@ -116,7 +124,7 @@ class SubscriptionMessagingInterface @Inject constructor(

override val allowedDomains: List<String> = emptyList()
override val featureName: String = "useSubscription"
override val methods: List<String> = listOf("subscriptionSelected", "getSubscriptionOptions", "backToSettings")
override val methods: List<String> = listOf("subscriptionSelected", "getSubscriptionOptions", "backToSettings", "activateSubscription")
}

inner class GetSubscriptionMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.subscriptions.impl.CurrentPurchase
import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR
Expand Down Expand Up @@ -50,6 +51,9 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
Expand All @@ -73,25 +77,31 @@ class SubscriptionWebViewViewModel @Inject constructor(
val currentPurchaseViewState = _currentPurchaseViewState.asStateFlow()

fun start() {
viewModelScope.launch {
subscriptionsManager.currentPurchaseState.collect {
val state = when (it) {
is CurrentPurchase.Failure -> Failure(it.message)
is CurrentPurchase.Success -> Success
is CurrentPurchase.InProgress, CurrentPurchase.PreFlowInProgress -> InProgress
is CurrentPurchase.Recovered -> Recovered
is CurrentPurchase.PreFlowFinished -> Inactive
}
_currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = state))
subscriptionsManager.currentPurchaseState.onEach {
val state = when (it) {
is CurrentPurchase.Failure -> Failure(it.message)
is CurrentPurchase.Success -> Success(
SubscriptionEventData(
PURCHASE_COMPLETED_FEATURE_NAME,
PURCHASE_COMPLETED_SUBSCRIPTION_NAME,
JSONObject(PURCHASE_COMPLETED_JSON),
),
)
is CurrentPurchase.InProgress, CurrentPurchase.PreFlowInProgress -> InProgress
is CurrentPurchase.Recovered -> Recovered
is CurrentPurchase.PreFlowFinished -> Inactive
}
}
_currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = state))
}.flowOn(dispatcherProvider.io())
.launchIn(viewModelScope)
}

fun processJsCallbackMessage(featureName: String, method: String, id: String?, data: JSONObject?) {
when (method) {
"backToSettings" -> backToSettings()
"getSubscriptionOptions" -> id?.let { getSubscriptionOptions(featureName, method, it) }
"subscriptionSelected" -> subscriptionSelected(data)
"activateSubscription" -> activateOnAnotherDevice()
else -> {
// NOOP
}
Expand All @@ -102,7 +112,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
viewModelScope.launch(dispatcherProvider.io()) {
val id = runCatching { data?.getString("id") }.getOrNull()
if (id.isNullOrBlank()) {
_currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = Failure("context")))
_currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = Failure("")))
} else {
command.send(SubscriptionSelected(id))
}
Expand Down Expand Up @@ -149,6 +159,12 @@ class SubscriptionWebViewViewModel @Inject constructor(
}
}

private fun activateOnAnotherDevice() {
viewModelScope.launch {
command.send(ActivateOnAnotherDevice)
}
}

private fun backToSettings() {
viewModelScope.launch {
command.send(BackToSettings)
Expand All @@ -172,7 +188,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
sealed class PurchaseStateView {
data object Inactive : PurchaseStateView()
data object InProgress : PurchaseStateView()
data object Success : PurchaseStateView()
data class Success(val subscriptionEventData: SubscriptionEventData) : PurchaseStateView()
data object Recovered : PurchaseStateView()
data class Failure(val message: String) : PurchaseStateView()
}
Expand All @@ -181,5 +197,12 @@ class SubscriptionWebViewViewModel @Inject constructor(
data object BackToSettings : Command()
data class SendResponseToJs(val data: JsCallbackData) : Command()
data class SubscriptionSelected(val id: String) : Command()
data object ActivateOnAnotherDevice : Command()
}

companion object {
const val PURCHASE_COMPLETED_FEATURE_NAME = "useSubscription"
const val PURCHASE_COMPLETED_SUBSCRIPTION_NAME = "onPurchaseUpdate"
const val PURCHASE_COMPLETED_JSON = """{ type: "completed" }"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.os.Bundle
import android.view.MenuItem
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebViewClient
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
Expand All @@ -35,16 +36,20 @@ import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.getActivityParams
import com.duckduckgo.subscriptions.impl.R.string
import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionsWebviewBinding
import com.duckduckgo.subscriptions.impl.ui.AddDeviceActivity.Companion.AddDeviceScreenWithEmptyParams
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.ActivateOnAnotherDevice
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettings
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.SendResponseToJs
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.SubscriptionSelected
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView
import com.duckduckgo.user.agent.api.UserAgentProvider
import kotlinx.coroutines.flow.distinctUntilChanged
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.flow.launchIn
Expand All @@ -67,6 +72,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
@Inject
lateinit var userAgent: UserAgentProvider

@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

private val viewModel: SubscriptionWebViewViewModel by bindViewModel()

private val binding: ActivitySubscriptionsWebviewBinding by viewBinding()
Expand Down Expand Up @@ -95,6 +103,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
},
)
it.webChromeClient = WebChromeClient()
it.webViewClient = WebViewClient()
it.settings.apply {
userAgentString = userAgent.userAgent(url)
javaScriptEnabled = true
Expand All @@ -121,7 +130,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
.onEach { processCommand(it) }
.launchIn(lifecycleScope)

viewModel.currentPurchaseViewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).onEach {
viewModel.currentPurchaseViewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).distinctUntilChanged().onEach {
renderPurchaseState(it.purchaseState)
}.launchIn(lifecycleScope)
}
Expand All @@ -131,6 +140,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
is BackToSettings -> backToSettings()
is SendResponseToJs -> sendResponseToJs(command.data)
is SubscriptionSelected -> selectSubscription(command.id)
is ActivateOnAnotherDevice -> activateOnAnotherDevice()
}
}

Expand All @@ -147,7 +157,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
is PurchaseStateView.Success -> {
binding.webview.show()
binding.progress.gone()
onPurchaseSuccess()
onPurchaseSuccess(purchaseState.subscriptionEventData)
}
is PurchaseStateView.Recovered -> {
binding.webview.show()
Expand Down Expand Up @@ -177,15 +187,15 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
.show()
}

private fun onPurchaseSuccess() {
private fun onPurchaseSuccess(subscriptionEventData: SubscriptionEventData) {
TextAlertDialogBuilder(this)
.setTitle(getString(string.purchaseCompletedTitle))
.setMessage(getString(string.purchaseCompletedText))
.setPositiveButton(string.ok)
.addEventListener(
object : TextAlertDialogBuilder.EventListener() {
override fun onPositiveButtonClicked() {
finish()
subscriptionJsMessaging.sendSubscriptionEvent(subscriptionEventData)
}
},
)
Expand Down Expand Up @@ -220,6 +230,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity() {
finish()
}

private fun activateOnAnotherDevice() {
globalActivityStarter.start(this, AddDeviceScreenWithEmptyParams)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class SubscriptionMessagingInterfaceTest {
}

@Test
fun whenProcessAndGetSubscriptionsIfNoIdDoNothing() = runTest {
fun whenProcessAndGetSubscriptionIfNoIdDoNothing() = runTest {
givenInterfaceIsRegistered()
givenAuthTokenIsSuccess()

Expand All @@ -208,12 +208,11 @@ class SubscriptionMessagingInterfaceTest {

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

verifyNoInteractions(jsMessageHelper)
assertEquals(0, callback.counter)
}

@Test
fun whenProcessAndBackToSettingsThenCallbackExecutedAndNotResponseSent() = runTest {
fun whenProcessAndBackToSettingsThenCallbackExecuted() = runTest {
givenInterfaceIsRegistered()

val message = """
Expand All @@ -222,24 +221,9 @@ class SubscriptionMessagingInterfaceTest {

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

verifyNoInteractions(jsMessageHelper)
assertEquals(1, callback.counter)
}

@Test
fun whenProcessAndBackToSettingsIfIdDoesNotExistThenDoNothing() = runTest {
givenInterfaceIsRegistered()

val message = """
{"context":"subscriptionPages","featureName":"test","method":"backToSettings","params":{}}
""".trimIndent()

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

verifyNoInteractions(jsMessageHelper)
assertEquals(0, callback.counter)
}

@Test
fun whenProcessAndSetSubscriptionMessageIfFeatureNameDoesNotMatchDoNothing() = runTest {
givenInterfaceIsRegistered()
Expand Down Expand Up @@ -345,6 +329,32 @@ class SubscriptionMessagingInterfaceTest {
assertEquals(1, callback.counter)
}

@Test
fun whenProcessAndActivateSubscriptionIfFeatureNameDoesNotMatchDoNothing() = runTest {
givenInterfaceIsRegistered()

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

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

assertEquals(0, callback.counter)
}

@Test
fun whenProcessAndActivateSubscriptionThenCallbackExecuted() = runTest {
givenInterfaceIsRegistered()

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

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

assertEquals(1, callback.counter)
}

private fun givenInterfaceIsRegistered() {
messagingInterface.register(webView, callback)
whenever(webView.url).thenReturn("https://abrown.duckduckgo.com")
Expand Down
Loading

0 comments on commit 8a05388

Please sign in to comment.