Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -549,15 +549,15 @@ status-keycard-qt-clean:
ifeq ($(USE_STATUS_KEYCARD_QT),1)
KEYCARD_LIB := $(STATUSKEYCARD_QT_LIB)
KEYCARD_LIBDIR := $(STATUSKEYCARD_QT_LIBDIR)
KEYCARD_LINKNAME := status-keycard-qt
KEYCARD_DYLIB_NAME := libstatus-keycard-qt.dylib

else
KEYCARD_LIB := $(STATUSKEYCARDGO)
KEYCARD_LIBDIR := $(STATUSKEYCARDGO_LIBDIR)
KEYCARD_LINKNAME := keycard
KEYCARD_DYLIB_NAME := libkeycard.$(LIB_EXT)
endif

KEYCARD_DYLIB_NAME := $(notdir $(KEYCARD_LIB))
KEYCARD_LINKNAME := $(patsubst lib%,%,$(basename $(KEYCARD_DYLIB_NAME)))

QRCODEGEN := vendor/QR-Code-generator/c/libqrcodegen.a

$(QRCODEGEN): | deps
Expand Down
9 changes: 9 additions & 0 deletions src/app/boot/app_controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import app_service/service/market/service as market_service
import app/modules/onboarding/module as onboarding_module
import app/modules/onboarding/post_onboarding/[keycard_replacement_task, keycard_convert_account, save_biometrics_task]
import app/modules/main/module as main_module
import app/modules/keycard_channel/module as keycard_channel_module
import app/core/notifications/notifications_manager
import app/global/global_singleton
import app/global/app_signals
Expand Down Expand Up @@ -105,6 +106,7 @@ type
# Modules
onboardingModule: onboarding_module.AccessInterface
mainModule: main_module.AccessInterface
keycardChannelModule: keycard_channel_module.AccessInterface

#################################################
# Forward declaration section
Expand Down Expand Up @@ -233,6 +235,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.marketService = market_service.newService(statusFoundation.events, result.settingsService)

# Modules
result.keycardChannelModule = keycard_channel_module.newModule(statusFoundation.events)
result.onboardingModule = onboarding_module.newModule[AppController](
result,
statusFoundation.events,
Expand Down Expand Up @@ -299,6 +302,9 @@ proc delete*(self: AppController) =
self.onboardingModule.delete
self.onboardingModule = nil
self.mainModule.delete
if not self.keycardChannelModule.isNil:
self.keycardChannelModule.delete
self.keycardChannelModule = nil

self.appSettingsVariant.delete
self.localAppSettingsVariant.delete
Expand Down Expand Up @@ -346,6 +352,9 @@ proc initializeQmlContext(self: AppController) =
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
singletonInstance.engine.setRootContextProperty("metrics", self.metricsVariant)

# Load keycard channel module (available before login for Session API)
self.keycardChannelModule.load()

singletonInstance.engine.load(newQUrl("qrc:///main.qml"))

proc onboardingDidLoad*(self: AppController) =
Expand Down
27 changes: 27 additions & 0 deletions src/app/modules/keycard_channel/controller.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ./io_interface
import app/core/eventemitter
import app_service/service/keycardV2/service as keycard_serviceV2

type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter

proc newController*(
delegate: io_interface.AccessInterface,
events: EventEmitter
): Controller =
result = Controller()
result.delegate = delegate
result.events = events

proc delete*(self: Controller) =
discard

proc init*(self: Controller) =
# Listen to channel state changes
self.events.on(keycard_serviceV2.SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED) do(e: Args):
let args = keycard_serviceV2.KeycardChannelStateArg(e)
self.delegate.setKeycardChannelState(args.state)


23 changes: 23 additions & 0 deletions src/app/modules/keycard_channel/io_interface.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
## Abstract class for any input/interaction with this module.

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

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

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

# View Delegate Interface
# Delegate for the view must be declared here due to use of QtObject and multi
# inheritance, which is not well supported in Nim.
method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

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


47 changes: 47 additions & 0 deletions src/app/modules/keycard_channel/module.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import nimqml

import io_interface, view, controller
import app/global/global_singleton
import app/core/eventemitter

export io_interface

type
Module* = ref object of io_interface.AccessInterface
view: View
viewVariant: QVariant
controller: Controller
moduleLoaded: bool

proc newModule*(
events: EventEmitter,
): Module =
result = Module()
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events)
result.moduleLoaded = false

singletonInstance.engine.setRootContextProperty("keycardChannelModule", result.viewVariant)

method delete*(self: Module) =
self.view.delete
self.viewVariant.delete
self.controller.delete

method load*(self: Module) =
self.controller.init()
self.view.load()

method isLoaded*(self: Module): bool =
return self.moduleLoaded

proc checkIfModuleDidLoad(self: Module) =
self.moduleLoaded = true

method viewDidLoad*(self: Module) =
self.checkIfModuleDidLoad()

