-
Notifications
You must be signed in to change notification settings - Fork 79
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
Implement the keycard flows for the new onbaording #17127
base: master
Are you sure you want to change the base?
Conversation
Jenkins BuildsClick to see older builds (47)
|
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.
Need to digest, and test :)
@@ -42,7 +41,8 @@ class OnboardingEnums | |||
Locked, | |||
// exit states | |||
NotEmpty, | |||
Empty | |||
Empty, | |||
Authorized |
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.
When is this used?
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.
We need to call Authorize on the card before doing anything that wants to access the actual data of the card. That includes getting a mnemonic or the keys.
try { | ||
const seedwords = root.getSeedWords() | ||
d.seedWords = JSON.parse(seedwords) | ||
root.seedphraseSubmitted(d.seedWords) |
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.
This should be emitted only after the user has confirmed and moved to the next page
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.
Earlier we were assuming, that seed words are generated locally, with no interaction with the keycard. So we assumed it's fast, sync operation which never fails. So it was convenient to pass it just as a property.
But if we obtain the seed words from the keycard, the flow is different, with different assumptions.
In the proposed implementation the call remains synchronous. But it may fail (the easiest option is to disconnect keycard in the meantime). Then the UI is left in invalid state, the situation is not handled at all. It makes the flow incomplete.
As it's synchronous, the question is really should be like that. There is communication with the device, probably we should be prepared for some delay without blocking the UI. Now in case of delay we have just freeze.
Next thing is emitting seedphraseSubmitted
signal:
const seedwords = root.getSeedWords()
d.seedWords = JSON.parse(seedwords)
root.seedphraseSubmitted(d.seedWords)
This probably doesn't make much sense. The backend knows that seed words were fetched because the function getSeedWords
was called.
The problem starts probably in the Figma design, where the case of fetching seed words in not covered at all because wrong assumptions we initially had.
The order of pin setting and seed words fetching is also not updated, making the flow harder to analyze.
@jrainville @caybro wdyt?
@@ -147,7 +166,9 @@ Page { | |||
syncState: root.onboardingStore.syncState | |||
addKeyPairState: root.onboardingStore.addKeyPairState | |||
|
|||
seedWords: root.onboardingStore.getMnemonic().split(" ") | |||
getSeedWords: function () { |
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 this a function now?
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.
We can only get the mnemonic when the keycard is authorized or initialized, so I pass the function to be called later when we have the right to call it.
onboardingModuleInst.obtainingPasswordSuccess.connect(root.obtainingPasswordSuccess) | ||
onboardingModuleInst.obtainingPasswordError.connect(root.obtainingPasswordError) | ||
d.onboardingModuleInst.appLoaded.connect(root.appLoaded) | ||
// TODO implement the following signals |
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.
Even if not implemented yet, this can stay here uncommented, right?
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.
Since the view functions don't exist, any connect
put below those don't activate because the function throws an error.
It felt safer to comment those for now as it confused me for a while why my signals didn't work.
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.
Just declare those signals?
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.
Nice to see the first version working!
However I think we should rethink several points raised in the review, especially how we handle the async operations and keep good separation between pure components UI and the underlying logic.
signal keycardPinCreated(string pin) | ||
signal setPinFailed(string error) |
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 general rule regarding signals/functions is that signals are used to notify change/event generated internally within the component. So in vast majority of changes they are called (emitted) from the component, not from outside.
I we want to interact with the component from outside, function is proper choice.
More details can be found there: https://github.com/Furkanzmc/QML-Coding-Guide?tab=readme-ov-file#sh-2-when-to-use-functions-and-signals
try { | ||
const seedwords = root.getSeedWords() | ||
d.seedWords = JSON.parse(seedwords) | ||
root.seedphraseSubmitted(d.seedWords) |
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.
Earlier we were assuming, that seed words are generated locally, with no interaction with the keycard. So we assumed it's fast, sync operation which never fails. So it was convenient to pass it just as a property.
But if we obtain the seed words from the keycard, the flow is different, with different assumptions.
In the proposed implementation the call remains synchronous. But it may fail (the easiest option is to disconnect keycard in the meantime). Then the UI is left in invalid state, the situation is not handled at all. It makes the flow incomplete.
As it's synchronous, the question is really should be like that. There is communication with the device, probably we should be prepared for some delay without blocking the UI. Now in case of delay we have just freeze.
Next thing is emitting seedphraseSubmitted
signal:
const seedwords = root.getSeedWords()
d.seedWords = JSON.parse(seedwords)
root.seedphraseSubmitted(d.seedWords)
This probably doesn't make much sense. The backend knows that seed words were fetched because the function getSeedWords
was called.
The problem starts probably in the Figma design, where the case of fetching seed words in not covered at all because wrong assumptions we initially had.
The order of pin setting and seed words fetching is also not updated, making the flow harder to analyze.
@jrainville @caybro wdyt?
required property var isSeedPhraseValid | ||
|
||
property bool displayKeycardPromoBanner | ||
|
||
signal loginWithKeycardRequested | ||
signal keycardFactoryResetRequested | ||
signal keyPairTransferRequested | ||
signal loadMnemonicRequested |
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.
Mnemonic == seed words?
We should adjust naming probably.
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 they are synonymous. I personally prefer mnemonic, it's also wha',s used in the backend, but seedPhrase
is used a lot in the new onboarding.
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's now actually "recovery phrase" anywhere in the UI
if (root.keycardState === Onboarding.KeycardState.NotEmpty) { | ||
if (d.withNewSeedphrase) { | ||
// Need to authorize before getting a seedphrase | ||
root.authorizationRequested() |
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.
This doesn't look correct. If the authorization doesn't require any input from from the user from this page and is not caused by any action triggered there, that request shouldn't be generated be the UI. In other words, it should not be the responsibility of the UI, which should be unaware of this process.
signal keycardPinCreated(string pin) | ||
signal setPinFailed(string error) |
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.
For async operations like adding key pair we have dedicated enum defined (AddKeyPairState
in enums.h) which is used for handling that ansyc operation. Similarly for syncing. Here the approach is different, UI emits
keycardPinCreatedand expects that at some point in the future
setPinFailed` will be called or some other thing will happen which means success (btw, how we may know that pin setting succeeded)?
@jrainville needs a rebase, and more importantly, updating the QML tests. For that, I'd suggest first updating the individual QML pages we have for the various flows as well |
d711fb4
to
6b598ce
Compare
6b598ce
to
51baa19
Compare
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.
Thanks for the reviews. I answered the questions and also refactored the code to use states instead of signal
@@ -42,7 +41,8 @@ class OnboardingEnums | |||
Locked, | |||
// exit states | |||
NotEmpty, | |||
Empty | |||
Empty, | |||
Authorized |
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.
We need to call Authorize on the card before doing anything that wants to access the actual data of the card. That includes getting a mnemonic or the keys.
required property var isSeedPhraseValid | ||
|
||
property bool displayKeycardPromoBanner | ||
|
||
signal loginWithKeycardRequested | ||
signal keycardFactoryResetRequested | ||
signal keyPairTransferRequested | ||
signal loadMnemonicRequested |
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 they are synonymous. I personally prefer mnemonic, it's also wha',s used in the backend, but seedPhrase
is used a lot in the new onboarding.
@@ -147,7 +166,9 @@ Page { | |||
syncState: root.onboardingStore.syncState | |||
addKeyPairState: root.onboardingStore.addKeyPairState | |||
|
|||
seedWords: root.onboardingStore.getMnemonic().split(" ") | |||
getSeedWords: function () { |
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.
We can only get the mnemonic when the keycard is authorized or initialized, so I pass the function to be called later when we have the right to call it.
onboardingModuleInst.obtainingPasswordSuccess.connect(root.obtainingPasswordSuccess) | ||
onboardingModuleInst.obtainingPasswordError.connect(root.obtainingPasswordError) | ||
d.onboardingModuleInst.appLoaded.connect(root.appLoaded) | ||
// TODO implement the following signals |
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.
Since the view functions don't exist, any connect
put below those don't activate because the function throws an error.
It felt safer to comment those for now as it confused me for a while why my signals didn't work.
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 job! 👍
I didn't check out the QML, I see guys reviewed it already.
@@ -110,6 +144,18 @@ proc restoreAccountAndLogin*(self: Controller, password, mnemonic: string, recov | |||
keycardInstanceUID, | |||
) | |||
|
|||
# TODO make async? |
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 procedure itself is quick, it starts a goroutine which takes time.
@@ -82,24 +88,31 @@ method load*[T](self: Module[T]) = | |||
self.controller.init() | |||
self.delegate.onboardingDidLoad() | |||
|
|||
method setPin*[T](self: Module[T], pin: string): bool = | |||
method setPin*[T](self: Module[T], pin: 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.
Would be nice to rename it to Initialize
for consistency
password = "", # For keycard it will be substituted with`encryption.publicKey` in status-go | ||
seedPhrase, | ||
recoverAccount = false, | ||
keycardInstanceUID = keycardEvent.keycardInfo.instanceUID, |
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.
We're using this last received keycardEvent
, which I think can be a bit wrong.
I think we should kinda hold/remember the active InstanceUID
, and in case a keycard was replaced with a different one, we should start any operations from the beginning.
This would basically ensure that any further operations are executed at the expected keycard, not just the one that's currently inserted. The time window for such mistakes is short and it's difficult to repro, but anyway should be important to do.
instanceUid: string, | ||
keycardKeys: KeycardExportedKeysDto, | ||
recoverAccount: bool, | ||
displayName: 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.
I think the displayName
and images are always empty now?
@@ -38,6 +44,7 @@ type | |||
controller: Controller | |||
localPairingStatus: LocalPairingStatus | |||
currentFlow: SecondaryFlow | |||
exportedKeys: KeycardExportedKeysDto |
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.
Just to keep the record of what we discussed today.
Would be very cool to spawn a routine (to be executed in a separate thread) for the Login
, that would call all keycard commands synchronously. Then:
- we won't need to write these complex chain of async operations
- we won't need to add such temporary props like
exportedKeys
, as it would be just a local variable inside such routine.
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 for this PR, just thoughts.
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 created a separate issue for that #17139
"params": %*[ params ], | ||
} | ||
var response = keycard_go.keycardCallRPC($request) | ||
debug "callRPC", request, response |
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.
Time to drop this I think. It will log all mnemonics, PINs and other wonderful information
self.events.emit(SIGNAL_KEYCARD_AUTHORIZE_FAILURE, KeycardErrorArg(error: e.msg)) | ||
|
||
proc receiveKeycardSignalV2(self: Service, signal: string) {.slot.} = | ||
debug "keycard_signalV2", response=signal |
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.
This will also log mnemonics 🙌
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.
hmm, where are these changes coming from? 🤔
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 here, is this realted?
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.
Ah it's just some random code I ran into when coding my service. I found that this service had some code that could crash so I fixed it up a bit. It should have no UX changes or anything (there was no try/catch
on the parseEnum
, so if we ever added a new type in status-go, the code could crash in desktop)
Backpressure.debounce(root, 100, function() { | ||
pinInput.clearPin() | ||
})() | ||
pinInput.statesInitialization() |
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.
This should really do the whole clearPin()
, and with a delay (on purpose)
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 sure why I changed that 🤔
It might be a mistake on my part
@@ -133,20 +148,80 @@ KeycardBasePage { | |||
target: errorText | |||
visible: true | |||
} | |||
StateChangeScript { | |||
script: { | |||
pinInput.statesInitialization() |
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 here probably
65416c6
to
78e4617
Compare
78e4617
to
ed4446e
Compare
What does the PR do
Fixes #17079
Hooks all the keycard flows for the new onboarding.
Based on top of #17058 to have the fix for required properties
status-keycard-go PR here done by @igor-sirotin : keycard-tech/status-keycard-go#13
Affected areas
The new onboarding (using the feature flag).
This is mostly Nim code, but I had to modify the QML a lot because I needed to make a lot of the keycard calls async, because they can be slow and would freeze the app (most notable case is the setPin).
Architecture compliance
My PR is consistent with this document: Status Desktop Architecture Guide
Screenshot of functionality (including design for comparison)
Create account from scratch:
new-account-basic.webm
Create account with mnemonic:
new-account-mnemonic.webm
"Login" with old keycard:
old-account-keycard.webm
Impact on end user
Nothing without the feature flag.
When it is active, the new onboarding shows and all the flows for the keycard should work when there is no previous account (you need to delete or rename the data dir).
How to test
export FLAG_ONBOARDING_V2_ENABLED=1 && make run
Risk
Tick one:
Low because the feature flag needs to be enabled.
When enabled, worst case is some flows don't work properly.