Announce multi-account support

Show a one-time bottom sheet on the home screen to announce that several
accounts can now be added (element-hq/element-meta#3349).

- Add `Announcement.MultiAccount`, which is never shown by default.
- AppMigration11 queues the announcement on application upgrade only,
  and marks it as shown if the user already has several accounts.
- HomePresenter shows the bottom sheet only when the announcement is
  pending, the MultiAccount feature flag is enabled and the user has a
  single account.
- The "Add account" button hides the sheet, marks the announcement as
  dismissed and starts the add account flow through the new
  `HomeEntryPoint.Callback.navigateToAddAccount()`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-10-05 17:01:02 +02:00
parent 2de0d78ced
commit c654bf0947
19 changed files with 444 additions and 6 deletions
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 Element Creations 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.migration.impl.migrations
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.libraries.sessionstorage.api.SessionStore
/**
* Ensure the multi-account announcement is displayed, but only on application upgrade,
* and only if the user does not already have several accounts.
*/
@ContributesIntoSet(AppScope::class)
class AppMigration11(
private val sessionStore: SessionStore,
private val announcementService: AnnouncementService,
) : AppMigration {
override val order: Int = 11
override suspend fun migrate(isFreshInstall: Boolean) {
if (isFreshInstall) return
if (sessionStore.getAllSessions().size > 1) {
// The user already uses several accounts, no need to announce the feature
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
} else {
announcementService.showAnnouncement(Announcement.MultiAccount)
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2026 Element Creations 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.migration.impl.migrations
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AppMigration11Test {
@Test
fun `migration on fresh install should not invoke the AnnouncementService`() = runTest {
val service = FakeAnnouncementService()
val migration = AppMigration11(
sessionStore = InMemorySessionStore(initialList = listOf(aSessionData())),
announcementService = service,
)
// FakeAnnouncementService lambdas throw by default
migration.migrate(isFreshInstall = true)
}
@Test
fun `migration on upgrade with a single account should show the announcement`() = runTest {
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
val service = FakeAnnouncementService(
showAnnouncementResult = showAnnouncementResult,
)
val migration = AppMigration11(
sessionStore = InMemorySessionStore(initialList = listOf(aSessionData())),
announcementService = service,
)
migration.migrate(isFreshInstall = false)
showAnnouncementResult.assertions().isCalledOnce()
.with(value(Announcement.MultiAccount))
}
@Test
fun `migration on upgrade with several accounts should mark the announcement as dismissed`() = runTest {
val onAnnouncementDismissedResult = lambdaRecorder<Announcement, Unit> { }
val service = FakeAnnouncementService(
onAnnouncementDismissedResult = onAnnouncementDismissedResult,
)
val migration = AppMigration11(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = "@alice:server.org"),
aSessionData(sessionId = "@bob:server.org"),
)
),
announcementService = service,
)
migration.migrate(isFreshInstall = false)
onAnnouncementDismissedResult.assertions().isCalledOnce()
.with(value(Announcement.MultiAccount))
}
}