method setKeycardChannelState*(self: Module, state: string) =
self.view.setKeycardChannelState(state)

42 changes: 42 additions & 0 deletions src/app/modules/keycard_channel/view.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import nimqml

import ./io_interface

QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
keycardChannelState: string # Operational channel state

proc setup(self: View)
proc delete*(self: View)
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.delegate = delegate
result.setup()

proc load*(self: View) =
self.delegate.viewDidLoad()

proc keycardChannelStateChanged*(self: View) {.signal.}
proc setKeycardChannelState*(self: View, value: string) =
if self.keycardChannelState == value:
return
self.keycardChannelState = value
self.keycardChannelStateChanged()
proc getKeycardChannelState*(self: View): string {.slot.} =
return self.keycardChannelState
QtProperty[string] keycardChannelState:
read = getKeycardChannelState
write = setKeycardChannelState
notify = keycardChannelStateChanged

proc keycardDismissed*(self: View) {.slot.} =
self.setKeycardChannelState("")

proc setup(self: View) =
self.QObject.setup

proc delete*(self: View) =
self.QObject.delete

2 changes: 1 addition & 1 deletion src/app/modules/main/wallet_section/send_new/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ method authenticateAndTransfer*(self: Module, uuid: string, fromAddr: string) =
self.controller.authenticate()

method onUserAuthenticated*(self: Module, password: string, pin: string) =
if password.len == 0:
if password.len == 0 and pin.len == 0:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A password should be set in both cases, regular/keycard user, while pin only for the keycard user, that's why only password was checked. It's not incorrect this way ofc.

self.transactionWasSent(uuid = self.tmpSendTransactionDetails.uuid, chainId = 0, approvalTx = false, txHash = "", error = authenticationCanceled)
self.clearTmpData()
else:
Expand Down
3 changes: 3 additions & 0 deletions src/app/modules/onboarding/controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,6 @@ proc storeMetadataAsync*(self: Controller, name: string, paths: seq[string]) =

proc asyncImportLocalBackupFile*(self: Controller, filePath: string) =
self.generalService.asyncImportLocalBackupFile(filePath)

proc startKeycardDetection*(self: Controller) =
self.keycardServiceV2.startDetection()
3 changes: 3 additions & 0 deletions src/app/modules/onboarding/io_interface.nim
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ method requestDeleteBiometrics*(self: AccessInterface, account: string) {.base.}
method requestLocalBackup*(self: AccessInterface, backupImportFileUrl: string) {.base.} =
raise newException(ValueError, "No implementation available")

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

# This way (using concepts) is used only for the modules managed by AppController
type
DelegateInterface* = concept c
Expand Down
3 changes: 3 additions & 0 deletions src/app/modules/onboarding/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ method requestLocalBackup*[T](self: Module[T], backupImportFileUrl: string) =
method requestDeleteBiometrics*[T](self: Module[T], account: string) =
self.view.deleteBiometricsRequested(account)

method startKeycardDetection*[T](self: Module[T]) =
self.controller.startKeycardDetection()

proc runPostLoginTasks*[T](self: Module[T]) =
let tasks = self.postLoginTasks
for task in tasks:
Expand Down
3 changes: 3 additions & 0 deletions src/app/modules/onboarding/view.nim
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ QtObject:
proc startKeycardFactoryReset(self: View) {.slot.} =
self.delegate.startKeycardFactoryReset()

proc startKeycardDetection(self: View) {.slot.} =
self.delegate.startKeycardDetection()

proc delete*(self: View) =
self.QObject.delete

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ method resolveKeycardNextState*(self: InsertKeycardState, keycardFlowType: strin
return nil
if keycardFlowType == ResponseTypeValueCardInserted:
controller.setKeycardData(updatePredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WronglyInsertedCard, add = false))

# Special handling for LoadAccount flow - return to the state we came from
# (RepeatPin or PinSet) to continue waiting for ENTER_MNEMONIC event
if (self.flowType == FlowType.SetupNewKeycard or
self.flowType == FlowType.SetupNewKeycardNewSeedPhrase or
self.flowType == FlowType.SetupNewKeycardOldSeedPhrase) and
controller.getCurrentKeycardServiceFlow() == KCSFlowType.LoadAccount and
not self.getBackState.isNil:
let backStateType = self.getBackState.stateType
if backStateType == StateType.RepeatPin or backStateType == StateType.PinSet:
# Return to the previous state to continue waiting for mnemonic entry
return self.getBackState

# Default behavior for other flows
if self.flowType == FlowType.SetupNewKeycard:
return createState(StateType.KeycardInserted, self.flowType, self.getBackState)
return createState(StateType.KeycardInserted, self.flowType, nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,28 @@ method executeCancelCommand*(self: PinSetState, controller: Controller) =
self.flowType == FlowType.SetupNewKeycardNewSeedPhrase or
self.flowType == FlowType.SetupNewKeycardOldSeedPhrase or
self.flowType == FlowType.UnlockKeycard:
controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false)
controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false)

