Skip to content
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

Onboarding Customisation Experiment #3003

Merged
merged 16 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.duckduckgo.app.global.DefaultRoleBrowserDialog
import com.duckduckgo.app.global.RealDefaultRoleBrowserDialog
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModelFactory
import com.duckduckgo.app.statistics.VariantManager
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import dagger.Module
Expand All @@ -35,7 +36,8 @@ class WelcomePageModule {
context: Context,
pixel: Pixel,
defaultRoleBrowserDialog: DefaultRoleBrowserDialog,
) = WelcomePageViewModelFactory(appInstallStore, context, pixel, defaultRoleBrowserDialog)
variantManager: VariantManager,
) = WelcomePageViewModelFactory(appInstallStore, context, pixel, defaultRoleBrowserDialog, variantManager)

@Provides
fun defaultRoleBrowserDialog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,24 @@

package com.duckduckgo.app.onboarding.ui

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivityOnboardingBinding
import com.duckduckgo.app.global.DuckDuckGoActivity
import com.duckduckgo.app.onboarding.ui.OnboardingViewModel.Command
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.mobile.android.ui.viewbinding.viewBinding
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@InjectWith(ActivityScope::class)
class OnboardingActivity : DuckDuckGoActivity() {
Expand All @@ -42,6 +51,29 @@ class OnboardingActivity : DuckDuckGoActivity() {
super.onCreate(savedInstanceState)
setContentView(binding.root)
configurePager()

observeViewModel()
viewModel.determineScreenOrientation()
}

private fun observeViewModel() {
viewModel.commands()
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { processCommand(it) }
.launchIn(lifecycleScope)
}

private fun processCommand(command: Command) {
when (command) {
is Command.ForceToPortraitForMobileDevices -> overrideScreenOrientation()
}
}

@SuppressLint("SourceLockedOrientationActivity")
private fun overrideScreenOrientation() {
if (resources.getBoolean(R.bool.onboarding_force_portrait)) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}

fun onContinueClicked() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,33 @@ import com.duckduckgo.app.global.DispatcherProvider
import com.duckduckgo.app.onboarding.store.AppStage
import com.duckduckgo.app.onboarding.store.UserStageStore
import com.duckduckgo.app.onboarding.ui.page.OnboardingPageFragment
import com.duckduckgo.app.statistics.VariantManager
import com.duckduckgo.app.statistics.isOnboardingCustomizationExperimentEnabled
import com.duckduckgo.di.scopes.ActivityScope
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

@ContributesViewModel(ActivityScope::class)
class OnboardingViewModel @Inject constructor(
private val userStageStore: UserStageStore,
private val pageLayoutManager: OnboardingPageManager,
private val dispatchers: DispatcherProvider,
private val variantManager: VariantManager,
) : ViewModel() {

private val command = Channel<Command>(1, DROP_OLDEST)
internal fun commands(): Flow<Command> = command.receiveAsFlow()

fun determineScreenOrientation() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include one test for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will 👌

if (variantManager.isOnboardingCustomizationExperimentEnabled()) {
viewModelScope.launch { command.send(Command.ForceToPortraitForMobileDevices) }
}
}

fun initializePages() {
pageLayoutManager.buildPageBlueprints()
}
Expand All @@ -52,4 +68,8 @@ class OnboardingViewModel @Inject constructor(
userStageStore.stageCompleted(AppStage.NEW)
}
}

