Skip to content

Commit

Permalink
feat(onbaording): integrate all happy paths for the non-keycard flows
Browse files Browse the repository at this point in the history
Fixes #17004
  • Loading branch information
jrainville authored Jan 21, 2025
1 parent 9dea479 commit 07675f3
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 126 deletions.
55 changes: 34 additions & 21 deletions src/app/boot/app_controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ proc connect(self: AppController) =
elif defined(production):
setLogLevel(chronicles.LogLevel.INFO)

# TODO remove these functions once we have only the new onboarding module
proc shouldStartWithOnboardingScreen(self: AppController): bool =
return self.accountsService.openedAccounts().len == 0
proc shouldUseTheNewOnboardingModule(self: AppController): bool =
# Only the onboarding for new users is implemented in the new module for now
return singletonInstance.featureFlags().getOnboardingV2Enabled() and self.shouldStartWithOnboardingScreen()

proc newAppController*(statusFoundation: StatusFoundation): AppController =
result = AppController()
result.syncKeycardBasedOnAppWalletState = false
Expand Down Expand Up @@ -244,17 +251,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.walletAccountService, result.networkService, result.nodeService, result.tokenService)
result.sharedUrlsService = shared_urls_service.newService(statusFoundation.events, statusFoundation.threadpool)
# Modules
result.startupModule = startup_module.newModule[AppController](
result,
statusFoundation.events,
result.keychainService,
result.accountsService,
result.generalService,
result.profileService,
result.keycardService,
result.devicesService
)
if singletonInstance.featureFlags().getOnboardingV2Enabled():
if result.shouldUseTheNewOnboardingModule():
result.onboardingModule = onboarding_module.newModule[AppController](
result,
statusFoundation.events,
Expand All @@ -263,6 +260,17 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.devicesService,
result.keycardServiceV2,
)
else:
result.startupModule = startup_module.newModule[AppController](
result,
statusFoundation.events,
result.keychainService,
result.accountsService,
result.generalService,
result.profileService,
result.keycardService,
result.devicesService
)
result.mainModule = main_module.newModule[AppController](
result,
statusFoundation.events,
Expand Down Expand Up @@ -405,8 +413,7 @@ proc checkForStoringPasswordToKeychain(self: AppController) =
else:
self.keychainService.storeData(account.keyUid, self.startupModule.getPin())

proc startupDidLoad*(self: AppController) =
# TODO move these functions to onboardingDidLoad
proc initializeQmlContext(self: AppController) =
singletonInstance.engine.setRootContextProperty("localAppSettings", self.localAppSettingsVariant)
singletonInstance.engine.setRootContextProperty("localAccountSettings", self.localAccountSettingsVariant)
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
Expand All @@ -415,17 +422,22 @@ proc startupDidLoad*(self: AppController) =

# We need to init a language service once qml is loaded
self.languageService.init()
# We need this to set app width/height appropriatelly on the app start.
self.startupModule.startUpUIRaised()
# We need this to set app width/height appropriately on the app start.
if not self.startupModule.isNil:
self.startupModule.startUpUIRaised()

proc startupDidLoad*(self: AppController) =
self.initializeQmlContext()

proc onboardingDidLoad*(self: AppController) =
debug "NEW ONBOARDING LOADED"
# TODO when removing the old startup module, we should move the functions above here
self.initializeQmlContext()

proc mainDidLoad*(self: AppController) =
self.applyNecessaryActionsAfterLoggingIn()
self.startupModule.moveToAppState()
self.checkForStoringPasswordToKeychain()
if not self.startupModule.isNil:
self.applyNecessaryActionsAfterLoggingIn()
self.startupModule.moveToAppState()
self.checkForStoringPasswordToKeychain()

proc start*(self: AppController) =
self.keycardService.init()
Expand All @@ -435,9 +447,10 @@ proc start*(self: AppController) =
self.accountsService.init()
self.devicesService.init()

self.startupModule.load()
if singletonInstance.featureFlags().getOnboardingV2Enabled():
if self.shouldUseTheNewOnboardingModule():
self.onboardingModule.load()
else:
self.startupModule.load()

proc load(self: AppController) =
self.settingsService.init()
Expand Down
37 changes: 34 additions & 3 deletions src/app/modules/onboarding/controller.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import chronicles, strutils
import io_interface
import uuids

import app/core/eventemitter
import app/core/signals/types
import app_service/service/general/service as general_service
import app_service/service/accounts/service as accounts_service
import app_service/service/accounts/dto/image_crop_rectangle
Expand All @@ -15,6 +17,7 @@ type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter
connectionIds: seq[UUID]
generalService: general_service.Service
accountsService: accounts_service.Service
devicesService: devices_service.Service
Expand All @@ -37,11 +40,25 @@ proc newController*(
result.devicesService = devicesService
result.keycardServiceV2 = keycardServiceV2

proc disconnect*(self: Controller) =
for id in self.connectionIds:
self.events.disconnect(id)

proc delete*(self: Controller) =
discard
self.disconnect()

proc init*(self: Controller) =
discard
var handlerId = self.events.onWithUUID(SignalType.NodeLogin.event) do(e:Args):
let signal = NodeSignal(e)
self.delegate.onNodeLogin(signal.error, signal.account, signal.settings)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e: Args):
let args = LocalPairingStatus(e)
if args.pairingType != PairingType.AppSync:
return
self.delegate.onLocalPairingStatusUpdate(args)
self.connectionIds.add(handlerId)

proc setPin*(self: Controller, pin: string): bool =
self.keycardServiceV2.setPin(pin)
Expand Down Expand Up @@ -91,4 +108,18 @@ proc restoreAccountAndLogin*(self: Controller, password, mnemonic: string, recov
imagePath = "",
ImageCropRectangle(),
keycardInstanceUID,
)
)

