Merge pull request #4944 from element-hq/feature/bma/version
Replace the Report a problem button with the app's version on the on boading screen.
This commit is contained in:
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.androidx.webkit)
|
||||
|
||||
@@ -12,5 +12,6 @@ sealed interface OnBoardingEvents {
|
||||
val defaultAccountProvider: String
|
||||
) : OnBoardingEvents
|
||||
|
||||
data object OnVersionClick : OnBoardingEvents
|
||||
data object ClearError : OnBoardingEvents
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ package io.element.android.features.login.impl.screens.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
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.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
|
||||
|
||||
class OnBoardingPresenter @AssistedInject constructor(
|
||||
@Assisted private val params: OnBoardingNode.Params,
|
||||
@@ -40,6 +44,8 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||
): OnBoardingPresenter
|
||||
}
|
||||
|
||||
private val multipleTapToUnlock = MultipleTapToUnlock()
|
||||
|
||||
@Composable
|
||||
override fun present(): OnBoardingState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
@@ -70,6 +76,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||
}
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
var showReportBug by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
@@ -82,6 +89,13 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||
)
|
||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||
OnBoardingEvents.OnVersionClick -> {
|
||||
if (canReportBug) {
|
||||
if (multipleTapToUnlock.unlock(localCoroutineScope)) {
|
||||
showReportBug = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +105,9 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
canReportBug = canReportBug,
|
||||
canReportBug = canReportBug && showReportBug,
|
||||
loginMode = loginMode,
|
||||
version = buildMeta.versionName,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ data class OnBoardingState(
|
||||
val canLoginWithQrCode: Boolean,
|
||||
val canCreateAccount: Boolean,
|
||||
val canReportBug: Boolean,
|
||||
val version: String,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val eventSink: (OnBoardingEvents) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -30,6 +30,7 @@ fun anOnBoardingState(
|
||||
canLoginWithQrCode: Boolean = false,
|
||||
canCreateAccount: Boolean = false,
|
||||
canReportBug: Boolean = false,
|
||||
version: String = "1.0.0",
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (OnBoardingEvents) -> Unit = {},
|
||||
) = OnBoardingState(
|
||||
@@ -39,6 +40,7 @@ fun anOnBoardingState(
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = canCreateAccount,
|
||||
canReportBug = canReportBug,
|
||||
version = version,
|
||||
loginMode = loginMode,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -202,12 +202,23 @@ private fun OnBoardingButtons(
|
||||
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable(onClick = onReportProblem),
|
||||
.clickable(onClick = onReportProblem)
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<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_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_to">"Sign in to %1$s"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>
|
||||
|
||||
@@ -83,19 +83,40 @@ class OnBoardingPresenterTest {
|
||||
assertThat(initialState.canLoginWithQrCode).isFalse()
|
||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
||||
assertThat(initialState.canReportBug).isTrue()
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
assertThat(awaitItem().canLoginWithQrCode).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
rageshakeFeatureAvailability = { false },
|
||||
)
|
||||
presenter.test {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ dependencies {
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.fullscreenintent.api)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.features.lockscreen.api)
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||
@@ -49,6 +50,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
) : Presenter<PreferencesRootState> {
|
||||
@Composable
|
||||
override fun present(): PreferencesRootState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val matrixUser = matrixClient.userProfile.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
// Force a refresh of the profile
|
||||
@@ -103,7 +105,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
fun handleEvent(event: PreferencesRootEvents) {
|
||||
when (event) {
|
||||
is PreferencesRootEvents.OnVersionInfoClick -> {
|
||||
showDeveloperSettingsProvider.unlockDeveloperSettings()
|
||||
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ package io.element.android.features.preferences.impl.utils
|
||||
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
@@ -19,18 +21,15 @@ class ShowDeveloperSettingsProvider @Inject constructor(
|
||||
companion object {
|
||||
const val DEVELOPER_SETTINGS_COUNTER = 7
|
||||
}
|
||||
private var counter = DEVELOPER_SETTINGS_COUNTER
|
||||
|
||||
private val multipleTapToUnlock = MultipleTapToUnlock(DEVELOPER_SETTINGS_COUNTER)
|
||||
private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE
|
||||
|
||||
private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild)
|
||||
val showDeveloperSettings: StateFlow<Boolean> = _showDeveloperSettings
|
||||
|
||||
fun unlockDeveloperSettings() {
|
||||
if (counter == 0) {
|
||||
return
|
||||
}
|
||||
counter--
|
||||
if (counter == 0) {
|
||||
fun unlockDeveloperSettings(scope: CoroutineScope) {
|
||||
if (multipleTapToUnlock.unlock(scope)) {
|
||||
_showDeveloperSettings.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,7 @@ android {
|
||||
dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.ui.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Returns true if the user has tapped [numberOfTapToUnlock] times in a short amount of time.
|
||||
* The counter is reset after 2 seconds of inactivity.
|
||||
*
|
||||
* @param numberOfTapToUnlock The number of taps required to unlock.
|
||||
*/
|
||||
class MultipleTapToUnlock(
|
||||
private val numberOfTapToUnlock: Int = 7,
|
||||
) {
|
||||
private var counter = numberOfTapToUnlock
|
||||
private var currentJob: Job? = null
|
||||
|
||||
fun unlock(scope: CoroutineScope): Boolean {
|
||||
counter--
|
||||
currentJob?.cancel()
|
||||
return if (counter > 0) {
|
||||
currentJob = scope.launch {
|
||||
delay(2.seconds)
|
||||
// Reset counter if user is not fast enough
|
||||
counter = numberOfTapToUnlock
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.ui.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MultipleTapToUnlockTest {
|
||||
@Test
|
||||
fun `test multiple tap should unlock`() = runTest {
|
||||
val sut = MultipleTapToUnlock(3)
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||
// All next call returns true
|
||||
advanceTimeBy(3.seconds)
|
||||
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||
}
|
||||
@Test
|
||||
fun `test waiting should reset counter`() = runTest {
|
||||
val sut = MultipleTapToUnlock(3)
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
advanceTimeBy(3.seconds)
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user