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:
commit
fe9fbe894c
22 changed files with 170 additions and 30 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0ab623f806fc90bef41beccc5d5f444a882e6634cdadecbbc341e10369bbcfdb
|
||||
size 315380
|
||||
oid sha256:f8dc4806d2d363c326ac777011b95a98111e69d8f7acaccf926dc959bf8e9636
|
||||
size 311242
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b73088b5af32e47d18d6961fe5622411f5342388d8a0d78c8f3fb5e27213146a
|
||||
size 315116
|
||||
oid sha256:0b55b1c3f2f76ac40cba5f7d49d897a4ffe22d6718444ca8a1197173c660f182
|
||||
size 306017
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24735f133066c55c6d88b7f9930ad983cca431611c7e6833ca5a4d657eeb14a5
|
||||
size 313116
|
||||
oid sha256:644e67fd8ccb1754ba4bfec6de5bc8c75a6603b75faa96847076cc27ad18804b
|
||||
size 309657
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d02a7ff9ff40f73d8c72b9378e10ba1f6580524cbdefcf290066b8047fdc33c
|
||||
size 307633
|
||||
oid sha256:bcc26d88db59b7499a9e1894608831b418a8088b248c5fb390a43ba8cd5aa319
|
||||
size 304249
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5ac9f2f168b895e8262181f789b0fe2ad2c97b1296a50c1453879438bfe436a
|
||||
size 395942
|
||||
oid sha256:344fa6743ab2f43819abf6045d4d98ef0ff91f6b31e6de4a87235d6b96160e53
|
||||
size 392425
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7c1c7d34935986d01113069c25849fc860133de582fe94ee63f2e28b181e87f
|
||||
size 397378
|
||||
oid sha256:ed48740f02725790f7de7a867d237449c07148367dc32ef7fbbd7b40d0ed9a4c
|
||||
size 379715
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a11f8383fc481255fb3fa6f1a5475d23c13c0460c33b04bc190cf3b5931893a3
|
||||
size 395108
|
||||
oid sha256:d6a6c752f1de16c4c64987d8f603d76ec0ed344b59d335b71004ce85500d852e
|
||||
size 383032
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36424ccefbbbd35201800dbcbb743a2d22aa6fe7a606054fc3acf8b98aead272
|
||||
size 381588
|
||||
oid sha256:4b9101e8d1502445e3997230800ac1cd4adc50a5392cebdcdc4dfc3e5fe56abc
|
||||
size 364584
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue