-
Notifications
You must be signed in to change notification settings - Fork 731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
QR Code Login UI #7338
QR Code Login UI #7338
Conversation
# Conflicts: # library/ui-strings/src/main/res/values/strings.xml # library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml # vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt # vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt # vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt # vector/src/main/java/im/vector/app/features/VectorFeatures.kt # vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt # vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt # vector/src/main/res/layout/fragment_other_sessions.xml # vector/src/main/res/layout/fragment_settings_devices.xml
e1a8226
to
d8ea9c8
Compare
…tor-im/element-android into feature/ons/qr_code_login_ui
|
@@ -144,6 +143,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( | |||
if (getVersionResult != null) { | |||
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() | |||
homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices() | |||
homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ | |||
getVersionResult.doesServerSupportThreads().orFalse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor but orFalse()
seem not needed anymore here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, done.
|
||
sealed class QrCodeLoginAction : VectorViewModelAction { | ||
data class OnQrCodeScanned(val qrCode: String) : QrCodeLoginAction() | ||
object QrCodeViewStarted : QrCodeLoginAction() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename QrCodeViewStarted
to GenerateQrCode
so that it reflects more the action we really want to perform?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion, done.
binding.qrCodeLoginHeaderImageView.backgroundTintList = ColorStateList.valueOf(backgroundTint) | ||
} | ||
|
||
fun setTitle(title: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we reuse the public methods inside the private ones to replace part of their implementation? For instance calling setTitle(title)
instead of binding.qrCodeLoginHeaderTitleTextView.text = title
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
private fun observeViewState() { | ||
viewModel.onEach { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not using the invalidate()
method here to react to change of viewState?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to ask the same thing :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I totally forgot this function, done.
} | ||
|
||
private fun onQrCodeScannerFailed() { | ||
Timber.d("QrCodeLoginInstructionsFragment.onQrCodeScannerFailed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we prompt the user about the error in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hugh is implementing it in another PR.
binding.instructions1Layout.isVisible = instruction1 != null | ||
binding.instructions2Layout.isVisible = instruction2 != null | ||
binding.instructions3Layout.isVisible = instruction3 != null | ||
binding.instruction1TextView.text = instruction1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could use our extension method: setTextOrHide
? And setInstructions(instructions: List<String>)
could be called here instead, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
private fun setInstruction(instructionLayout: LinearLayout, instructionTextView: TextView, instruction: String?) { | ||
instruction?.let { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we could use our extension method: setTextOrHide
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, because the layout visibility has to change, not the TextView visibility
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh you are right, I have read it too fast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a lot to say after @mnaturel review, just some minor remarks. Nice work!
<string name="qr_code_login_header_failed_denied_description">The request was denied on the other device.</string> | ||
<string name="qr_code_login_new_device_instruction_1">Open ${app_name} on your other device</string> | ||
<string name="qr_code_login_new_device_instruction_2">Go to Settings -> Security & Privacy -> Show All Sessions</string> | ||
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to avoid translation mistake, change to
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string> | |
<string name="qr_code_login_new_device_instruction_3">Select \'%s\'</string> |
and inject at runtime the wording of the button.
Which is the key qr_code_login_show_qr_code_button
IIUC which contains the text... Show QR code in this device
.
} | ||
} else { | ||
// check if selected server supports MSC3882 first | ||
homeServerConnectionConfigFactory.create(homeServerUrl)?.let { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use the block viewModelScope.launch {
only here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, done.
authAPI.versions() | ||
} | ||
} | ||
return if (versions.isSuccess) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a network error the client will consider that the feature is not supported (which is maybe acceptable here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is ok to silently hide it on network problems.
} | ||
null -> { | ||
Timber.i("QrCodeLoginArgs is null. This is not expected.") | ||
finish() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also add a return
statement to avoid calling observeViewEvents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, done.
} | ||
|
||
private fun setInstruction(instructionLayout: LinearLayout, instructionTextView: TextView, instruction: String?) { | ||
instruction?.let { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, because the layout visibility has to change, not the TextView visibility
@Parcelize | ||
data class QrCodeLoginArgs( | ||
val loginType: QrCodeLoginType, | ||
val showQrCodeByDefault: Boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to showQrCodeImmediately
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense, done.
showInstructionsFragment(qrCodeLoginArgs) | ||
} | ||
QrCodeLoginType.LINK_A_DEVICE -> { | ||
if (qrCodeLoginArgs.showQrCodeByDefault.orFalse()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
orFalse()
is not needed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, done.
showQrCodeByDefault = true, | ||
) | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe extract to a sub method to avoid code duplication:
private fun navigateToQrCodeScreen(showQrCodeImmediately: Boolean) {
navigator
.openLoginWithQrCode(
requireActivity(),
QrCodeLoginArgs(
loginType = QrCodeLoginType.LINK_A_DEVICE,
showQrCodeByDefault = showQrCodeImmediately,
)
)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or better you can expose it inside the VectorSettingsDevicesViewNavigator
so that we can test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done (didn't have time to create a standalone navigator, will do when we start writing tests for this feature).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The navigator was already in place for info, the idea was just to move the method inside it.
app:drawableLeftCompat="@drawable/ic_qr_code" | ||
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd" | ||
app:layout_constraintStart_toStartOf="@id/loginGutterStart" | ||
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe hide by default to avoid blinking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
android:background="@drawable/circle_qr_code_login_instruction_with_border" | ||
android:padding="6dp" | ||
android:text="@string/four" | ||
android:textColor="?colorPrimary" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no instruction4TextView
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, design changed. Removed.
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
super.onViewCreated(view, savedInstanceState) | ||
initCancelButton() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "try again" button also needs to be initialised.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you implement it in your PR?
_viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen) | ||
} | ||
|
||
// TODO. UI test purpose. Fixme remove! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it supposed to be remove in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the onFailed
, onConnectionEstablished
and onSigninIn
methods are not used, is it expected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is expected as the use of them is in #7369
} | ||
|
||
/** | ||
* TODO. UI test purpose. Fixme accordingly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question, will it be implemented later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but I want to keep placeholder functions.
@@ -118,6 +118,33 @@ class OnboardingViewModel @AssistedInject constructor( | |||
} | |||
} | |||
|
|||
private fun observeQrCodeLoginCapability(homeServerUrl: String) = viewModelScope.launch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is method named with observe
prefix? It seems there is no observation pattern. Here, I would rather name it checkQrCodeLoginCapability
or refreshQrCodeLoginCapability
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -70,6 +74,28 @@ class FtueAuthCombinedLoginFragment : | |||
viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content())) | |||
} | |||
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) } | |||
|
|||
viewModel.onEach(OnboardingViewState::canLoginWithQrCode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to be moved inside invalidate
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not possible probably since this is an abstract class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is because the invalidate
method has been declared final
in the AbstractFtueAuthFragment
. I wonder why it has been declared final
.
android:viewportWidth="13" | ||
android:viewportHeight="12"> | ||
<path | ||
android:pathData="M7.167,12V10.667H8.5V12H7.167ZM5.833,10.667V7.333H7.167V10.667H5.833ZM11.167,8.667V6H12.5V8.667H11.167ZM9.833,6V4.667H11.167V6H9.833ZM1.833,7.333V6H3.167V7.333H1.833ZM0.5,6V4.667H1.833V6H0.5ZM6.5,1.333V0H7.833V1.333H6.5ZM1.333,3.167H3.667V0.833H1.333V3.167ZM1,4C0.856,4 0.736,3.953 0.642,3.858C0.547,3.764 0.5,3.644 0.5,3.5V0.5C0.5,0.356 0.547,0.236 0.642,0.142C0.736,0.047 0.856,0 1,0H4C4.144,0 4.264,0.047 4.358,0.142C4.453,0.236 4.5,0.356 4.5,0.5V3.5C4.5,3.644 4.453,3.764 4.358,3.858C4.264,3.953 4.144,4 4,4H1ZM1.333,11.167H3.667V8.833H1.333V11.167ZM1,12C0.856,12 0.736,11.953 0.642,11.858C0.547,11.764 0.5,11.644 0.5,11.5V8.5C0.5,8.356 0.547,8.236 0.642,8.142C0.736,8.047 0.856,8 1,8H4C4.144,8 4.264,8.047 4.358,8.142C4.453,8.236 4.5,8.356 4.5,8.5V11.5C4.5,11.644 4.453,11.764 4.358,11.858C4.264,11.953 4.144,12 4,12H1ZM9.333,3.167H11.667V0.833H9.333V3.167ZM9,4C8.856,4 8.736,3.953 8.642,3.858C8.547,3.764 8.5,3.644 8.5,3.5V0.5C8.5,0.356 8.547,0.236 8.642,0.142C8.736,0.047 8.856,0 9,0H12C12.144,0 12.264,0.047 12.358,0.142C12.453,0.236 12.5,0.356 12.5,0.5V3.5C12.5,3.644 12.453,3.764 12.358,3.858C12.264,3.953 12.144,4 12,4H9ZM9.833,12V10H8.5V8.667H11.167V10.667H12.5V12H9.833ZM7.167,7.333V6H9.833V7.333H7.167ZM4.5,7.333V6H3.167V4.667H7.167V6H5.833V7.333H4.5ZM5.167,4V1.333H6.5V2.667H7.833V4H5.167ZM2,2.5V1.5H3V2.5H2ZM2,10.5V9.5H3V10.5H2ZM10,2.5V1.5H11V2.5H10Z" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The path seems long for this icon. I think we may either use the PNG version or reduce the complexity using a tool like https://github.com/alexjlockwood/avocado
android:viewportWidth="54" | ||
android:viewportHeight="38"> | ||
<path | ||
android:pathData="M53.667,19C53.667,17.533 52.467,16.333 51,16.333H45.48C44.173,7.293 36.413,0.333 27,0.333C17.587,0.333 9.827,7.293 8.52,16.333H3C1.533,16.333 0.333,17.533 0.333,19C0.333,20.467 1.533,21.667 3,21.667H8.52C9.827,30.707 17.587,37.667 27,37.667C36.413,37.667 44.173,30.707 45.48,21.667H51C52.467,21.667 53.667,20.467 53.667,19ZM35,25.667C35,27.133 33.8,28.333 32.333,28.333H21.667C20.2,28.333 19,27.133 19,25.667V17.667C19,16.2 20.2,15 21.667,15V12.333C21.667,9.107 24.547,6.52 27.907,7.08C30.52,7.507 32.333,9.96 32.333,12.627V15C33.8,15 35,16.2 35,17.667V25.667ZM29,21.667C29,22.76 28.093,23.667 27,23.667C25.907,23.667 25,22.76 25,21.667C25,20.573 25.907,19.667 27,19.667C28.093,19.667 29,20.573 29,21.667ZM29.667,12.333V15H24.333V12.333C24.333,10.867 25.533,9.667 27,9.667C28.467,9.667 29.667,10.867 29.667,12.333Z" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same remark for the complexity of the path here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@onurays Could you fill the PR description? I have tried to test it but I could not see any QR code login UI even when I use a server which is supposed to support it. I guess this is due to the fact we are currently using build flags?
After some testings between 2 Android devices (did not find the QR code options in Web session), here is what I have seen:
|
It will be valid for web soon where feature has been enabled. iOS support will come later. Given the constraints of rolling out this feature across platforms I think it is acceptable for the wording to be slightly unclear whilst it is behind a feature flag.
The implementation of the logic is in a separate PR: #7369 The plan is to get the UI merged in the current PR and then apply the logic PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay for me. There are some remaining non blocking comments that should be fixed later.
SonarCloud Quality Gate failed. |
Thanks for the update. For future reference, this comment has been ignored: #7338 (comment) |
This comment has now been followed up in #7369 (comment). |
Type of change
Content
New screens for signing in by scanning a QR code. This PR does not include the logic to do the login. This logic is in a separate PR: #7369
Motivation and context
The goal is to make signing in and set up of E2EE Element Android simpler in the case that the user already has an active session on another (capable) device.
The capability is behind a feature flag and also requires a compatible homeserver.
The "Show QR Code Login in Device Manager" is in preparation for the next phase of development of this feature.
Screenshots / GIFs
On enabling the feature and entering a homeserver (or choosing to show for all homeservers) a new button is visible:
On press it you get some instructions:
After this the new screens show in a carousel instead of being attached to real logic.
Tests
Tested devices
Checklist