Let enterprise build be able to override (or disable) the bug report URL.

This commit is contained in:
Benoit Marty
2025-08-08 15:17:00 +02:00
parent 76849c4374
commit d7e4e00b5d
40 changed files with 266 additions and 78 deletions

View File

@@ -9,8 +9,6 @@ package io.element.android.x
import android.app.Application import android.app.Application
import androidx.startup.AppInitializer import androidx.startup.AppInitializer
import io.element.android.appconfig.RageshakeConfig
import io.element.android.appconfig.isEnabled
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.AppComponent import io.element.android.x.di.AppComponent
@@ -25,9 +23,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
AppInitializer.getInstance(this).apply { AppInitializer.getInstance(this).apply {
if (RageshakeConfig.isEnabled) { initializeComponent(CrashInitializer::class.java)
initializeComponent(CrashInitializer::class.java)
}
initializeComponent(PlatformInitializer::class.java) initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java)
} }

View File

@@ -25,9 +25,3 @@ object RageshakeConfig {
*/ */
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
} }
/**
* Whether the rageshake feature is enabled.
*/
val RageshakeConfig.isEnabled: Boolean
get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty()

View File

@@ -47,6 +47,7 @@ import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
@@ -125,6 +126,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val logoutEntryPoint: LogoutEntryPoint, private val logoutEntryPoint: LogoutEntryPoint,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService,
snackbarDispatcher: SnackbarDispatcher, snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>( ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@@ -182,7 +184,9 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
lifecycleScope.launch {
sessionEnterpriseService.init()
}
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)

View File

@@ -0,0 +1,16 @@
/*
* 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.features.enterprise.api
sealed interface BugReportUrl {
data object UseDefault : BugReportUrl
data object Disabled : BugReportUrl
data class Custom(
val url: String,
) : BugReportUrl
}

View File

@@ -9,6 +9,7 @@ package io.element.android.features.enterprise.api
import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
@@ -22,6 +23,8 @@ interface EnterpriseService {
fun firebasePushGateway(): String? fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String? fun unifiedPushDefaultPushGateway(): String?
val bugReportUrlFlow: Flow<BugReportUrl>
companion object { companion object {
const val ANY_ACCOUNT_PROVIDER = "*" const val ANY_ACCOUNT_PROVIDER = "*"
} }

View File

@@ -9,4 +9,6 @@ package io.element.android.features.enterprise.api
interface SessionEnterpriseService { interface SessionEnterpriseService {
suspend fun isElementCallAvailable(): Boolean suspend fun isElementCallAvailable(): Boolean
suspend fun init()
} }

View File

@@ -11,9 +11,11 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.compound.tokens.generated.compoundColorsDark import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@@ -31,4 +33,6 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override fun firebasePushGateway(): String? = null override fun firebasePushGateway(): String? = null
override fun unifiedPushDefaultPushGateway(): String? = null override fun unifiedPushDefaultPushGateway(): String? = null
override val bugReportUrlFlow = flowOf(BugReportUrl.UseDefault)
} }

View File

@@ -14,5 +14,6 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService { class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService {
override suspend fun init() = Unit
override suspend fun isElementCallAvailable(): Boolean = true override suspend fun isElementCallAvailable(): Boolean = true
} }

View File

@@ -8,10 +8,14 @@
package io.element.android.features.enterprise.test package io.element.android.features.enterprise.test
import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeEnterpriseService( class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false, override val isEnterpriseBuild: Boolean = false,
@@ -51,7 +55,6 @@ class FakeEnterpriseService(
return unifiedPushDefaultPushGatewayResult() return unifiedPushDefaultPushGatewayResult()
} }
companion object { val bugReportUrlMutableFlow = MutableStateFlow<BugReportUrl>(BugReportUrl.UseDefault)
const val A_FAKE_HOMESERVER = "a_fake_homeserver" override val bugReportUrlFlow: Flow<BugReportUrl> = bugReportUrlMutableFlow.asStateFlow()
}
} }

View File

@@ -14,6 +14,9 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeSessionEnterpriseService( class FakeSessionEnterpriseService(
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
) : SessionEnterpriseService { ) : SessionEnterpriseService {
override suspend fun init() {
}
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult() isElementCallAvailableResult()
} }

View File

@@ -43,7 +43,7 @@ class HomePresenter @Inject constructor(
override fun present(): HomeState { override fun present(): HomeState {
val matrixUser = client.userProfile.collectAsState() val matrixUser = client.userProfile.collectAsState()
val isOnline by syncService.isOnline.collectAsState() val isOnline by syncService.isOnline.collectAsState()
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val roomListState = roomListPresenter.present() val roomListState = roomListPresenter.present()
val isSpaceFeatureEnabled by remember { val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space) featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)

View File

@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@@ -49,7 +50,7 @@ class HomePresenterTest {
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
val presenter = createHomePresenter( val presenter = createHomePresenter(
client = matrixClient, client = matrixClient,
rageshakeFeatureAvailability = { false }, rageshakeFeatureAvailability = { flowOf(false) },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -66,6 +67,21 @@ class HomePresenterTest {
} }
} }
@Test
fun `present - can report bug`() = runTest {
val presenter = createHomePresenter(
rageshakeFeatureAvailability = { flowOf(true) },
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.canReportBug).isFalse()
val finalState = awaitItem()
assertThat(finalState.canReportBug).isTrue()
}
}
@Test @Test
fun `present - space feature enabled`() = runTest { fun `present - space feature enabled`() = runTest {
val presenter = createHomePresenter( val presenter = createHomePresenter(
@@ -132,7 +148,7 @@ class HomePresenterTest {
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
syncService: SyncService = FakeSyncService(), syncService: SyncService = FakeSyncService(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
indicatorService: IndicatorService = FakeIndicatorService(), indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService() featureFlagService: FeatureFlagService = FakeFeatureFlagService()
) = HomePresenter( ) = HomePresenter(

View File

@@ -8,6 +8,7 @@
package io.element.android.features.login.impl.screens.onboarding package io.element.android.features.login.impl.screens.onboarding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
@@ -82,7 +83,7 @@ class OnBoardingPresenter @AssistedInject constructor(
value = linkAccountProvider == null && value = linkAccountProvider == null &&
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
} }
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
var showReportBug by rememberSaveable { mutableStateOf(false) } var showReportBug by rememberSaveable { mutableStateOf(false) }
val loginMode by loginHelper.collectLoginMode() val loginMode by loginHelper.collectLoginMode()

View File

@@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
@@ -25,7 +26,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl( val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false, isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true, isAllowedToConnectToHomeserver = true,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = true, enforceElementPro = true,
), ),
) )
@@ -38,7 +39,7 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = false, isEnterpriseBuild = false,
// false here. // false here.
isAllowedToConnectToHomeserver = false, isAllowedToConnectToHomeserver = false,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = true, enforceElementPro = true,
), ),
) )
@@ -50,7 +51,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl( val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false, isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true, isAllowedToConnectToHomeserver = true,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = false, enforceElementPro = false,
), ),
) )
@@ -62,7 +63,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl( val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false, isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true, isAllowedToConnectToHomeserver = true,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = null, enforceElementPro = null,
), ),
) )
@@ -85,7 +86,7 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = false, isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = false, isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = false, enforceElementPro = false,
), ),
) )
@@ -97,7 +98,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl( val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = true, isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = true, isAllowedToConnectToHomeserver = true,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = true, enforceElementPro = true,
), ),
) )
@@ -109,7 +110,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl( val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = true, isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = true, isAllowedToConnectToHomeserver = true,
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = false, enforceElementPro = false,
), ),
) )
@@ -122,7 +123,7 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = true, isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = false, isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = true, enforceElementPro = true,
), ),
) )
@@ -135,7 +136,7 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = true, isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = false, isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = ElementWellKnown( elementWellKnown = anElementWellKnown(
enforceElementPro = false, enforceElementPro = false,
), ),
) )

View File

@@ -15,6 +15,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
@@ -114,7 +115,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server element pro required error`() = runTest { fun `present - change server element pro required error`() = runTest {
val getElementWellKnownResult = lambdaRecorder<String, ElementWellKnown> { val getElementWellKnownResult = lambdaRecorder<String, ElementWellKnown> {
ElementWellKnown( anElementWellKnown(
enforceElementPro = true, enforceElementPro = true,
) )
} }

View File

@@ -36,6 +36,8 @@ import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -78,7 +80,6 @@ class OnBoardingPresenterTest {
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
), ),
rageshakeFeatureAvailability = { true },
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@@ -94,7 +95,7 @@ class OnBoardingPresenterTest {
@Test @Test
fun `present - clicking on version 7 times has no effect if 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 = { flowOf(false) },
) )
presenter.test { presenter.test {
skipItems(1) skipItems(1)
@@ -239,7 +240,7 @@ private fun createPresenter(
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
enterpriseService: EnterpriseService = FakeEnterpriseService(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
rageshakeFeatureAvailability: () -> Boolean = { true }, rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
loginHelper: LoginHelper = createLoginHelper(), loginHelper: LoginHelper = createLoginHelper(),
) = OnBoardingPresenter( ) = OnBoardingPresenter(
params = params, params = params,

View File

@@ -83,7 +83,7 @@ class PreferencesRootPresenter @Inject constructor(
var canDeactivateAccount by remember { var canDeactivateAccount by remember {
mutableStateOf(false) mutableStateOf(false)
} }
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
canDeactivateAccount = matrixClient.canDeactivateAccount() canDeactivateAccount = matrixClient.canDeactivateAccount()
} }

View File

@@ -32,6 +32,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -102,7 +103,7 @@ class PreferencesRootPresenterTest {
) )
createPresenter( createPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
rageshakeFeatureAvailability = { false }, rageshakeFeatureAvailability = { flowOf(false) },
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.canReportBug).isFalse() assertThat(initialState.canReportBug).isFalse()
@@ -119,7 +120,7 @@ class PreferencesRootPresenterTest {
val indicatorService = FakeIndicatorService() val indicatorService = FakeIndicatorService()
createPresenter( createPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
rageshakeFeatureAvailability = { false }, rageshakeFeatureAvailability = { flowOf(false) },
indicatorService = indicatorService, indicatorService = indicatorService,
).test { ).test {
skipItems(1) skipItems(1)
@@ -185,7 +186,7 @@ class PreferencesRootPresenterTest {
matrixClient: FakeMatrixClient = FakeMatrixClient(), matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
indicatorService: IndicatorService = FakeIndicatorService(), indicatorService: IndicatorService = FakeIndicatorService(),
) = PreferencesRootPresenter( ) = PreferencesRootPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,

View File

@@ -7,6 +7,8 @@
package io.element.android.features.rageshake.api package io.element.android.features.rageshake.api
import kotlinx.coroutines.flow.Flow
fun interface RageshakeFeatureAvailability { fun interface RageshakeFeatureAvailability {
fun isAvailable(): Boolean fun isAvailable(): Flow<Boolean>
} }

View File

@@ -26,6 +26,7 @@ setupAnvil()
dependencies { dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
@@ -50,6 +51,7 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk) testImplementation(libs.test.mockk)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)

View File

@@ -8,15 +8,19 @@
package io.element.android.features.rageshake.impl package io.element.android.features.rageshake.impl
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.RageshakeConfig
import io.element.android.appconfig.isEnabled
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.features.rageshake.impl.reporter.BugReporterUrlProvider
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability { class DefaultRageshakeFeatureAvailability @Inject constructor(
override fun isAvailable(): Boolean { private val bugReporterUrlProvider: BugReporterUrlProvider,
return RageshakeConfig.isEnabled ) : RageshakeFeatureAvailability {
override fun isAvailable(): Flow<Boolean> {
return bugReporterUrlProvider.provide()
.map { it != null }
} }
} }

View File

@@ -5,10 +5,13 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.rageshake.impl.crash package io.element.android.features.rageshake.impl.crash
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
@@ -19,6 +22,8 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -32,12 +37,15 @@ class DefaultCrashDetectionPresenter @Inject constructor(
@Composable @Composable
override fun present(): CrashDetectionState { override fun present(): CrashDetectionState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val crashDetected = remember { val crashDetected by remember {
if (rageshakeFeatureAvailability.isAvailable()) { rageshakeFeatureAvailability.isAvailable()
crashDataStore.appHasCrashed() .flatMapLatest { isAvailable ->
} else { if (isAvailable) {
flowOf(false) crashDataStore.appHasCrashed()
} } else {
flowOf(false)
}
}
}.collectAsState(false) }.collectAsState(false)
fun handleEvents(event: CrashDetectionEvents) { fun handleEvents(event: CrashDetectionEvents) {
@@ -49,7 +57,7 @@ class DefaultCrashDetectionPresenter @Inject constructor(
return CrashDetectionState( return CrashDetectionState(
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
crashDetected = crashDetected.value, crashDetected = crashDetected,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View File

@@ -39,7 +39,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
val isSupported: MutableState<Boolean> = rememberSaveable { val isSupported: MutableState<Boolean> = rememberSaveable {
mutableStateOf(rageshake.isAvailable()) mutableStateOf(rageshake.isAvailable())
} }
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() } val isFeatureAvailable by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val isEnabled by remember { val isEnabled by remember {
rageshakeDataStore.isEnabled() rageshakeDataStore.isEnabled()
}.collectAsState(initial = false) }.collectAsState(initial = false)

View File

@@ -0,0 +1,22 @@
/*
* 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.features.rageshake.impl.reporter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.RageshakeConfig
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
fun interface BugReportAppNameProvider {
fun provide(): String
}
@ContributesBinding(AppScope::class)
class DefaultBugReportAppNameProvider @Inject constructor() : BugReportAppNameProvider {
override fun provide(): String = RageshakeConfig.BUG_REPORT_APP_NAME
}

View File

@@ -7,8 +7,9 @@
package io.element.android.features.rageshake.impl.reporter package io.element.android.features.rageshake.impl.reporter
import kotlinx.coroutines.flow.Flow
import okhttp3.HttpUrl import okhttp3.HttpUrl
fun interface BugReporterUrlProvider { fun interface BugReporterUrlProvider {
fun provide(): HttpUrl fun provide(): Flow<HttpUrl?>
} }

View File

@@ -114,6 +114,12 @@ class DefaultBugReporter @Inject constructor(
canContact: Boolean, canContact: Boolean,
listener: BugReporterListener, listener: BugReporterListener,
) { ) {
val url = bugReporterUrlProvider.provide().first()
if (url == null) {
// It should not happen, but if the URL is null, we cannot proceed
Timber.e("## sendBugReport() : bug report URL is null")
error("Bug report URL is null, cannot send bug report")
}
// enumerate files to delete // enumerate files to delete
val bugReportFiles: MutableList<File> = ArrayList() val bugReportFiles: MutableList<File> = ArrayList()
var response: Response? = null var response: Response? = null
@@ -243,7 +249,7 @@ class DefaultBugReporter @Inject constructor(
} }
// build the request // build the request
val request = Request.Builder() val request = Request.Builder()
.url(bugReporterUrlProvider.provide()) .url(url)
.post(requestBody) .post(requestBody)
.build() .build()
var errorMessage: String? = null var errorMessage: String? = null

View File

@@ -9,14 +9,31 @@ package io.element.android.features.rageshake.impl.reporter
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.RageshakeConfig import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultBugReporterUrlProvider @Inject constructor() : BugReporterUrlProvider { class DefaultBugReporterUrlProvider @Inject constructor(
override fun provide(): HttpUrl { private val bugReportAppNameProvider: BugReportAppNameProvider,
return RageshakeConfig.BUG_REPORT_URL.toHttpUrl() private val enterpriseService: EnterpriseService,
) : BugReporterUrlProvider {
override fun provide(): Flow<HttpUrl?> {
if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null)
return enterpriseService.bugReportUrlFlow
.map { bugReportUrl ->
when (bugReportUrl) {
is BugReportUrl.Custom -> bugReportUrl.url
BugReportUrl.Disabled -> null
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }
}
}
.map { it?.toHttpUrl() }
} }
} }

View File

@@ -18,6 +18,7 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -104,6 +105,6 @@ class CrashDetectionPresenterTest {
) = DefaultCrashDetectionPresenter( ) = DefaultCrashDetectionPresenter(
buildMeta = buildMeta, buildMeta = buildMeta,
crashDataStore = crashDataStore, crashDataStore = crashDataStore,
rageshakeFeatureAvailability = { isFeatureAvailable }, rageshakeFeatureAvailability = { flowOf(isFeatureAvailable) },
) )
} }

View File

@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
@@ -52,7 +53,7 @@ class RageshakeDetectionPresenterTest {
preferencesPresenter = DefaultRageshakePreferencesPresenter( preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake, rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore, rageshakeDataStore = rageshakeDataStore,
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -77,7 +78,7 @@ class RageshakeDetectionPresenterTest {
preferencesPresenter = DefaultRageshakePreferencesPresenter( preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake, rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore, rageshakeDataStore = rageshakeDataStore,
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -103,7 +104,7 @@ class RageshakeDetectionPresenterTest {
preferencesPresenter = DefaultRageshakePreferencesPresenter( preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake, rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore, rageshakeDataStore = rageshakeDataStore,
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -138,7 +139,7 @@ class RageshakeDetectionPresenterTest {
preferencesPresenter = DefaultRageshakePreferencesPresenter( preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake, rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore, rageshakeDataStore = rageshakeDataStore,
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@@ -173,7 +174,7 @@ class RageshakeDetectionPresenterTest {
preferencesPresenter = DefaultRageshakePreferencesPresenter( preferencesPresenter = DefaultRageshakePreferencesPresenter(
rageshake = rageshake, rageshake = rageshake,
rageshakeDataStore = rageshakeDataStore, rageshakeDataStore = rageshakeDataStore,
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {

View File

@@ -16,6 +16,7 @@ import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -29,7 +30,7 @@ class RageshakePreferencesPresenterTest {
val presenter = DefaultRageshakePreferencesPresenter( val presenter = DefaultRageshakePreferencesPresenter(
FakeRageShake(isAvailableValue = true), FakeRageShake(isAvailableValue = true),
FakeRageshakeDataStore(isEnabled = true), FakeRageshakeDataStore(isEnabled = true),
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -46,7 +47,7 @@ class RageshakePreferencesPresenterTest {
val presenter = DefaultRageshakePreferencesPresenter( val presenter = DefaultRageshakePreferencesPresenter(
FakeRageShake(isAvailableValue = false), FakeRageShake(isAvailableValue = false),
FakeRageshakeDataStore(isEnabled = true), FakeRageshakeDataStore(isEnabled = true),
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -63,7 +64,7 @@ class RageshakePreferencesPresenterTest {
val presenter = DefaultRageshakePreferencesPresenter( val presenter = DefaultRageshakePreferencesPresenter(
FakeRageShake(isAvailableValue = true), FakeRageShake(isAvailableValue = true),
FakeRageshakeDataStore(isEnabled = true), FakeRageshakeDataStore(isEnabled = true),
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@@ -83,7 +84,7 @@ class RageshakePreferencesPresenterTest {
val presenter = DefaultRageshakePreferencesPresenter( val presenter = DefaultRageshakePreferencesPresenter(
FakeRageShake(isAvailableValue = true), FakeRageShake(isAvailableValue = true),
FakeRageshakeDataStore(isEnabled = true), FakeRageshakeDataStore(isEnabled = true),
rageshakeFeatureAvailability = { true }, rageshakeFeatureAvailability = { flowOf(true) },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View File

@@ -31,6 +31,7 @@ import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionSt
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.MultipartReader import okhttp3.MultipartReader
@@ -464,7 +465,7 @@ class DefaultBugReporterTest {
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
sessionStore = sessionStore, sessionStore = sessionStore,
buildMeta = buildMeta, buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") }, bugReporterUrlProvider = { flowOf(server.url("/")) },
sdkMetadata = FakeSdkMetadata("123456789"), sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
tracingService = tracingService, tracingService = tracingService,

View File

@@ -7,18 +7,44 @@
package io.element.android.features.rageshake.impl.reporter package io.element.android.features.rageshake.impl.reporter
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.RageshakeConfig import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.test.FakeEnterpriseService
import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Test import org.junit.Test
class DefaultBugReporterUrlProviderTest { class DefaultBugReporterUrlProviderTest {
@Test @Test
fun `test DefaultBugReporterUrlProvider`() { fun `provide return values when there is an rageshake app name`() = runTest {
val sut = DefaultBugReporterUrlProvider() val enterpriseService = FakeEnterpriseService()
if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) { val sut = DefaultBugReporterUrlProvider(
val result = sut.provide() bugReportAppNameProvider = { "rageshakeAppName" },
assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) enterpriseService = enterpriseService,
)
sut.provide().test {
assertThat(awaitItem()).isEqualTo(
RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }?.toHttpUrl()
)
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Disabled)
assertThat(awaitItem()).isNull()
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Custom("https://aURL.org"))
assertThat(awaitItem()).isEqualTo("https://aURL.org".toHttpUrl())
}
}
@Test
fun `provide return null when there is no rageshake app name`() = runTest {
val enterpriseService = FakeEnterpriseService()
val sut = DefaultBugReporterUrlProvider(
bugReportAppNameProvider = { "" },
enterpriseService = enterpriseService,
)
sut.provide().test {
assertThat(awaitItem()).isNull()
awaitComplete()
} }
} }
} }

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.wellknown.api package io.element.android.libraries.wellknown.api
data class ElementWellKnown( data class ElementWellKnown(
val registrationHelperUrl: String? = null, val registrationHelperUrl: String?,
val enforceElementPro: Boolean? = null, val enforceElementPro: Boolean?,
val rageshakeUrl: String?,
) )

View File

@@ -8,10 +8,10 @@
package io.element.android.libraries.wellknown.api package io.element.android.libraries.wellknown.api
data class WellKnown( data class WellKnown(
val homeServer: WellKnownBaseConfig? = null, val homeServer: WellKnownBaseConfig?,
val identityServer: WellKnownBaseConfig? = null, val identityServer: WellKnownBaseConfig?,
) )
data class WellKnownBaseConfig( data class WellKnownBaseConfig(
val baseURL: String? = null val baseURL: String?
) )

View File

@@ -13,7 +13,6 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellKnown
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetriever
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject

View File

@@ -25,4 +25,6 @@ data class InternalElementWellKnown(
val registrationHelperUrl: String? = null, val registrationHelperUrl: String? = null,
@SerialName("enforce_element_pro") @SerialName("enforce_element_pro")
val enforceElementPro: Boolean? = null, val enforceElementPro: Boolean? = null,
@SerialName("rageshake_url")
val rageshakeUrl: String? = null,
) )

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
internal fun InternalElementWellKnown.map() = ElementWellKnown( internal fun InternalElementWellKnown.map() = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl,
) )
internal fun InternalWellKnown.map() = WellKnown( internal fun InternalWellKnown.map() = WellKnown(

View File

@@ -0,0 +1,26 @@
/*
* 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.features.wellknown.test
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
import io.element.android.libraries.wellknown.api.WellKnown
import io.element.android.tests.testutils.simulateLongTask
class FakeSessionWellknownRetriever(
private val getWellKnownResult: () -> WellKnown? = { null },
private val getElementWellKnownResult: () -> ElementWellKnown? = { null },
) : SessionWellknownRetriever {
override suspend fun getWellKnown(): WellKnown? = simulateLongTask {
getWellKnownResult()
}
override suspend fun getElementWellKnown(): ElementWellKnown? = simulateLongTask {
getElementWellKnownResult()
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.features.wellknown.test
import io.element.android.libraries.wellknown.api.ElementWellKnown
fun anElementWellKnown(
registrationHelperUrl: String? = null,
enforceElementPro: Boolean? = null,
rageshakeUrl: String? = null,
) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl,
)