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

Show data protection consent screen during onboarding #4825

Merged
merged 21 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
35cfc00
Add `FilledButton` to custom button components in Compose
msasikanth Nov 30, 2023
445cbb1
Add default init parameter for `mobiusViewModels` extension
msasikanth Nov 30, 2023
260005b
Provide preference to mark data protection consent status
msasikanth Nov 30, 2023
9632593
Add effect to mark data protection consent
msasikanth Nov 30, 2023
af1bd91
Add effect to open country selection screen
msasikanth Nov 30, 2023
ac1767f
When data protection consent is marked, then open country selection s…
msasikanth Nov 30, 2023
d909094
When agree button is clicked, then mark data protection consent
msasikanth Nov 30, 2023
6a0f91e
Rename `OpenCountrySelectionScreen` to `MoveToRegistrationActivity`
msasikanth Nov 30, 2023
779a9e2
Wire Mobius loop with UI
msasikanth Nov 30, 2023
50c4ce5
When app onboarding is finished, open onboarding consent screen
msasikanth Nov 30, 2023
9965c64
When get started button is clicked, then open onboarding consent screen
msasikanth Nov 30, 2023
90a6f1f
Add effect to complete onboarding in consent screen
msasikanth Nov 30, 2023
8c95e18
When data protection consent is marked, then complete onboarding
msasikanth Nov 30, 2023
8bd3f20
When onboarding is completed, then move to registration activity
msasikanth Nov 30, 2023
969de14
Remove support for marking onboarding complete in onboarding screen
msasikanth Nov 30, 2023
6ca0634
Update spacing around button frame to 12dp
msasikanth Nov 30, 2023
ab12909
Update CHANGELOG
msasikanth Nov 30, 2023
1c7227f
String formatting fixes
Nov 30, 2023
787237e
Update data protection consent strings data
msasikanth Dec 1, 2023
434592a
Update maestro test login flow to agree to data protection consent
msasikanth Dec 1, 2023
d31bf62
String formatting fixes
Dec 1, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ proguard/
.idea/*.xml
.idea/codeStyles
.idea/gradle.properties
.idea/inspectionProfiles/Project_Default.xml
.navigation/
captures/
*.iml
Expand Down
40 changes: 38 additions & 2 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Next Release

### Features

- Show data protection consent screen during onboarding

### Internal

- Migrate alert facility change sheet to use Mobius loop
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.simple.clinic.consent.onboarding

sealed interface OnboardingConsentEffect {

data object MarkDataProtectionConsent : OnboardingConsentEffect

data object CompleteOnboardingEffect : OnboardingConsentEffect
}

sealed interface OnboardingConsentViewEffect : OnboardingConsentEffect {

data object MoveToRegistrationActivity : OnboardingConsentViewEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.simple.clinic.consent.onboarding

import com.f2prateek.rx.preferences2.Preference
import com.spotify.mobius.functions.Consumer
import com.spotify.mobius.rx2.RxMobius
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.reactivex.ObservableTransformer
import org.simple.clinic.consent.onboarding.OnboardingConsentEffect.CompleteOnboardingEffect
import org.simple.clinic.consent.onboarding.OnboardingConsentEffect.MarkDataProtectionConsent
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.FinishedMarkingDataProtectionConsent
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.OnboardingCompleted
import org.simple.clinic.main.TypedPreference
import org.simple.clinic.main.TypedPreference.Type.DataProtectionConsent
import org.simple.clinic.util.scheduler.SchedulersProvider

class OnboardingConsentEffectHandler @AssistedInject constructor(
@TypedPreference(DataProtectionConsent) private val hasUserConsentedToDataProtection: Preference<Boolean>,
@TypedPreference(TypedPreference.Type.OnboardingComplete) private val hasUserCompletedOnboarding: Preference<Boolean>,
private val schedulersProvider: SchedulersProvider,
@Assisted private val viewEffectsConsumer: Consumer<OnboardingConsentViewEffect>
) {

@AssistedFactory
interface Factory {
fun create(viewEffectsConsumer: Consumer<OnboardingConsentViewEffect>): OnboardingConsentEffectHandler
}

fun build(): ObservableTransformer<OnboardingConsentEffect, OnboardingConsentEvent> {
return RxMobius
.subtypeEffectHandler<OnboardingConsentEffect, OnboardingConsentEvent>()
.addTransformer(MarkDataProtectionConsent::class.java, markDataProtectionConsent())
.addConsumer(OnboardingConsentViewEffect::class.java, viewEffectsConsumer::accept)
.addTransformer(CompleteOnboardingEffect::class.java, completeOnboardingEffect())
.build()
}

private fun completeOnboardingEffect(): ObservableTransformer<CompleteOnboardingEffect, OnboardingConsentEvent> {
return ObservableTransformer { effects ->
effects
.observeOn(schedulersProvider.io())
.doOnNext { hasUserCompletedOnboarding.set(true) }
.map { OnboardingCompleted }
}
}

private fun markDataProtectionConsent(): ObservableTransformer<MarkDataProtectionConsent, OnboardingConsentEvent> {
return ObservableTransformer { effects ->
effects
.observeOn(schedulersProvider.io())
.doOnNext { hasUserConsentedToDataProtection.set(true) }
.map { FinishedMarkingDataProtectionConsent }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.simple.clinic.consent.onboarding

import org.simple.clinic.widgets.UiEvent

sealed interface OnboardingConsentEvent : UiEvent {

data object FinishedMarkingDataProtectionConsent : OnboardingConsentEvent

data object AgreeButtonClicked : OnboardingConsentEvent {
override val analyticsName: String = "Onboarding Consent Screen:Agree Button Clicked"
}

data object OnboardingCompleted : OnboardingConsentEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.simple.clinic.consent.onboarding

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data object OnboardingConsentModel : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package org.simple.clinic.consent.onboarding

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.Fragment
import kotlinx.parcelize.Parcelize
import org.simple.clinic.R
import org.simple.clinic.common.ui.components.FilledButton
import org.simple.clinic.common.ui.theme.SimpleTheme
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.AgreeButtonClicked
import org.simple.clinic.di.injector
import org.simple.clinic.navigation.v2.ScreenKey
import org.simple.clinic.registerorlogin.AuthenticationActivity
import org.simple.clinic.util.disableAnimations
import org.simple.clinic.util.finishWithoutAnimations
import org.simple.clinic.util.mobiusViewModels
import org.simple.clinic.util.unsafeLazy
import javax.inject.Inject

class OnboardingConsentScreenFragment : Fragment(), UiActions {

@Inject
lateinit var effectHandlerFactory: OnboardingConsentEffectHandler.Factory

private val viewEffectHandler by unsafeLazy { OnboardingConsentViewEffectHandler(this) }
private val viewModel by mobiusViewModels(
defaultModel = { OnboardingConsentModel },
update = { OnboardingConsentUpdate() },
effectHandler = { viewEffectsConsumer -> effectHandlerFactory.create(viewEffectsConsumer).build() }
)

override fun onAttach(context: Context) {
super.onAttach(context)
context.injector<Injector>().inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SimpleTheme {
OnboardingConsentScreen(
onAccept = { viewModel.dispatchEvent(AgreeButtonClicked) }
)
}
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.viewEffects.setObserver(
viewLifecycleOwner,
{ viewEffect -> viewEffectHandler.handle(viewEffect) },
{ pausedViewEffects -> pausedViewEffects.forEach { viewEffectHandler.handle(it) } }
)
}

override fun moveToRegistrationActivity() {
// This navigation should not be done here, we need a way to publish
// an event to the parent activity (maybe via the screen router's
// event bus?) and handle the navigation there.
// TODO(vs): 2019-11-07 Move this to an event that is subscribed in the parent activity
msasikanth marked this conversation as resolved.
Show resolved Hide resolved
val intent = AuthenticationActivity
.forNewLogin(requireActivity())
.disableAnimations()

activity?.startActivity(intent)
activity?.finishWithoutAnimations()
}

interface Injector {
fun inject(target: OnboardingConsentScreenFragment)
}

@Parcelize
data class Key(
override val analyticsName: String = "Onboarding Consent Screen"
) : ScreenKey() {

override fun instantiateFragment() = OnboardingConsentScreenFragment()
}
}

@Composable
private fun OnboardingConsentScreen(
modifier: Modifier = Modifier,
onAccept: () -> Unit
) {
Scaffold(
modifier = modifier,
bottomBar = {
Box(
modifier = Modifier
.fillMaxWidth()
.background(SimpleTheme.colors.material.primaryVariant)
.padding(dimensionResource(id = R.dimen.spacing_12))
) {
FilledButton(
onClick = onAccept,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.screen_onboarding_concent_accept_button))
}
}
}
) { paddingValues ->
val toolbarColor = SimpleTheme.colors.toolbarPrimary
val toolbarHeight = dimensionResource(id = R.dimen.spacing_192)

Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.drawWithCache {
onDrawWithContent {
drawRect(
color = toolbarColor,
size = size.copy(height = toolbarHeight.toPx())
)
drawContent()
}
}
) {
Image(
modifier = Modifier
.padding(
top = dimensionResource(id = R.dimen.spacing_40),
bottom = dimensionResource(id = R.dimen.spacing_44)
)
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.logo_large),
contentDescription = null
)

ConsentCard()
}
}
}

@Composable
private fun ConsentCard(modifier: Modifier = Modifier) {
val spacing24 = dimensionResource(id = R.dimen.spacing_24)

Card(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = spacing24),
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(spacing24),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.screen_onboarding_consent_title),
style = SimpleTheme.typography.material.h6
)

Spacer(modifier = Modifier.requiredHeight(spacing24))

Text(
text = stringResource(id = R.string.screen_onboarding_consent_subtitle),
style = SimpleTheme.typography.material.body1
)
}
}
}

@Preview
@Composable
private fun OnboardingConsentScreenPreview() {
SimpleTheme {
OnboardingConsentScreen(
onAccept = {
// no-op
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.simple.clinic.consent.onboarding

import com.spotify.mobius.Next
import com.spotify.mobius.Update
import org.simple.clinic.consent.onboarding.OnboardingConsentEffect.CompleteOnboardingEffect
import org.simple.clinic.consent.onboarding.OnboardingConsentEffect.MarkDataProtectionConsent
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.AgreeButtonClicked
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.FinishedMarkingDataProtectionConsent
import org.simple.clinic.consent.onboarding.OnboardingConsentEvent.OnboardingCompleted
import org.simple.clinic.consent.onboarding.OnboardingConsentViewEffect.MoveToRegistrationActivity
import org.simple.clinic.mobius.dispatch

class OnboardingConsentUpdate : Update<OnboardingConsentModel, OnboardingConsentEvent, OnboardingConsentEffect> {

override fun update(model: OnboardingConsentModel, event: OnboardingConsentEvent): Next<OnboardingConsentModel, OnboardingConsentEffect> {
return when (event) {
FinishedMarkingDataProtectionConsent -> dispatch(CompleteOnboardingEffect)
AgreeButtonClicked -> dispatch(MarkDataProtectionConsent)
OnboardingCompleted -> dispatch(MoveToRegistrationActivity)
}
}
}
Loading
Loading