method resolveKeycardNextState*(self: PinSetState, keycardFlowType: string, keycardEvent: KeycardEvent,
controller: Controller): State =
# Handle temporary card disconnection during LoadAccount flow (after card initialization)
# This can happen if the user hasn't tapped "Continue" yet and the card disconnects
if self.flowType == FlowType.SetupNewKeycard or
self.flowType == FlowType.SetupNewKeycardNewSeedPhrase or
self.flowType == FlowType.SetupNewKeycardOldSeedPhrase:
# INSERT_CARD during LoadAccount flow means card is reconnecting after initialization
if keycardFlowType == ResponseTypeValueInsertCard and
keycardEvent.error.len > 0 and
keycardEvent.error == ErrorConnection and
controller.getCurrentKeycardServiceFlow() == KCSFlowType.LoadAccount:
# Don't cancel the flow - transition to InsertKeycard state and wait for reconnection
controller.reRunCurrentFlowLater()
return createState(StateType.InsertKeycard, self.flowType, self)
# CARD_INSERTED after temporary disconnection - stay in PinSet and continue
if keycardFlowType == ResponseTypeValueCardInserted and
controller.getCurrentKeycardServiceFlow() == KCSFlowType.LoadAccount:
# Card reconnected successfully, stay in PinSet
return nil

# No specific handling needed - this state transitions via primary button
return nil
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ method executeCancelCommand*(self: RepeatPinState, controller: Controller) =

method resolveKeycardNextState*(self: RepeatPinState, keycardFlowType: string, keycardEvent: KeycardEvent,
controller: Controller): State =
# Handle temporary card disconnection during LoadAccount flow (after card initialization)
# This happens on Android/iOS when card is disconnected and needs to be re-detected
if self.flowType == FlowType.SetupNewKeycard or
self.flowType == FlowType.SetupNewKeycardNewSeedPhrase or
self.flowType == FlowType.SetupNewKeycardOldSeedPhrase:
# INSERT_CARD during LoadAccount flow means card is reconnecting after initialization
if keycardFlowType == ResponseTypeValueInsertCard and
keycardEvent.error.len > 0 and
keycardEvent.error == ErrorConnection and
controller.getCurrentKeycardServiceFlow() == KCSFlowType.LoadAccount:
# Don't cancel the flow - transition to InsertKeycard state and wait for reconnection
controller.reRunCurrentFlowLater()
return createState(StateType.InsertKeycard, self.flowType, self)
# CARD_INSERTED after temporary disconnection - stay in RepeatPin and continue waiting
if keycardFlowType == ResponseTypeValueCardInserted and
controller.getCurrentKeycardServiceFlow() == KCSFlowType.LoadAccount:
# Card reconnected successfully, continue waiting for ENTER_MNEMONIC event
return nil

let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller)
if not state.isNil:
return state
Expand Down
5 changes: 4 additions & 1 deletion src/app_service/service/keycard/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,7 @@ const ResponseParamWhisperKey* = RequestParamWhisperKey
const ResponseParamMnemonicIdxs* = RequestParamMnemonicIdxs
const ResponseParamTXSignature* = RequestParamTXSignature
const ResponseParamExportedKey* = RequestParamExportedKey
const ResponseParamMasterKeyAddress* = RequestParamMasterKeyAddress
const ResponseParamMasterKeyAddress* = RequestParamMasterKeyAddress

const SignalKeycardStatusChanged* = "status-changed"
const SignalKeycardChannelStateChanged* = "channel-state-changed"
10 changes: 10 additions & 0 deletions src/app_service/service/keycard/service.nim
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ QtObject:
return

let flowType = typeObj.getStr
if flowType == SignalKeycardChannelStateChanged:
return #nothing related to flows here

let flowEvent = toKeycardEvent(eventObj)
self.lastReceivedKeycardData = (flowType: flowType, flowEvent: flowEvent)
self.events.emit(SIGNAL_KEYCARD_RESPONSE, KeycardLibArgs(flowType: flowType, flowEvent: flowEvent))
Expand All @@ -151,8 +154,15 @@ QtObject:
return seedPhrase

proc updateLocalPayloadForCurrentFlow(self: Service, obj: JsonNode, cleanBefore = false) {.featureGuard(KEYCARD_ENABLED).} =
# CRITICAL FIX: Check if obj is the same reference as setPayloadForCurrentFlow
# This happens when onTimeout calls startFlow(self.setPayloadForCurrentFlow)
# If we iterate and modify the same object, the iterator gets corrupted!
if cast[pointer](obj) == cast[pointer](self.setPayloadForCurrentFlow):
return

if cleanBefore:
self.setPayloadForCurrentFlow = %* {}

for k, v in obj:
self.setPayloadForCurrentFlow[k] = v

Expand Down
Loading