Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jrainville
Copy link
Member

@jrainville jrainville commented Jan 24, 2025

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

Screenshot of functionality (including design for comparison)

  • I've checked the design and this PR matches it

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

  1. Enable the feature flag: export FLAG_ONBOARDING_V2_ENABLED=1 && make run
  2. Make sure you have no previous account (delete the data dir)
  3. Use the onboarding. All flows to create an account should work now

Risk

Tick one:

  • Low risk: 2 devs MUST perform testing as specified above and attach their results as comments to this PR before merging.
  • High risk: QA team MUST perform additional testing in the specified affected areas before merging.

Low because the feature flag needs to be enabled.

When enabled, worst case is some flows don't work properly.

@jrainville jrainville requested review from a team, micieslak, caybro and alexjba as code owners January 24, 2025 20:23
@jrainville jrainville requested review from Cuteivist, igor-sirotin and glitchminer and removed request for a team January 24, 2025 20:23
@jrainville jrainville changed the title Feat/status keycard go api v2 Implement the keycard flows for the new onbaording Jan 24, 2025
@status-im-auto
Copy link
Member

status-im-auto commented Jan 24, 2025

Jenkins Builds

Click to see older builds (47)
Commit #️⃣ Finished (UTC) Duration Platform Result
d711fb4 #1 2025-01-24 20:29:32 ~6 min macos/aarch64 📄log
d711fb4 #1 2025-01-24 20:31:29 ~8 min macos/x86_64 📄log
✔️ d711fb4 #1 2025-01-24 20:31:45 ~8 min tests/nim 📄log
d711fb4 #1 2025-01-24 20:32:45 ~9 min linux/x86_64 📄log
d711fb4 #1 2025-01-24 20:36:03 ~12 min tests/ui 📄log
d711fb4 #1 2025-01-24 20:37:40 ~14 min windows/x86_64 📄log
✔️ d711fb4 #1 2025-01-24 20:49:12 ~25 min linux-nix/x86_64 📦tgz
6b598ce #3 2025-01-27 16:16:18 ~6 min macos/aarch64 📄log
6b598ce #3 2025-01-27 16:21:00 ~11 min windows/x86_64 📄log
6b598ce #3 2025-01-27 16:22:14 ~12 min macos/x86_64 📄log
6b598ce #3 2025-01-27 16:26:32 ~16 min linux/x86_64 📄log
✔️ 6b598ce #3 2025-01-27 16:28:46 ~18 min tests/nim 📄log
✔️ 6b598ce #3 2025-01-27 16:36:30 ~26 min linux-nix/x86_64 📦tgz
6b598ce #3 2025-01-27 19:53:58 ~13 min tests/ui 📄log
51baa19 #4 2025-01-28 20:20:33 ~6 min macos/x86_64 📄log
51baa19 #4 2025-01-28 20:21:19 ~7 min linux/x86_64 📄log
✔️ 51baa19 #4 2025-01-28 20:22:44 ~8 min tests/nim 📄log
51baa19 #4 2025-01-28 20:26:10 ~11 min windows/x86_64 📄log
51baa19 #4 2025-01-28 20:26:22 ~12 min tests/ui 📄log
✔️ 51baa19 #4 2025-01-28 20:37:02 ~22 min linux-nix/x86_64 📦tgz
3beb47e #5 2025-01-28 21:10:43 ~4 min macos/x86_64 📄log
3beb47e #5 2025-01-28 21:13:17 ~7 min linux/x86_64 📄log
✔️ 3beb47e #5 2025-01-28 21:15:17 ~8 min tests/nim 📄log
3beb47e #5 2025-01-28 21:15:52 ~9 min windows/x86_64 📄log
3beb47e #5 2025-01-28 21:18:36 ~12 min tests/ui 📄log
✔️ 3beb47e #5 2025-01-28 21:27:34 ~21 min linux-nix/x86_64 📦tgz
f25d096 #6 2025-01-29 00:57:57 ~5 min macos/x86_64 📄log
✔️ f25d096 #6 2025-01-29 01:00:22 ~8 min tests/nim 📄log
f25d096 #6 2025-01-29 01:00:38 ~8 min linux/x86_64 📄log
f25d096 #6 2025-01-29 01:02:05 ~9 min windows/x86_64 📄log
f25d096 #6 2025-01-29 01:03:41 ~11 min tests/ui 📄log
✔️ f25d096 #6 2025-01-29 01:10:27 ~18 min linux-nix/x86_64 📦tgz
✔️ f25d096 #6 2025-01-29 08:10:02 ~7 hr 17 min macos/aarch64 🍎dmg
✔️ b3c028a #7 2025-01-29 15:41:41 ~5 min macos/aarch64 🍎dmg
b3c028a #7 2025-01-29 15:42:11 ~5 min macos/x86_64 📄log
b3c028a #7 2025-01-29 15:43:35 ~7 min linux/x86_64 📄log
✔️ b3c028a #7 2025-01-29 15:44:28 ~8 min tests/nim 📄log
b3c028a #7 2025-01-29 15:48:49 ~12 min windows/x86_64 📄log
b3c028a #7 2025-01-29 15:50:13 ~13 min tests/ui 📄log
✔️ b3c028a #7 2025-01-29 15:57:21 ~21 min linux-nix/x86_64 📦tgz
65416c6 #8 2025-01-29 17:25:37 ~4 min macos/x86_64 📄log
✔️ 65416c6 #8 2025-01-29 17:26:46 ~5 min macos/aarch64 🍎dmg
65416c6 #8 2025-01-29 17:28:13 ~7 min linux/x86_64 📄log
✔️ 65416c6 #8 2025-01-29 17:29:56 ~8 min tests/nim 📄log
65416c6 #8 2025-01-29 17:30:38 ~9 min windows/x86_64 📄log
65416c6 #8 2025-01-29 17:34:41 ~13 min tests/ui 📄log
✔️ 65416c6 #8 2025-01-29 17:42:39 ~21 min linux-nix/x86_64 📦tgz
Commit #️⃣ Finished (UTC) Duration Platform Result
78e4617 #9 2025-01-29 18:10:47 ~4 min macos/x86_64 📄log
✔️ 78e4617 #9 2025-01-29 18:11:26 ~5 min macos/aarch64 🍎dmg
78e4617 #9 2025-01-29 18:13:05 ~7 min linux/x86_64 📄log
✔️ 78e4617 #9 2025-01-29 18:14:43 ~8 min tests/nim 📄log
78e4617 #9 2025-01-29 18:16:00 ~9 min windows/x86_64 📄log
✔️ 78e4617 #9 2025-01-29 18:18:28 ~12 min tests/ui 📄log
✔️ 78e4617 #9 2025-01-29 18:26:42 ~20 min linux-nix/x86_64 📦tgz
ed4446e #10 2025-01-29 20:01:29 ~4 min macos/x86_64 📄log
✔️ ed4446e #10 2025-01-29 20:03:18 ~6 min macos/aarch64 🍎dmg
ed4446e #10 2025-01-29 20:04:17 ~7 min linux/x86_64 📄log
✔️ ed4446e #10 2025-01-29 20:06:04 ~9 min tests/nim 📄log
ed4446e #10 2025-01-29 20:06:58 ~9 min windows/x86_64 📄log
✔️ ed4446e #10 2025-01-29 20:13:45 ~16 min tests/ui 📄log
✔️ ed4446e #10 2025-01-29 20:18:10 ~21 min linux-nix/x86_64 📦tgz