proc setLoggedInAccount*(self: Controller, account: AccountDto) =
self.accountsService.setLoggedInAccount(account)
self.accountsService.updateLoggedInAccount(account.name, account.images)

proc loginLocalPairingAccount*(self: Controller, account: AccountDto, password, chatkey: string) =
self.accountsService.login(
account,
password,
chatPrivateKey = chatKey
)

proc finishPairingThroughSeedPhraseProcess*(self: Controller, installationId: string) =
self.devicesService.finishPairingThroughSeedPhraseProcess(installationId)
15 changes: 14 additions & 1 deletion src/app/modules/onboarding/io_interface.nim
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
type
AccessInterface* {.pure inheritable.} = ref object of RootObj

from app_service/service/settings/dto/settings import SettingsDto
from app_service/service/accounts/dto/accounts import AccountDto
from app_service/service/devices/dto/local_pairing_status import LocalPairingStatus

method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

method onAppLoaded*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

method onNodeLogin*(self: AccessInterface, error: string, account: AccountDto, settings: SettingsDto) {.base.} =
raise newException(ValueError, "No implementation available")

method load*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

Expand All @@ -28,10 +35,16 @@ method validateLocalPairingConnectionString*(self: AccessInterface, connectionSt
method inputConnectionStringForBootstrapping*(self: AccessInterface, connectionString: string) {.base.} =
raise newException(ValueError, "No implementation available")

method finishOnboardingFlow*(self: AccessInterface, primaryFlowInt, secondaryFlowInt: int, dataJson: string): string {.base.} =
method finishOnboardingFlow*(self: AccessInterface, flowInt: int, dataJson: string): string {.base.} =
raise newException(ValueError, "No implementation available")

method onLocalPairingStatusUpdate*(self: AccessInterface, status: LocalPairingStatus) {.base.} =
raise newException(ValueError, "No implementation available")

# This way (using concepts) is used only for the modules managed by AppController
type
DelegateInterface* = concept c
c.onboardingDidLoad()
c.appReady()
c.finishAppLoading()
c.userLoggedIn()
137 changes: 82 additions & 55 deletions src/app/modules/onboarding/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ import app_service/service/general/service as general_service
import app_service/service/accounts/service as accounts_service
import app_service/service/devices/service as devices_service
import app_service/service/keycardV2/service as keycard_serviceV2
from app_service/service/settings/dto/settings import SettingsDto
from app_service/service/accounts/dto/accounts import AccountDto

export io_interface

logScope:
topics = "onboarding-module"

type PrimaryFlow* {.pure} = enum
Unknown = 0,
CreateProfile,
Login

type SecondaryFlow* {.pure} = enum
Unknown = 0,
CreateProfileWithPassword,
Expand All @@ -37,6 +34,7 @@ type
view: View
viewVariant: QVariant
controller: Controller
localPairingStatus: LocalPairingStatus

proc newModule*[T](
delegate: T,
Expand Down Expand Up @@ -67,6 +65,7 @@ method delete*[T](self: Module[T]) =
self.controller.delete

method onAppLoaded*[T](self: Module[T]) =
self.view.appLoaded()
singletonInstance.engine.setRootContextProperty("onboardingModule", newQVariant())
self.view.delete
self.view = nil
Expand Down Expand Up @@ -98,65 +97,93 @@ method validateLocalPairingConnectionString*[T](self: Module[T], connectionStrin
method inputConnectionStringForBootstrapping*[T](self: Module[T], connectionString: string) =
self.controller.inputConnectionStringForBootstrapping(connectionString)

method finishOnboardingFlow*[T](self: Module[T], primaryFlowInt, secondaryFlowInt: int, dataJson: string): string =
method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string): string =
try:
let primaryFlow = PrimaryFlow(primaryFlowInt)
let secondaryFlow = SecondaryFlow(secondaryFlowInt)
let flow = SecondaryFlow(flowInt)

let data = parseJson(dataJson)
let password = data["password"].str
let seedPhrase = data["seedPhrase"].str
let seedPhrase = data["seedphrase"].str

var err = ""

# CREATE PROFILE PRIMARY FLOW
if primaryFlow == PrimaryFlow.CreateProfile:
case secondaryFlow:
of SecondaryFlow.CreateProfileWithPassword:
err = self.controller.createAccountAndLogin(password)
of SecondaryFlow.CreateProfileWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = false,
keycardInstanceUID = "",
)
of SecondaryFlow.CreateProfileWithKeycard:
# TODO implement keycard function
discard
of SecondaryFlow.CreateProfileWithKeycardNewSeedphrase:
# TODO implement keycard function
discard
of SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase:
# TODO implement keycard function
discard
else:
raise newException(ValueError, "Unknown secondary flow for CreateProfile: " & $secondaryFlow)

# LOGIN PRIMARY FLOW
elif primaryFlow == PrimaryFlow.Login:
case secondaryFlow:
of SecondaryFlow.LoginWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = true,
keycardInstanceUID = "",
)
of SecondaryFlow.LoginWithSyncing:
self.controller.inputConnectionStringForBootstrapping(data["connectionString"].str)
of SecondaryFlow.LoginWithKeycard:
# TODO implement keycard function
discard
else:
raise newException(ValueError, "Unknown secondary flow for Login: " & $secondaryFlow)
if err != "":
raise newException(ValueError, err)
else:
raise newException(ValueError, "Unknown primary flow: " & $primaryFlow)

