Show version on OnBoarding screen.

This commit is contained in:
Benoit Marty 2025-06-26 18:13:13 +02:00
parent 11cbc2c293
commit 19a8412f2f
8 changed files with 59 additions and 6 deletions

View file

@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.qrcode) implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oidc.api)
implementation(projects.libraries.uiUtils)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
implementation(platform(libs.network.retrofit.bom)) implementation(platform(libs.network.retrofit.bom))
implementation(libs.androidx.webkit) implementation(libs.androidx.webkit)

View file

@ -12,5 +12,6 @@ sealed interface OnBoardingEvents {
val defaultAccountProvider: String val defaultAccountProvider: String
) : OnBoardingEvents ) : OnBoardingEvents
data object OnVersionClick : OnBoardingEvents
data object ClearError : OnBoardingEvents data object ClearError : OnBoardingEvents
} }

View file

@ -9,9 +9,12 @@ package io.element.android.features.login.impl.screens.onboarding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -24,6 +27,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
class OnBoardingPresenter @AssistedInject constructor( class OnBoardingPresenter @AssistedInject constructor(
@Assisted private val params: OnBoardingNode.Params, @Assisted private val params: OnBoardingNode.Params,
@ -40,6 +44,8 @@ class OnBoardingPresenter @AssistedInject constructor(
): OnBoardingPresenter ): OnBoardingPresenter
} }
private val multipleTapToUnlock = MultipleTapToUnlock()
@Composable @Composable
override fun present(): OnBoardingState { override fun present(): OnBoardingState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
@ -70,6 +76,7 @@ class OnBoardingPresenter @AssistedInject constructor(
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
} }
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
var showReportBug by rememberSaveable { mutableStateOf(false) }
val loginMode by loginHelper.collectLoginMode() val loginMode by loginHelper.collectLoginMode()
@ -82,6 +89,13 @@ class OnBoardingPresenter @AssistedInject constructor(
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
) )
OnBoardingEvents.ClearError -> loginHelper.clearError() OnBoardingEvents.ClearError -> loginHelper.clearError()
OnBoardingEvents.OnVersionClick -> {
if (canReportBug) {
if (multipleTapToUnlock.unlock(localCoroutineScope)) {
showReportBug = true
}
}
}
} }
} }
@ -91,8 +105,9 @@ class OnBoardingPresenter @AssistedInject constructor(
mustChooseAccountProvider = mustChooseAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider,
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
canReportBug = canReportBug, canReportBug = canReportBug && showReportBug,
loginMode = loginMode, loginMode = loginMode,
version = buildMeta.versionName,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

@ -17,6 +17,7 @@ data class OnBoardingState(
val canLoginWithQrCode: Boolean, val canLoginWithQrCode: Boolean,
val canCreateAccount: Boolean, val canCreateAccount: Boolean,
val canReportBug: Boolean, val canReportBug: Boolean,
val version: String,
val loginMode: AsyncData<LoginMode>, val loginMode: AsyncData<LoginMode>,
val eventSink: (OnBoardingEvents) -> Unit, val eventSink: (OnBoardingEvents) -> Unit,
) { ) {

View file

@ -30,6 +30,7 @@ fun anOnBoardingState(
canLoginWithQrCode: Boolean = false, canLoginWithQrCode: Boolean = false,
canCreateAccount: Boolean = false, canCreateAccount: Boolean = false,
canReportBug: Boolean = false, canReportBug: Boolean = false,
version: String = "1.0.0",
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized, loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (OnBoardingEvents) -> Unit = {}, eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState( ) = OnBoardingState(
@ -39,6 +40,7 @@ fun anOnBoardingState(
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = canCreateAccount, canCreateAccount = canCreateAccount,
canReportBug = canReportBug, canReportBug = canReportBug,
version = version,
loginMode = loginMode, loginMode = loginMode,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -202,12 +202,23 @@ private fun OnBoardingButtons(
// Add a report problem text button. Use a Text since we need a special theme here. // Add a report problem text button. Use a Text since we need a special theme here.
Text( Text(
modifier = Modifier modifier = Modifier
.padding(16.dp) .clickable(onClick = onReportProblem)
.clickable(onClick = onReportProblem), .padding(16.dp),
text = stringResource(id = CommonStrings.common_report_a_problem), text = stringResource(id = CommonStrings.common_report_a_problem),
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
) )
} else {
Text(
modifier = Modifier
.clickable {
state.eventSink(OnBoardingEvents.OnVersionClick)
}
.padding(16.dp),
text = stringResource(id = R.string.screen_onboarding_app_version, state.version),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
} }
} }
} }

View file

@ -34,6 +34,7 @@
<string name="screen_login_subtitle">"Matrix is an open network for secure, decentralised communication."</string> <string name="screen_login_subtitle">"Matrix is an open network for secure, decentralised communication."</string>
<string name="screen_login_title">"Welcome back!"</string> <string name="screen_login_title">"Welcome back!"</string>
<string name="screen_login_title_with_homeserver">"Sign in to %1$s"</string> <string name="screen_login_title_with_homeserver">"Sign in to %1$s"</string>
<string name="screen_onboarding_app_version">"Version %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Sign in manually"</string> <string name="screen_onboarding_sign_in_manually">"Sign in manually"</string>
<string name="screen_onboarding_sign_in_to">"Sign in to %1$s"</string> <string name="screen_onboarding_sign_in_to">"Sign in to %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string> <string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>

View file

@ -83,19 +83,40 @@ class OnBoardingPresenterTest {
assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.canLoginWithQrCode).isFalse()
assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.productionApplicationName).isEqualTo("B")
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
assertThat(initialState.canReportBug).isTrue() assertThat(initialState.canReportBug).isFalse()
assertThat(awaitItem().canLoginWithQrCode).isTrue() assertThat(awaitItem().canLoginWithQrCode).isTrue()
} }
} }
@Test @Test
fun `present - rageshake not available`() = runTest { fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest {
val presenter = createPresenter( val presenter = createPresenter(
rageshakeFeatureAvailability = { false }, rageshakeFeatureAvailability = { false },
) )
presenter.test { presenter.test {
skipItems(1) skipItems(1)
assertThat(awaitItem().canReportBug).isFalse() awaitItem().also { state ->
assertThat(state.canReportBug).isFalse()
repeat(7) {
state.eventSink(OnBoardingEvents.OnVersionClick)
}
}
expectNoEvents()
}
}
@Test
fun `present - clicking on version 7 times will reveal the report a problem button`() = runTest {
val presenter = createPresenter()
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.canReportBug).isFalse()
repeat(7) {
state.eventSink(OnBoardingEvents.OnVersionClick)
}
}
assertThat(awaitItem().canReportBug).isTrue()
} }
} }