Skip to content

Sign in with element classic final#6296

Merged
bmarty merged 9 commits into
developfrom
feature/bma/signInWithElementClassicFinal
Apr 14, 2026
Merged

Sign in with element classic final#6296
bmarty merged 9 commits into
developfrom
feature/bma/signInWithElementClassicFinal

Conversation

@bmarty
Copy link
Copy Markdown
Member

@bmarty bmarty commented Mar 6, 2026

Content

Let Element X import details from any existing session from Element Classic to sign in and verify the session automatically.

Note: require element-hq/element-android#9136 to merged and released.

Motivation and context

Help users of Element Classic migrate smoothly to Element X.

Screenshots / GIFs

See recorded ones.

Demo of the happy path (case 5 below):

Screen_recording_20260413_145110.mp4

Demo of case 11:

Screen_recording_20260413_152344.mp4

Tests

There are several cases to consider, here is a summary:

Case Element Classic state Effect on Element X
1 Element Classic is not installed No effect
2 Element Classic is installed, but no session exist No effect
3 Element Classic is installed, a session exists, but the session is not verified, or some secrets (private keys) are missing Pre-fill the homeserver and the userId, but will not be able to verify the session
4 Element Classic is installed, a session exists, is verified, all the secrets are known, but a session with the same userId exists on Element X.This can happen since Element X supports multi accounts. No effect.
5 Happy path:Element Classic is installed, a session exists, is verified, all the secrets are known, and no session with the same userId exists on Element X The flow to import a session from Element Classic is possible
6 Error in the communication between the 2 applications (Android) / accessing the shared storage (iOS) to retrieve the data. No effect, no error displayed. Only logs.
7 Edge case:Element Classic is installed, a session exists, is verified, all the secrets are known, and no session with the same userId exists on Element X. But once on the MAS page displayed by Element X, user logs in to a different account No effect, the regular login flow continues, i.e. the user will have to manually verify the session. The secrets exported by EC will not be used and just deleted.
8 Edge case:Element X provides the “mxid:” hint to MAS, but MAS just ignores it if it is connected to another account. It could warn the user. MAS issue: The login_hint is ignored when already logged in. If the user continues, the secrets will not be used (mismatched userId) and the user will have to verify manually.
9 Edge case:The user does not use the button to sign in with Element Classic, but does a manual login. At the end the user signs in to the same account as the Element Classic one. It is acceptable that EX requests the secret from EC to perform the verification step automatically.
10 Element Classic is installed, a session exists, is verified, all the secrets are known, and no session with the same userId exists on Element X. But EC is logged in using SSO that Element X does not support No effect
11 Element Classic is installed, a session exists, is verified, the cross-signing keys are known but backups nor recovery have been set up. Running the migration will transfer the cross-signing keys but no backup key. Since EX is set up to automatically create a backup, it will do so. This in turn enables Element Classic to upload backups but not download. If the user decides to log out of EX it will not get a warning about setting up recovery since it is not the last device the user has. This would leave Classic in a broken state where it uploads to the backup but has no ability to download backups. Display a dialog telling the user that they should first enable keybackup on Element Classic, before login using Element X. When the user is done with Element Classic, when Element X is resumed, the data are loaded again
12 Element Classic has a verified session, had a key backup, but key backup is now deleted. Element Classic still exports the old key backup key to Element X Handle the same as 11.

Tested devices

  • Physical
  • Emulator
  • OS version(s):

Checklist

  • Changes have been tested on an Android device or Android emulator with API 24
  • UI change has been tested on both light and dark themes
  • Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
  • Pull request is based on the develop branch
  • Pull request title will be used in the release note, it clearly define what will change for the user
  • Pull request includes screenshots or videos if containing UI changes
  • You've made a self review of your PR

@bmarty bmarty added the PR-Feature For a new feature label Mar 6, 2026
@bmarty bmarty force-pushed the feature/bma/signInWithElementClassicFinal branch from 01673ce to 8abb802 Compare March 6, 2026 14:23
@bmarty bmarty added the Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. label Mar 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 6, 2026

📱 Scan the QR code below to install the build (arm64 only) for this PR.
QR code
If you can't scan the QR code you can install the build via this link: https://i.diawi.com/X8bbcj

@bmarty bmarty added Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. and removed Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. labels Mar 6, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 6, 2026

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 72.30216% with 154 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.04%. Comparing base (c73d004) to head (fd3c4c2).
⚠️ Report is 29 commits behind head on develop.

Files with missing lines Patch % Lines
...res/login/impl/classic/ElementClassicConnection.kt 72.39% 34 Missing and 11 partials ⚠️
...atrix/impl/auth/RustMatrixAuthenticationService.kt 0.00% 36 Missing ⚠️
...ies/designsystem/components/avatar/BitmapAvatar.kt 25.64% 27 Missing and 2 partials ⚠️
.../features/login/impl/LoginFlowTransitionHandler.kt 0.00% 14 Missing ⚠️
...chitecture/appyx/FaderOrSliderTransitionHandler.kt 0.00% 14 Missing ⚠️
...s/classic/loginwithclassic/LoginWithClassicView.kt 95.32% 4 Missing and 1 partial ⚠️
...id/libraries/androidutils/service/ServiceBinder.kt 0.00% 4 Missing ⚠️
...s/classic/missingkeybackup/MissingKeyBackupView.kt 89.65% 0 Missing and 3 partials ⚠️
...t/android/features/login/impl/login/LoginHelper.kt 50.00% 1 Missing and 1 partial ⚠️
...ogin/impl/screens/classic/ClassicFlowNodeHelper.kt 96.00% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #6296      +/-   ##
===========================================
+ Coverage    81.02%   81.04%   +0.02%     
===========================================
  Files         2596     2608      +12     
  Lines        71726    72098     +372     
  Branches      9288     9337      +49     
===========================================
+ Hits         58114    58432     +318     
- Misses       10199    10236      +37     
- Partials      3413     3430      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bmarty bmarty force-pushed the feature/bma/signInWithElementClassicFinal branch from 10f8556 to 7927b17 Compare March 16, 2026 17:27
@bmarty bmarty force-pushed the feature/bma/signInWithElementClassicFinal branch 2 times, most recently from 52a59e8 to 436113a Compare April 9, 2026 12:55
@bmarty bmarty added Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. and removed Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. labels Apr 9, 2026
@github-actions github-actions Bot removed the Record-Screenshots Runs the 'Record Screenshots' CI job and adds a commit with any new screenshots found. label Apr 9, 2026
@bmarty bmarty force-pushed the feature/bma/signInWithElementClassicFinal branch from f39687d to 73e1a09 Compare April 13, 2026 09:16
@bmarty bmarty marked this pull request as ready for review April 13, 2026 12:42
@bmarty bmarty requested a review from a team as a code owner April 13, 2026 12:42
@bmarty bmarty requested review from jmartinesp and removed request for a team April 13, 2026 12:42
Ensure that the timeout has effect only in Idle state.
@bmarty bmarty force-pushed the feature/bma/signInWithElementClassicFinal branch from 676c11c to f5e1cbe Compare April 13, 2026 13:37
Copy link
Copy Markdown
Member

@jmartinesp jmartinesp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments about the code, just nitpicks so feel free to ignore them.

I'll give it another check with the IDE later and then test it. How should I test this, btw? Do I need versions of EXA and EA signed with some specific keys?

Comment on lines +150 to +151
// Do not emit error, else the regular on boarding flow will be displayed
// emitState(ElementClassicConnectionState.Error("The messenger is null, can't request data"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this still be around?

Comment on lines +65 to +67
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultElementClassicConnection(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some diagram or explanation of how the flow should happen would be helpful as docs.

}

override fun start() {
Timber.tag(loggerTag.value).w("start()")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a debug log now?

Comment on lines +196 to +197
private val mutableStateFlow = MutableStateFlow<ElementClassicConnectionState>(ElementClassicConnectionState.Idle)
override val stateFlow = mutableStateFlow.asStateFlow()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should this be at the start of this component?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using a getter better to you?
get() = mutableStateFlow.asStateFlow()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just meant other properties are at the top of the component (~L75), so this should probably be there too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I was confused because this class has a fun start() 😅

Comment on lines +308 to +311
private fun getElementClassicComponent() = ComponentName(
BuildConfig.elementClassicPackage,
ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this could probably be a val instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, the function was different before.

@bmarty
Copy link
Copy Markdown
Member Author

bmarty commented Apr 14, 2026

How should I test this, btw? Do I need versions of EXA and EA signed with some specific keys?

The code should run with debug versions and release version of the applications (EC debug <-> EX debug) and (EC release <-> EX release). But I need to check if nightly build could communicate together I do not think this is the case.

You can install Element Classic from develop (now that element-hq/element-android#9136 has been merged) and EX from this branch (using diawi QR code should be OK)

Copy link
Copy Markdown
Member

@jmartinesp jmartinesp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works really well! Just a couple of things I noticed:

  1. I had a couple of sessions on EXA that I've been using for a while (so multi-account enabled). When I logged out of both accounts to test this, EXA wouldn't display the new screen to use EA's session data. After I cleared the app's data this was working, so maybe the logout didn't get rid of some data in the local DBs and case 4 happened, where EXA thought it didn't need to display anything because the same session already existed? Or maybe it was the force-kill that helped with this? It's not a blocking issue IMO, and I can't reproduce it anymore.
  2. Another weird thing I experienced is I had EA with an account without key storage and EXA already opened in the initial screen. I then verified EA and went back to EXA, and when trying to log in it still asked me to verify my account, maybe because the session data had been shared before I verified EA (in the new initial screen)? Restarting the app used the right flow and I was logged in and verified directly.

@bmarty
Copy link
Copy Markdown
Member Author

bmarty commented Apr 14, 2026

For point 1, I am not sure what happened.
For point 2, we EX is resumed on the Sign in with Classic screen, the data is not refreshed on this screen, only in the MissingKeyBackup screen (here). This could be changed and the data could be refreshed each time ClassicFlowNode is resumed maybe. Not sure if it could have some side effect, but I can give it a try

This ensure that Element X is always up to date regarding Element Classic state.
@bmarty
Copy link
Copy Markdown
Member Author

bmarty commented Apr 14, 2026

Thanks @jmartinesp. I have added fd3c4c2 that should ensure that Element X is always up to date with Element Classic. So now:

  • start EX. The screen to login to Element Classic is displayed
  • go to background and to foreground. Nothing particular happen (in particular no glitch in the avatar that got fetched again)
  • switch to Element Classic
  • change the avatar of the account
  • switch to Element X: the avatar should be updated
  • switch to Element Classic
  • Sign out
  • switch to Element X: the screen to login with Classic is replaced by the on boarding screen

I have also checked that there is no regression when the screen to go and enable key backup on Element Classic is displayed.

Also this change is simplifying the code, so all good!

@sonarqubecloud
Copy link
Copy Markdown

private val slider: ModifierTransitionHandler<LoginFlowNode.NavTarget, BackStack.State>,
private val fader: ModifierTransitionHandler<LoginFlowNode.NavTarget, BackStack.State>,
) : ModifierTransitionHandler<LoginFlowNode.NavTarget, BackStack.State>() {
override fun createModifier(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Modifier factory functions should be extensions on Modifier

* Handler of incoming messages from service.
*/
@Suppress("DEPRECATION")
inner class IncomingHandler : Handler() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ This Handler class should be static or leaks might occur (io.element.android.features.login.impl.classic.DefaultElementClassicConnection.IncomingHandler)

private val slider: ModifierTransitionHandler<NavTarget, BackStack.State>,
private val fader: ModifierTransitionHandler<NavTarget, BackStack.State>,
) : ModifierTransitionHandler<NavTarget, BackStack.State>() {
override fun createModifier(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Modifier factory functions should be extensions on Modifier

@bmarty bmarty merged commit 4b8368f into develop Apr 14, 2026
32 of 34 checks passed
@bmarty bmarty deleted the feature/bma/signInWithElementClassicFinal branch April 14, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR-Feature For a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants