diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index 3157be83261..1d2c1658229 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -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 @@ -105,6 +106,7 @@ type # Modules onboardingModule: onboarding_module.AccessInterface mainModule: main_module.AccessInterface + keycardChannelModule: keycard_channel_module.AccessInterface ################################################# # Forward declaration section @@ -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, @@ -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 @@ -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) = diff --git a/src/app/modules/keycard_channel/constants.nim b/src/app/modules/keycard_channel/constants.nim new file mode 100644 index 00000000000..523aa7b4f28 --- /dev/null +++ b/src/app/modules/keycard_channel/constants.nim @@ -0,0 +1,9 @@ +## Constants for keycard channel operational states +## These values must match the strings emitted by status-keycard-qt + +const KEYCARD_CHANNEL_STATE_IDLE* = "idle" +const KEYCARD_CHANNEL_STATE_WAITING_FOR_KEYCARD* = "waiting-for-keycard" +const KEYCARD_CHANNEL_STATE_READING* = "reading" +const KEYCARD_CHANNEL_STATE_ERROR* = "error" + + diff --git a/src/app/modules/keycard_channel/controller.nim b/src/app/modules/keycard_channel/controller.nim new file mode 100644 index 00000000000..294080279d3 --- /dev/null +++ b/src/app/modules/keycard_channel/controller.nim @@ -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) + + diff --git a/src/app/modules/keycard_channel/io_interface.nim b/src/app/modules/keycard_channel/io_interface.nim new file mode 100644 index 00000000000..cfbc0aee51d --- /dev/null +++ b/src/app/modules/keycard_channel/io_interface.nim @@ -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") + + diff --git a/src/app/modules/keycard_channel/module.nim b/src/app/modules/keycard_channel/module.nim new file mode 100644 index 00000000000..79b77c47857 --- /dev/null +++ b/src/app/modules/keycard_channel/module.nim @@ -0,0 +1,49 @@ +import nimqml + +import io_interface, view, controller +import app/global/global_singleton +import app/core/eventemitter +import ./constants + +export io_interface +export constants + +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) + diff --git a/src/app/modules/keycard_channel/view.nim b/src/app/modules/keycard_channel/view.nim new file mode 100644 index 00000000000..51128d5e7ee --- /dev/null +++ b/src/app/modules/keycard_channel/view.nim @@ -0,0 +1,62 @@ +import nimqml + +import ./io_interface +import ./constants + +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.keycardChannelState = KEYCARD_CHANNEL_STATE_IDLE + 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 + + # Constants for channel states (readonly properties for QML) + proc getStateIdle*(self: View): string {.slot.} = + return KEYCARD_CHANNEL_STATE_IDLE + QtProperty[string] stateIdle: + read = getStateIdle + + proc getStateWaitingForKeycard*(self: View): string {.slot.} = + return KEYCARD_CHANNEL_STATE_WAITING_FOR_KEYCARD + QtProperty[string] stateWaitingForKeycard: + read = getStateWaitingForKeycard + + proc getStateReading*(self: View): string {.slot.} = + return KEYCARD_CHANNEL_STATE_READING + QtProperty[string] stateReading: + read = getStateReading + + proc getStateError*(self: View): string {.slot.} = + return KEYCARD_CHANNEL_STATE_ERROR + QtProperty[string] stateError: + read = getStateError + + proc setup(self: View) = + self.QObject.setup + + proc delete*(self: View) = + self.QObject.delete + diff --git a/src/app_service/service/keycard/constants.nim b/src/app_service/service/keycard/constants.nim index c162037d03d..0b4059e3d5a 100644 --- a/src/app_service/service/keycard/constants.nim +++ b/src/app_service/service/keycard/constants.nim @@ -109,4 +109,7 @@ const ResponseParamWhisperKey* = RequestParamWhisperKey const ResponseParamMnemonicIdxs* = RequestParamMnemonicIdxs const ResponseParamTXSignature* = RequestParamTXSignature const ResponseParamExportedKey* = RequestParamExportedKey -const ResponseParamMasterKeyAddress* = RequestParamMasterKeyAddress \ No newline at end of file +const ResponseParamMasterKeyAddress* = RequestParamMasterKeyAddress + +const SignalKeycardStatusChanged* = "status-changed" +const SignalKeycardChannelStateChanged* = "channel-state-changed" diff --git a/src/app_service/service/keycard/service.nim b/src/app_service/service/keycard/service.nim index 32eec8db972..5710fca09ff 100644 --- a/src/app_service/service/keycard/service.nim +++ b/src/app_service/service/keycard/service.nim @@ -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)) diff --git a/src/app_service/service/keycardV2/service.nim b/src/app_service/service/keycardV2/service.nim index dda560c715a..ea7b56d7e03 100644 --- a/src/app_service/service/keycardV2/service.nim +++ b/src/app_service/service/keycardV2/service.nim @@ -9,6 +9,7 @@ import ./dto, rpc featureGuard KEYCARD_ENABLED: import keycard_go import constants as status_const + import ../keycard/constants as keycard_constants export dto @@ -21,6 +22,7 @@ const PUKLengthForStatusApp* = 12 const KeycardLibCallsInterval = 500 # 0.5 seconds const SIGNAL_KEYCARD_STATE_UPDATED* = "keycardStateUpdated" +const SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED* = "keycardChannelStateUpdated" const SIGNAL_KEYCARD_SET_PIN_FAILURE* = "keycardSetPinFailure" const SIGNAL_KEYCARD_AUTHORIZE_FINISHED* = "keycardAuthorizeFinished" const SIGNAL_KEYCARD_LOAD_MNEMONIC_FAILURE* = "keycardLoadMnemonicFailure" @@ -60,6 +62,9 @@ type KeycardExportedKeysArg* = ref object of Args exportedKeys*: KeycardExportedKeysDto + KeycardChannelStateArg* = ref object of Args + state*: string + include utils include app_service/common/async_tasks include async_tasks @@ -100,7 +105,6 @@ QtObject: if status_const.IS_MACOS and status_const.IS_INTEL: sleep 700 self.initializeRPC() - self.asyncStart(status_const.KEYCARDPAIRINGDATAFILE) discard proc initializeRPC(self: Service) {.slot, featureGuard(KEYCARD_ENABLED).} = @@ -110,10 +114,15 @@ QtObject: try: # Since only one service can register to signals, we pass the signal to the old service too var jsonSignal = signal.parseJson - if jsonSignal["type"].getStr == "status-changed": + let signalType = jsonSignal["type"].getStr + + if signalType == keycard_constants.SignalKeycardStatusChanged: let keycardEvent = jsonSignal["event"].toKeycardEventDto() - self.events.emit(SIGNAL_KEYCARD_STATE_UPDATED, KeycardEventArg(keycardEvent: keycardEvent)) + elif signalType == keycard_constants.SignalKeycardChannelStateChanged: + let state = jsonSignal["event"]["state"].getStr + debug "keycardV2 service: emitting channel state update", state=state, signal=SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED + self.events.emit(SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED, KeycardChannelStateArg(state: state)) except Exception as e: error "error receiving a keycard signal", err=e.msg, data = signal