internal sealed class Command {
object ForceToPortraitForMobileDevices : Command()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.onboarding.ui.customisationexperiment

enum class DDGFeatureOnboardingOption {
PRIVATE_SEARCH,
TRACKER_BLOCKING,
SMALLER_DIGITAL_FOOTPRINT,
FASTER_PAGE_LOADS,
FEWER_ADS,
ONE_CLICK_DATA_CLEARING,
}
nalcalag marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.onboarding.ui.customisationexperiment

import android.content.Context
import android.util.AttributeSet
import androidx.annotation.StringRes
import androidx.constraintlayout.widget.ConstraintLayout
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ViewMultiselectListItemBinding
import com.duckduckgo.mobile.android.ui.viewbinding.viewBinding

class MultiselectListItem @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.multiselectListItemStyle,
) : ConstraintLayout(context, attrs, defStyleAttr) {

private val binding: ViewMultiselectListItemBinding by viewBinding()

var primaryText: String = ""
private set
var trailingEmoji: String = ""
private set
var isItemSelected: Boolean = false
set(value) {
when (value) {
true -> binding.itemContainer.setBackgroundResource(R.drawable.background_multiselect_list_item_selected)
false -> binding.itemContainer.setBackgroundResource(R.drawable.background_multiselect_list_item)
}
field = value
}

init {
context.obtainStyledAttributes(
attrs,
R.styleable.MultiselectListItem,
0,
R.style.Widget_DuckDuckGo_MultiselectListItem,
).apply {

setPrimaryText(getString(R.styleable.MultiselectListItem_primaryText))
setTrailingEmoji(getString(R.styleable.MultiselectListItem_trailingEmoji))
isItemSelected = getBoolean(R.styleable.MultiselectListItem_selected, false)

recycle()
}
binding.itemContainer.setOnClickListener { isItemSelected = !isItemSelected }
}

/** Sets the item title */
fun setPrimaryText(title: String?) {
primaryText = title.orEmpty()
binding.primaryText.text = primaryText
}

/** Sets the item title */
fun setPrimaryText(@StringRes title: Int) {
primaryText = context.getString(title)
binding.primaryText.text = primaryText
}

/** Sets the item trailing emoji */
fun setTrailingEmoji(emojiText: String?) {
trailingEmoji = emojiText.orEmpty()
binding.emojiText.text = emojiText
}

/** Sets the item trailing emoji */
fun setTrailingEmoji(@StringRes emojiTextRes: Int) {
trailingEmoji = context.getString(emojiTextRes)
binding.emojiText.text = trailingEmoji
}

fun setOnClickListener(onClick: () -> Unit) {
binding.itemContainer.setOnClickListener {
isItemSelected = !isItemSelected
onClick.invoke()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ContentOnboardingWelcomeBinding
import com.duckduckgo.app.browser.databinding.IncludeDaxMultiselectDialogCtaBinding
import com.duckduckgo.app.global.extensions.html
import com.duckduckgo.app.onboarding.ui.customisationexperiment.DDGFeatureOnboardingOption
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.mobile.android.ui.view.gone
import com.duckduckgo.mobile.android.ui.view.show
import com.duckduckgo.mobile.android.ui.viewbinding.viewBinding
import javax.inject.Inject
import kotlinx.android.synthetic.main.include_dax_multiselect_dialog_cta.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.*
Expand Down Expand Up @@ -111,6 +116,62 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome)
WelcomePageView.State.Finish -> {
onContinuePressed()
}
WelcomePageView.State.ShowFeatureOptionsCta -> startMultiselectDialogAnimation()
WelcomePageView.State.ShowControlDaxCta -> showDaxDialogCta()
}
}

private fun startMultiselectDialogAnimation() {
val ctaText = context?.getString(R.string.onboardingFeatureOptionsTitle).orEmpty()
binding.daxDialogCta.root.gone()
binding.daxDialogMultiselectCta.apply {
root.show()
root.dialogTextCta.startTypingAnimation(ctaText)
ViewCompat.animate(root)
.alpha(MAX_ALPHA)
.setDuration(ANIMATION_DURATION)
.withEndAction {
ViewCompat.animate(root.featureOptionsContainer)
.alpha(MAX_ALPHA)
.setDuration(ANIMATION_DURATION)
.setStartDelay(ANIMATION_DURATION)
.withEndAction {
setMultiselectListeners(this)
}
}
}
}