Copy link
Member

@caybro caybro left a 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 :)

storybook/pages/OnboardingLayoutPage.qml Show resolved Hide resolved
ui/StatusQ/src/onboarding/enums.h Show resolved Hide resolved
@@ -42,7 +41,8 @@ class OnboardingEnums
Locked,
// exit states
NotEmpty,
Empty
Empty,
Authorized
Copy link
Member

Choose a reason for hiding this comment

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

When is this used?

Copy link
Member Author

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)
Copy link
Member

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

Copy link
Member

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?

ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml Outdated Show resolved Hide resolved
@@ -147,7 +166,9 @@ Page {
syncState: root.onboardingStore.syncState
addKeyPairState: root.onboardingStore.addKeyPairState

seedWords: root.onboardingStore.getMnemonic().split(" ")
getSeedWords: function () {
Copy link
Member

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?

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

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

Just declare those signals?

Copy link
Member

@micieslak micieslak left a 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)
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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()
Copy link
Member

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)
Copy link
Member

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 futuresetPinFailed` will be called or some other thing will happen which means success (btw, how we may know that pin setting succeeded)?

@caybro
Copy link
Member

caybro commented Jan 27, 2025

@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

Base automatically changed from feat/new-onboarding-metrics to master January 27, 2025 16:06
@jrainville jrainville force-pushed the feat/status-keycard-go-api-v2 branch from d711fb4 to 6b598ce Compare January 27, 2025 16:09
@jrainville jrainville force-pushed the feat/status-keycard-go-api-v2 branch from 6b598ce to 51baa19 Compare January 28, 2025 20:14
Copy link
Member Author

@jrainville jrainville left a 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

storybook/pages/OnboardingLayoutPage.qml Show resolved Hide resolved
@@ -42,7 +41,8 @@ class OnboardingEnums
Locked,
// exit states
NotEmpty,
Empty
Empty,
Authorized
Copy link
Member Author

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
Copy link
Member Author

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 () {
Copy link
Member Author

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
Copy link
Member Author

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.

Copy link
Contributor

@igor-sirotin igor-sirotin left a 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?
Copy link
Contributor

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) =
Copy link
Contributor

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,
Copy link
Contributor

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.

src/app/modules/onboarding/module.nim Outdated Show resolved Hide resolved
instanceUid: string,
keycardKeys: KeycardExportedKeysDto,
recoverAccount: bool,
displayName: string,
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

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
Copy link
Contributor

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 🙌

Copy link
Contributor

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? 🤔

Copy link
Contributor

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?

Copy link
Member Author

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()
Copy link
Member

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)

Copy link
Member Author

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()
Copy link
Member

Choose a reason for hiding this comment

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

same here probably

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Onboarding] Integrate Keycard flows for the new onboarding
6 participants