Skip to content

Commit e71a2fa

Browse files
committed
feat(keycard): Add support for keycard channel events
The keycard channel events will inform the app of the channel state (waiting for keycard, reading, error, idle). This will be used on mobile platforms to control a drawer that informs the user when it's required to tap the keycard.
1 parent d281472 commit e71a2fa

File tree

8 files changed

+193
-3
lines changed

8 files changed

+193
-3
lines changed

src/app/boot/app_controller.nim

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import app_service/service/market/service as market_service
4040
import app/modules/onboarding/module as onboarding_module
4141
import app/modules/onboarding/post_onboarding/[keycard_replacement_task, keycard_convert_account, save_biometrics_task]
4242
import app/modules/main/module as main_module
43+
import app/modules/keycard_channel/module as keycard_channel_module
4344
import app/core/notifications/notifications_manager
4445
import app/global/global_singleton
4546
import app/global/app_signals
@@ -105,6 +106,7 @@ type
105106
# Modules
106107
onboardingModule: onboarding_module.AccessInterface
107108
mainModule: main_module.AccessInterface
109+
keycardChannelModule: keycard_channel_module.AccessInterface
108110

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

235237
# Modules
238+
result.keycardChannelModule = keycard_channel_module.newModule(statusFoundation.events)
236239
result.onboardingModule = onboarding_module.newModule[AppController](
237240
result,
238241
statusFoundation.events,
@@ -299,6 +302,9 @@ proc delete*(self: AppController) =
299302
self.onboardingModule.delete
300303
self.onboardingModule = nil
301304
self.mainModule.delete
305+
if not self.keycardChannelModule.isNil:
306+
self.keycardChannelModule.delete
307+
self.keycardChannelModule = nil
302308

303309
self.appSettingsVariant.delete
304310
self.localAppSettingsVariant.delete
@@ -346,6 +352,9 @@ proc initializeQmlContext(self: AppController) =
346352
singletonInstance.engine.setRootContextProperty("globalUtils", self.globalUtilsVariant)
347353
singletonInstance.engine.setRootContextProperty("metrics", self.metricsVariant)
348354

355+
# Load keycard channel module (available before login for Session API)
356+
self.keycardChannelModule.load()
357+
349358
singletonInstance.engine.load(newQUrl("qrc:///main.qml"))
350359

351360
proc onboardingDidLoad*(self: AppController) =
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Constants for keycard channel operational states
2+
## These values must match the strings emitted by status-keycard-qt
3+
4+
const KEYCARD_CHANNEL_STATE_IDLE* = "idle"
5+
const KEYCARD_CHANNEL_STATE_WAITING_FOR_KEYCARD* = "waiting-for-keycard"
6+
const KEYCARD_CHANNEL_STATE_READING* = "reading"
7+
const KEYCARD_CHANNEL_STATE_ERROR* = "error"
8+
9+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ./io_interface
2+
import app/core/eventemitter
3+
import app_service/service/keycardV2/service as keycard_serviceV2
4+
5+
type
6+
Controller* = ref object of RootObj
7+
delegate: io_interface.AccessInterface
8+
events: EventEmitter
9+
10+
proc newController*(
11+
delegate: io_interface.AccessInterface,
12+
events: EventEmitter
13+
): Controller =
14+
result = Controller()
15+
result.delegate = delegate
16+
result.events = events
17+
18+
proc delete*(self: Controller) =
19+
discard
20+
21+
proc init*(self: Controller) =
22+
# Listen to channel state changes
23+
self.events.on(keycard_serviceV2.SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED) do(e: Args):
24+
let args = keycard_serviceV2.KeycardChannelStateArg(e)
25+
self.delegate.setKeycardChannelState(args.state)
26+
27+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
type
2+
AccessInterface* {.pure inheritable.} = ref object of RootObj
3+
## Abstract class for any input/interaction with this module.
4+
5+
method delete*(self: AccessInterface) {.base.} =
6+
raise newException(ValueError, "No implementation available")
7+
8+
method load*(self: AccessInterface) {.base.} =
9+
raise newException(ValueError, "No implementation available")
10+
11+
method isLoaded*(self: AccessInterface): bool {.base.} =
12+
raise newException(ValueError, "No implementation available")
13+
14+
# View Delegate Interface
15+
# Delegate for the view must be declared here due to use of QtObject and multi
16+
# inheritance, which is not well supported in Nim.
17+
method viewDidLoad*(self: AccessInterface) {.base.} =
18+
raise newException(ValueError, "No implementation available")
19+
20+
method setKeycardChannelState*(self: AccessInterface, state: string) {.base.} =
21+
raise newException(ValueError, "No implementation available")
22+
23+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import nimqml
2+
3+
import io_interface, view, controller
4+
import app/global/global_singleton
5+
import app/core/eventemitter
6+
import ./constants
7+
8+
export io_interface
9+
export constants
10+
11+
type
12+
Module* = ref object of io_interface.AccessInterface
13+
view: View
14+
viewVariant: QVariant
15+
controller: Controller
16+
moduleLoaded: bool
17+
18+
proc newModule*(
19+
events: EventEmitter,
20+
): Module =
21+
result = Module()
22+
result.view = view.newView(result)
23+
result.viewVariant = newQVariant(result.view)
24+
result.controller = controller.newController(result, events)
25+
result.moduleLoaded = false
26+
27+
singletonInstance.engine.setRootContextProperty("keycardChannelModule", result.viewVariant)
28+
29+
method delete*(self: Module) =
30+
self.view.delete
31+
self.viewVariant.delete
32+
self.controller.delete
33+
34+
method load*(self: Module) =
35+
self.controller.init()
36+
self.view.load()
37+
38+
method isLoaded*(self: Module): bool =
39+
return self.moduleLoaded
40+
41+
proc checkIfModuleDidLoad(self: Module) =
42+
self.moduleLoaded = true
43+
44+
method viewDidLoad*(self: Module) =
45+
self.checkIfModuleDidLoad()
46+
47+
method setKeycardChannelState*(self: Module, state: string) =
48+
self.view.setKeycardChannelState(state)
49+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import nimqml
2+
3+
import ./io_interface
4+
import ./constants
5+
6+
QtObject:
7+
type
8+
View* = ref object of QObject
9+
delegate: io_interface.AccessInterface
10+
keycardChannelState: string # Operational channel state
11+
12+
proc setup(self: View)
13+
proc delete*(self: View)
14+
proc newView*(delegate: io_interface.AccessInterface): View =
15+
new(result, delete)
16+
result.delegate = delegate
17+
result.keycardChannelState = KEYCARD_CHANNEL_STATE_IDLE
18+
result.setup()
19+
20+
proc load*(self: View) =
21+
self.delegate.viewDidLoad()
22+
23+
proc keycardChannelStateChanged*(self: View) {.signal.}
24+
proc setKeycardChannelState*(self: View, value: string) =
25+
if self.keycardChannelState == value:
26+
return
27+
self.keycardChannelState = value
28+
self.keycardChannelStateChanged()
29+
proc getKeycardChannelState*(self: View): string {.slot.} =
30+
return self.keycardChannelState
31+
QtProperty[string] keycardChannelState:
32+
read = getKeycardChannelState
33+
write = setKeycardChannelState
34+
notify = keycardChannelStateChanged
35+
36+
# Constants for channel states (readonly properties for QML)
37+
proc getStateIdle*(self: View): string {.slot.} =
38+
return KEYCARD_CHANNEL_STATE_IDLE
39+
QtProperty[string] stateIdle:
40+
read = getStateIdle
41+
42+
proc getStateWaitingForKeycard*(self: View): string {.slot.} =
43+
return KEYCARD_CHANNEL_STATE_WAITING_FOR_KEYCARD
44+
QtProperty[string] stateWaitingForKeycard:
45+
read = getStateWaitingForKeycard
46+
47+
proc getStateReading*(self: View): string {.slot.} =
48+
return KEYCARD_CHANNEL_STATE_READING
49+
QtProperty[string] stateReading:
50+
read = getStateReading
51+
52+
proc getStateError*(self: View): string {.slot.} =
53+
return KEYCARD_CHANNEL_STATE_ERROR
54+
QtProperty[string] stateError:
55+
read = getStateError
56+
57+
proc setup(self: View) =
58+
self.QObject.setup
59+
60+
proc delete*(self: View) =
61+
self.QObject.delete
62+

src/app_service/service/keycard/service.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ QtObject:
125125
return
126126

127127
let flowType = typeObj.getStr
128+
if flowType == "channel-state-changed":
129+
return #nothing related to flows here
130+
128131
let flowEvent = toKeycardEvent(eventObj)
129132
self.lastReceivedKeycardData = (flowType: flowType, flowEvent: flowEvent)
130133
self.events.emit(SIGNAL_KEYCARD_RESPONSE, KeycardLibArgs(flowType: flowType, flowEvent: flowEvent))

src/app_service/service/keycardV2/service.nim

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const PUKLengthForStatusApp* = 12
2121
const KeycardLibCallsInterval = 500 # 0.5 seconds
2222

2323
const SIGNAL_KEYCARD_STATE_UPDATED* = "keycardStateUpdated"
24+
const SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED* = "keycardChannelStateUpdated"
2425
const SIGNAL_KEYCARD_SET_PIN_FAILURE* = "keycardSetPinFailure"
2526
const SIGNAL_KEYCARD_AUTHORIZE_FINISHED* = "keycardAuthorizeFinished"
2627
const SIGNAL_KEYCARD_LOAD_MNEMONIC_FAILURE* = "keycardLoadMnemonicFailure"
@@ -60,6 +61,9 @@ type
6061
KeycardExportedKeysArg* = ref object of Args
6162
exportedKeys*: KeycardExportedKeysDto
6263

64+
KeycardChannelStateArg* = ref object of Args
65+
state*: string
66+
6367
include utils
6468
include app_service/common/async_tasks
6569
include async_tasks
@@ -100,7 +104,6 @@ QtObject:
100104
if status_const.IS_MACOS and status_const.IS_INTEL:
101105
sleep 700
102106
self.initializeRPC()
103-
self.asyncStart(status_const.KEYCARDPAIRINGDATAFILE)
104107
discard
105108

106109
proc initializeRPC(self: Service) {.slot, featureGuard(KEYCARD_ENABLED).} =
@@ -110,10 +113,15 @@ QtObject:
110113
try:
111114
# Since only one service can register to signals, we pass the signal to the old service too
112115
var jsonSignal = signal.parseJson
113-
if jsonSignal["type"].getStr == "status-changed":
116+
let signalType = jsonSignal["type"].getStr
117+
118+
if signalType == "status-changed":
114119
let keycardEvent = jsonSignal["event"].toKeycardEventDto()
115-
116120
self.events.emit(SIGNAL_KEYCARD_STATE_UPDATED, KeycardEventArg(keycardEvent: keycardEvent))
121+
elif signalType == "channel-state-changed":
122+
let state = jsonSignal["event"]["state"].getStr
123+
debug "keycardV2 service: emitting channel state update", state=state, signal=SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED
124+
self.events.emit(SIGNAL_KEYCARD_CHANNEL_STATE_UPDATED, KeycardChannelStateArg(state: state))
117125
except Exception as e:
118126
error "error receiving a keycard signal", err=e.msg, data = signal
119127

0 commit comments

Comments
 (0)