private fun setMultiselectListeners(binding: IncludeDaxMultiselectDialogCtaBinding) {
binding.primaryCta.setOnClickListener { getSelectedOptionsAndContinue() }
binding.secondaryCta.setOnClickListener { event(WelcomePageView.Event.OnSkipOptions) }
binding.optionPrivateSearch.setOnClickListener { showContinueButton() }
binding.optionTrackerBlocking.setOnClickListener { showContinueButton() }
binding.optionSmallerFootprint.setOnClickListener { showContinueButton() }
binding.optionFasterPageLoads.setOnClickListener { showContinueButton() }
binding.optionFewerAds.setOnClickListener { showContinueButton() }
binding.optionOneClickDataClearing.setOnClickListener { showContinueButton() }
}

private fun getSelectedOptionsAndContinue() {
var options: Map<DDGFeatureOnboardingOption, Boolean> = mapOf()
binding.daxDialogMultiselectCta.apply {
options = mapOf(
DDGFeatureOnboardingOption.PRIVATE_SEARCH to optionPrivateSearch.isItemSelected,
DDGFeatureOnboardingOption.TRACKER_BLOCKING to optionTrackerBlocking.isItemSelected,
DDGFeatureOnboardingOption.SMALLER_DIGITAL_FOOTPRINT to optionSmallerFootprint.isItemSelected,
DDGFeatureOnboardingOption.FASTER_PAGE_LOADS to optionFasterPageLoads.isItemSelected,
DDGFeatureOnboardingOption.FEWER_ADS to optionFewerAds.isItemSelected,
DDGFeatureOnboardingOption.ONE_CLICK_DATA_CLEARING to optionOneClickDataClearing.isItemSelected,
)
}
event(WelcomePageView.Event.OnContinueOptions(options))
}

private fun showContinueButton() {
binding.daxDialogMultiselectCta.apply {
primaryCta.show()
secondaryCta.gone()
}
}

Expand Down Expand Up @@ -189,14 +250,20 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome)
.setDuration(ANIMATION_DURATION)
.setStartDelay(startDelay)
.withEndAction {
typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer)
.alpha(MAX_ALPHA)
.setDuration(ANIMATION_DURATION)
.withEndAction {
welcomeAnimationFinished = true
binding.daxDialogCta.dialogTextCta.startTypingAnimation(ctaText)
setPrimaryCtaListenerAfterWelcomeAlphaAnimation()
}
event(WelcomePageView.Event.ShowFirstDaxOnboardingDialog)
}
}

private fun showDaxDialogCta() {
binding.daxDialogMultiselectCta.root.gone()
binding.daxDialogCta.root.show()
typingAnimation = ViewCompat.animate(binding.daxDialogCta.daxCtaContainer)
.alpha(MAX_ALPHA)
.setDuration(ANIMATION_DURATION)
.withEndAction {
welcomeAnimationFinished = true
binding.daxDialogCta.dialogTextCta.startTypingAnimation(ctaText)
setPrimaryCtaListenerAfterWelcomeAlphaAnimation()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@
package com.duckduckgo.app.onboarding.ui.page

import android.content.Intent
import com.duckduckgo.app.onboarding.ui.customisationexperiment.DDGFeatureOnboardingOption

object WelcomePageView {
sealed class Event {
object OnPrimaryCtaClicked : Event()
object OnDefaultBrowserSet : Event()
object OnDefaultBrowserNotSet : Event()
object OnSkipOptions : Event()
data class OnContinueOptions(val options: Map<DDGFeatureOnboardingOption, Boolean>) : Event()
object ShowFirstDaxOnboardingDialog : Event()
}

sealed class State {
object Idle : State()
data class ShowDefaultBrowserDialog(val intent: Intent) : State()
object Finish : State()
object ShowFeatureOptionsCta : State()
object ShowControlDaxCta : State()
}
}
Loading