case flow:
# CREATE PROFILE FLOWS
of SecondaryFlow.CreateProfileWithPassword:
err = self.controller.createAccountAndLogin(password)
of SecondaryFlow.CreateProfileWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = false,
keycardInstanceUID = "",
)
of SecondaryFlow.CreateProfileWithKeycard:
# TODO implement keycard function
discard
of SecondaryFlow.CreateProfileWithKeycardNewSeedphrase:
# TODO implement keycard function
discard
of SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase:
# TODO implement keycard function
discard

# LOGIN FLOWS
of SecondaryFlow.LoginWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = true,
keycardInstanceUID = "",
)
of SecondaryFlow.LoginWithSyncing:
# The pairing was already done directly through inputConnectionStringForBootstrapping, we can login
self.controller.loginLocalPairingAccount(
self.localPairingStatus.account,
self.localPairingStatus.password,
self.localPairingStatus.chatKey,
)
of SecondaryFlow.LoginWithKeycard:
# TODO implement keycard function
discard
else:
raise newException(ValueError, "Unknown flow: " & $flow)

return err
except Exception as e:
error "Error finishing Onboarding Flow", msg = e.msg
return e.msg

proc finishAppLoading2[T](self: Module[T]) =
self.delegate.appReady()

# TODO get the flow to send the right metric
# let currStateObj = self.view.currentStartupStateObj()
# if not currStateObj.isNil:
# var eventType = "user-logged-in"
# if currStateObj.flowType() != FlowType.AppLogin:
# eventType = "onboarding-completed"
# singletonInstance.globalEvents.addCentralizedMetricIfEnabled(eventType,
# $(%*{"flowType": currStateObj.flowType()}))

self.delegate.finishAppLoading()

method onNodeLogin*[T](self: Module[T], error: string, account: AccountDto, settings: SettingsDto) =
if error.len != 0:
# TODO: Handle error
echo "ERROR from onNodeLogin: ", error
return

self.controller.setLoggedInAccount(account)

if self.localPairingStatus != nil and self.localPairingStatus.installation != nil and self.localPairingStatus.installation.id != "":
# We tried to login by pairing, so finilize the process
self.controller.finishPairingThroughSeedPhraseProcess(self.localPairingStatus.installation.id)

self.finishAppLoading2()

method onLocalPairingStatusUpdate*[T](self: Module[T], status: LocalPairingStatus) =
self.localPairingStatus = status
self.view.setSyncState(status.state.int)

{.pop.}
Loading

0 comments on commit 07675f3

Please sign in to comment.