From 557703543c655332abbae16b409390e2a2549af8 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 14 Nov 2022 11:24:16 +0100 Subject: [PATCH] fix(@desktop/keycard): migrating keypair looks somehow stucked for a while before switching to `Migrating key pair to Keycard` state Fixes: #8177 --- src/app/boot/app_controller.nim | 3 +- .../profile_section/keycard/controller.nim | 2 + .../main/wallet_section/accounts/module.nim | 3 + .../keycard_popup/controller.nim | 30 +++++-- .../internal/migrating_key_pair_state.nim | 74 ++++++++++++------ .../service/accounts/async_tasks.nim | 21 +++++ src/app_service/service/accounts/service.nim | 78 ++++++++++++------- .../service/wallet_account/async_tasks.nim | 23 ++++++ .../service/wallet_account/service.nim | 26 ++++++- .../popups/keycard/states/KeycardInit.qml | 25 ++++-- ui/imports/utils/Constants.qml | 9 +++ 11 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 src/app_service/service/accounts/async_tasks.nim diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index 56e6d5ebd4..a76ce7cdf7 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -138,7 +138,8 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.nodeConfigurationService = node_configuration_service.newService(statusFoundation.fleetConfiguration, result.settingsService) result.keychainService = keychain_service.newService(statusFoundation.events) - result.accountsService = accounts_service.newService(statusFoundation.fleetConfiguration) + result.accountsService = accounts_service.newService(statusFoundation.events, statusFoundation.threadpool, + statusFoundation.fleetConfiguration) result.networkService = network_service.newService(statusFoundation.events, result.settingsService) result.contactsService = contacts_service.newService( statusFoundation.events, statusFoundation.threadpool, result.networkService, result.settingsService, diff --git a/src/app/modules/main/profile_section/keycard/controller.nim b/src/app/modules/main/profile_section/keycard/controller.nim index 56713d2cd6..55bb6b4c35 100644 --- a/src/app/modules/main/profile_section/keycard/controller.nim +++ b/src/app/modules/main/profile_section/keycard/controller.nim @@ -49,6 +49,8 @@ proc init*(self: Controller) = self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args): let args = KeycardActivityArgs(e) + if not args.success: + return self.delegate.onNewKeycardSet(args.keyPair) self.events.on(SIGNAL_KEYCARD_LOCKED) do(e: Args): diff --git a/src/app/modules/main/wallet_section/accounts/module.nim b/src/app/modules/main/wallet_section/accounts/module.nim index 8a318caa7a..d0401ef9ae 100644 --- a/src/app/modules/main/wallet_section/accounts/module.nim +++ b/src/app/modules/main/wallet_section/accounts/module.nim @@ -167,6 +167,9 @@ method load*(self: Module) = self.refreshWalletAccounts() self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args): + let args = KeycardActivityArgs(e) + if not args.success: + return self.refreshWalletAccounts() self.controller.init() diff --git a/src/app/modules/shared_modules/keycard_popup/controller.nim b/src/app/modules/shared_modules/keycard_popup/controller.nim index 5d474ae2ed..5a5ae6b606 100644 --- a/src/app/modules/shared_modules/keycard_popup/controller.nim +++ b/src/app/modules/shared_modules/keycard_popup/controller.nim @@ -51,6 +51,8 @@ type tmpUsePinFromBiometrics: bool tmpOfferToStoreUpdatedPinToKeychain: bool tmpKeycardUid: string + tmpAddingMigratedKeypairSuccess: bool + tmpConvertingProfileSuccess: bool proc newController*(delegate: io_interface.AccessInterface, uniqueIdentifier: string, @@ -78,6 +80,8 @@ proc newController*(delegate: io_interface.AccessInterface, result.tmpSeedPhraseLength = 0 result.tmpSelectedKeyPairIsProfile = false result.tmpUsePinFromBiometrics = false + result.tmpAddingMigratedKeypairSuccess = false + result.tmpConvertingProfileSuccess = false proc serviceApplicable[T](service: T): bool = if not service.isNil: @@ -141,6 +145,16 @@ proc init*(self: Controller) = self.delegate.onUserAuthenticated(args.password, args.pin) self.connectionIds.add(handlerId) + self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args): + let args = KeycardActivityArgs(e) + self.tmpAddingMigratedKeypairSuccess = args.success + self.delegate.onSecondaryActionClicked() + + self.events.on(SIGNAL_CONVERTING_PROFILE_KEYPAIR) do(e: Args): + let args = ResultArgs(e) + self.tmpConvertingProfileSuccess = args.success + self.delegate.onSecondaryActionClicked() + proc getKeycardData*(self: Controller): string = return self.delegate.getKeycardData() @@ -289,15 +303,18 @@ proc verifyPassword*(self: Controller, password: string): bool = return return self.accountsService.verifyPassword(password) -proc convertSelectedKeyPairToKeycardAccount*(self: Controller, password: string): bool = +proc convertSelectedKeyPairToKeycardAccount*(self: Controller, password: string) = if not serviceApplicable(self.accountsService): return let acc = self.accountsService.createAccountFromMnemonic(self.getSeedPhrase(), includeEncryption = true) singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NOT_NOW) - return self.accountsService.convertToKeycardAccount(self.tmpSelectedKeyPairDto.keyUid, + self.accountsService.convertToKeycardAccount(self.tmpSelectedKeyPairDto.keyUid, currentPassword = password, newPassword = acc.derivedAccounts.encryption.publicKey) +proc getConvertingProfileSuccess*(self: Controller): bool = + return self.tmpConvertingProfileSuccess + proc getLoggedInAccount*(self: Controller): AccountDto = if not serviceApplicable(self.accountsService): return @@ -429,8 +446,8 @@ proc terminateCurrentFlow*(self: Controller, lastStepInTheCurrentFlow: bool) = let (_, flowEvent) = self.getLastReceivedKeycardData() var data = SharedKeycarModuleFlowTerminatedArgs(uniqueIdentifier: self.uniqueIdentifier, lastStepInTheCurrentFlow: lastStepInTheCurrentFlow) - let exportedEncryptionPubKey = flowEvent.generatedWalletAccount.publicKey if lastStepInTheCurrentFlow: + let exportedEncryptionPubKey = flowEvent.generatedWalletAccount.publicKey data.password = if exportedEncryptionPubKey.len > 0: exportedEncryptionPubKey else: self.getPassword() data.pin = self.getPin() data.keyUid = flowEvent.keyUid @@ -452,13 +469,16 @@ proc getBalanceForAddress*(self: Controller, address: string): float64 = return return self.walletAccountService.fetchBalanceForAddress(address) -proc addMigratedKeyPair*(self: Controller, keyPair: KeyPairDto): bool = +proc addMigratedKeyPair*(self: Controller, keyPair: KeyPairDto) = if not serviceApplicable(self.walletAccountService): return if not serviceApplicable(self.accountsService): return let keystoreDir = self.accountsService.getKeyStoreDir() - return self.walletAccountService.addMigratedKeyPair(keyPair, keystoreDir) + self.walletAccountService.addMigratedKeyPairAsync(keyPair, keystoreDir) + +proc getAddingMigratedKeypairSuccess*(self: Controller): bool = + return self.tmpAddingMigratedKeypairSuccess proc getAllMigratedKeyPairs*(self: Controller): seq[KeyPairDto] = if not serviceApplicable(self.walletAccountService): diff --git a/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim index c99fbec1c2..b4106aa9f0 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim @@ -1,33 +1,36 @@ type MigratingKeyPairState* = ref object of State - migrationSuccess: bool + authenticationDone: bool + authenticationOk: bool + addingMigratedKeypairDone: bool + addingMigratedKeypairOk: bool + profileConversionDone: bool + profileConversionOk: bool proc newMigratingKeyPairState*(flowType: FlowType, backState: State): MigratingKeyPairState = result = MigratingKeyPairState() result.setup(flowType, StateType.MigratingKeyPair, backState) - result.migrationSuccess = false + result.authenticationDone = false + result.authenticationOk = false + result.addingMigratedKeypairDone = false + result.addingMigratedKeypairOk = false + result.profileConversionDone = false + result.profileConversionOk = false proc delete*(self: MigratingKeyPairState) = self.State.delete proc doMigration(self: MigratingKeyPairState, controller: Controller) = - if self.flowType == FlowType.SetupNewKeycard: - let password = controller.getPassword() - controller.setPassword("") - if controller.getSelectedKeyPairIsProfile(): - self.migrationSuccess = controller.verifyPassword(password) - if not self.migrationSuccess: - return - let selectedKeyPairDto = controller.getSelectedKeyPairDto() - self.migrationSuccess = controller.addMigratedKeyPair(selectedKeyPairDto) - if not self.migrationSuccess: - return - if controller.getSelectedKeyPairIsProfile(): - self.migrationSuccess = self.migrationSuccess and controller.convertSelectedKeyPairToKeycardAccount(password) - if not self.migrationSuccess: - return - controller.runStoreMetadataFlow(selectedKeyPairDto.keycardName, controller.getPin(), - controller.getSelectedKeyPairWalletPaths()) + let selectedKeyPairDto = controller.getSelectedKeyPairDto() + controller.addMigratedKeyPair(selectedKeyPairDto) + +proc doConversion(self: MigratingKeyPairState, controller: Controller) = + let password = controller.getPassword() + controller.convertSelectedKeyPairToKeycardAccount(password) + +proc runStoreMetadataFlow(self: MigratingKeyPairState, controller: Controller) = + let selectedKeyPairDto = controller.getSelectedKeyPairDto() + controller.runStoreMetadataFlow(selectedKeyPairDto.keycardName, controller.getPin(), controller.getSelectedKeyPairWalletPaths()) method executePrePrimaryStateCommand*(self: MigratingKeyPairState, controller: Controller) = if self.flowType == FlowType.SetupNewKeycard: @@ -37,13 +40,40 @@ method executePrePrimaryStateCommand*(self: MigratingKeyPairState, controller: C self.doMigration(controller) method executePreSecondaryStateCommand*(self: MigratingKeyPairState, controller: Controller) = + ## Secondary action is called after each async action during migration process. if self.flowType == FlowType.SetupNewKeycard: - self.doMigration(controller) + if controller.getSelectedKeyPairIsProfile(): + if not self.authenticationDone: + self.authenticationDone = true + let password = controller.getPassword() + self.authenticationOk = controller.verifyPassword(password) + if self.authenticationOk: + self.doMigration(controller) + return + if not self.addingMigratedKeypairDone: + self.addingMigratedKeypairDone = true + self.addingMigratedKeypairOk = controller.getAddingMigratedKeypairSuccess() + if self.addingMigratedKeypairOk: + self.doConversion(controller) + return + if not self.profileConversionDone: + self.profileConversionDone = true + self.profileConversionOk = controller.getConvertingProfileSuccess() + if self.profileConversionOk: + self.runStoreMetadataFlow(controller) + else: + if not self.addingMigratedKeypairDone: + self.addingMigratedKeypairDone = true + self.addingMigratedKeypairOk = controller.getAddingMigratedKeypairSuccess() + if self.addingMigratedKeypairOk: + self.runStoreMetadataFlow(controller) method getNextSecondaryState*(self: MigratingKeyPairState, controller: Controller): State = if self.flowType == FlowType.SetupNewKeycard: - if not self.migrationSuccess: - return createState(StateType.KeyPairMigrateFailure, self.flowType, nil) + if self.authenticationDone and not self.authenticationOk or + self.addingMigratedKeypairDone and not self.addingMigratedKeypairOk or + self.profileConversionDone and not self.profileConversionOk: + return createState(StateType.KeyPairMigrateFailure, self.flowType, nil) method resolveKeycardNextState*(self: MigratingKeyPairState, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = diff --git a/src/app_service/service/accounts/async_tasks.nim b/src/app_service/service/accounts/async_tasks.nim new file mode 100644 index 0000000000..b8d399b66c --- /dev/null +++ b/src/app_service/service/accounts/async_tasks.nim @@ -0,0 +1,21 @@ +################################################# +# Async convert profile keypair +################################################# + +type + ConvertToKeycardAccountTaskArg* = ref object of QObjectTaskArg + accountDataJson: JsonNode + settingsJson: JsonNode + hashedCurrentPassword: string + newPassword: string + keyStoreDir: string + +const convertToKeycardAccountTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[ConvertToKeycardAccountTaskArg](argEncoded) + try: + let response = status_account.convertToKeycardAccount(arg.keyStoreDir, arg.accountDataJson, arg.settingsJson, + arg.hashedCurrentPassword, arg.newPassword) + arg.finish(response) + except Exception as e: + error "error converting profile keypair: ", message = e.msg + arg.finish("") \ No newline at end of file diff --git a/src/app_service/service/accounts/service.nim b/src/app_service/service/accounts/service.nim index 6f88ba9e67..f77ba5585a 100644 --- a/src/app_service/service/accounts/service.nim +++ b/src/app_service/service/accounts/service.nim @@ -8,6 +8,9 @@ from ../keycard/service import KeycardEvent, KeyDetails import ../../../backend/general as status_general import ../../../backend/core as status_core +import ../../../app/core/eventemitter +import ../../../app/core/signals/types +import ../../../app/core/tasks/[qt, threadpool] import ../../../app/core/fleets/fleet_configuration import ../../common/[account_constants, network_constants, utils, string_utils] import ../../../constants as main_constants @@ -30,10 +33,18 @@ const KDF_ITERATIONS* {.intdefine.} = 256_000 # specific peer to set for testing messaging and mailserver functionality with squish. let TEST_PEER_ENR = getEnv("TEST_PEER_ENR").string +const SIGNAL_CONVERTING_PROFILE_KEYPAIR* = "convertingProfileKeypair" + +type ResultArgs* = ref object of Args + success*: bool + include utils +include async_tasks QtObject: type Service* = ref object of QObject + events: EventEmitter + threadpool: ThreadPool fleetConfiguration: FleetConfiguration generatedAccounts: seq[GeneratedAccountDto] accounts: seq[AccountDto] @@ -46,10 +57,11 @@ QtObject: proc delete*(self: Service) = self.QObject.delete - proc newService*(fleetConfiguration: FleetConfiguration): Service = - result = Service() + proc newService*(events: EventEmitter, threadpool: ThreadPool, fleetConfiguration: FleetConfiguration): Service = new(result, delete) result.QObject.setup + result.events = events + result.threadpool = threadpool result.fleetConfiguration = fleetConfiguration result.isFirstTimeAccountLogin = false result.keyStoreDir = main_constants.ROOTKEYSTOREDIR @@ -677,36 +689,48 @@ QtObject: error "error: ", procName="verifyAccountPassword", errName = e.name, errDesription = e.msg - proc convertToKeycardAccount*(self: Service, keyUid: string, currentPassword: string, newPassword: string): bool = + proc convertToKeycardAccount*(self: Service, keyUid: string, currentPassword: string, newPassword: string) = + var accountDataJson = %* { + "name": self.getLoggedInAccount().name, + "key-uid": keyUid + } + var settingsJson = %* { + "display-name": self.getLoggedInAccount().name + } + + self.addKeycardDetails(settingsJson, accountDataJson) + + if(accountDataJson.isNil or settingsJson.isNil): + let description = "at least one json object is not prepared well" + error "error: ", procName="convertToKeycardAccount", errDesription = description + return + + let hashedCurrentPassword = hashString(currentPassword) + let arg = ConvertToKeycardAccountTaskArg( + tptr: cast[ByteAddress](convertToKeycardAccountTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onConvertToKeycardAccount", + accountDataJson: accountDataJson, + settingsJson: settingsJson, + keyStoreDir: self.keyStoreDir, + hashedCurrentPassword: hashedCurrentPassword, + newPassword: newPassword + ) + self.threadpool.start(arg) + + proc onConvertToKeycardAccount*(self: Service, response: string) {.slot.} = + var result = false try: - var accountDataJson = %* { - "name": self.getLoggedInAccount().name, - "key-uid": keyUid - } - var settingsJson = %* { - "display-name": self.getLoggedInAccount().name - } - - self.addKeycardDetails(settingsJson, accountDataJson) - - if(accountDataJson.isNil or settingsJson.isNil): - let description = "at least one json object is not prepared well" - error "error: ", procName="convertToKeycardAccount", errDesription = description - return - - let hashedCurrentPassword = hashString(currentPassword) - let response = status_account.convertToKeycardAccount(self.keyStoreDir, accountDataJson, settingsJson, - hashedCurrentPassword, newPassword) - - if(response.result.contains("error")): - let errMsg = response.result["error"].getStr + let rpcResponse = Json.decode(response, RpcResponse[JsonNode]) + if(rpcResponse.result.contains("error")): + let errMsg = rpcResponse.result["error"].getStr if(errMsg.len == 0): - return true + result = true else: error "error: ", procName="convertToKeycardAccount", errDesription = errMsg - return false except Exception as e: - error "error: ", procName="convertToKeycardAccount", errName = e.name, errDesription = e.msg + error "error handilng migrated keypair response", errDesription=e.msg + self.events.emit(SIGNAL_CONVERTING_PROFILE_KEYPAIR, ResultArgs(success: result)) proc verifyPassword*(self: Service, password: string): bool = try: diff --git a/src/app_service/service/wallet_account/async_tasks.nim b/src/app_service/service/wallet_account/async_tasks.nim index 66f5a7fcce..183435317e 100644 --- a/src/app_service/service/wallet_account/async_tasks.nim +++ b/src/app_service/service/wallet_account/async_tasks.nim @@ -443,3 +443,26 @@ const prepareTokensTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = arg.finish(builtTokensPerAccount) +################################################# +# Async add migrated keypair +################################################# + +type + AddMigratedKeyPairTaskArg* = ref object of QObjectTaskArg + keyPair: KeyPairDto + keyStoreDir: string + +const addMigratedKeyPairTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AddMigratedKeyPairTaskArg](argEncoded) + try: + let response = backend.addMigratedKeyPair( + arg.keyPair.keycardUid, + arg.keyPair.keycardName, + arg.keyPair.keyUid, + arg.keyPair.accountsAddresses, + arg.keyStoreDir + ) + arg.finish(response) + except Exception as e: + error "error adding new keypair: ", message = e.msg + arg.finish("") \ No newline at end of file diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index 110709bad5..41c0972111 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -89,6 +89,7 @@ type TokensPerAccountArgs* = ref object of Args accountsTokens*: OrderedTable[string, seq[WalletTokenDto]] # [wallet address, list of tokens] type KeycardActivityArgs* = ref object of Args + success*: bool keycardUid*: string keycardNewUid*: string keycardNewName*: string @@ -113,6 +114,7 @@ QtObject: walletAccounts: OrderedTable[string, WalletAccountDto] timerStartTimeInSeconds: int64 priceCache: TimedCache + processedKeyPair: KeyPairDto # Forward declaration proc buildAllTokens(self: Service, calledFromTimerOrInit = false) @@ -547,6 +549,28 @@ QtObject: error "error: ", procName=procName, errDesription = errMsg return false + proc addMigratedKeyPairAsync*(self: Service, keyPair: KeyPairDto, keyStoreDir: string) = + let arg = AddMigratedKeyPairTaskArg( + tptr: cast[ByteAddress](addMigratedKeyPairTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onMigratedKeyPairAdded", + keyPair: keyPair, + keyStoreDir: keyStoreDir + ) + self.processedKeyPair = keyPair + self.threadpool.start(arg) + + proc onMigratedKeyPairAdded*(self: Service, response: string) {.slot.} = + var result = false + try: + let rpcResponse = Json.decode(response, RpcResponse[JsonNode]) + result = self.responseHasNoErrors("addMigratedKeyPair", rpcResponse) + except Exception as e: + error "error handilng migrated keypair response", errDesription=e.msg + let data = KeycardActivityArgs(success: result, keyPair: self.processedKeyPair) + self.processedKeyPair = KeyPairDto() + self.events.emit(SIGNAL_NEW_KEYCARD_SET, data) + proc addMigratedKeyPair*(self: Service, keyPair: KeyPairDto, keyStoreDir: string): bool = try: let response = backend.addMigratedKeyPair( @@ -558,7 +582,7 @@ QtObject: ) result = self.responseHasNoErrors("addMigratedKeyPair", response) if result: - self.events.emit(SIGNAL_NEW_KEYCARD_SET, KeycardActivityArgs(keyPair: keyPair)) + self.events.emit(SIGNAL_NEW_KEYCARD_SET, KeycardActivityArgs(success: true, keyPair: keyPair)) except Exception as e: error "error: ", procName="addMigratedKeyPair", errName = e.name, errDesription = e.msg diff --git a/ui/imports/shared/popups/keycard/states/KeycardInit.qml b/ui/imports/shared/popups/keycard/states/KeycardInit.qml index 1ce90dfbb4..d38b03b62c 100644 --- a/ui/imports/shared/popups/keycard/states/KeycardInit.qml +++ b/ui/imports/shared/popups/keycard/states/KeycardInit.qml @@ -26,6 +26,7 @@ Item { id: d readonly property bool hideKeyPair: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.hideKeyPair + readonly property bool continuousProcessingAnimation: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair } Timer { @@ -484,13 +485,25 @@ Item { } PropertyChanges { target: image - pattern: Constants.keycardAnimations.warning.pattern + pattern: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.pattern : + Constants.keycardAnimations.warning.pattern source: "" - startImgIndexForTheFirstLoop: Constants.keycardAnimations.warning.startImgIndexForTheFirstLoop - startImgIndexForOtherLoops: Constants.keycardAnimations.warning.startImgIndexForOtherLoops - endImgIndex: Constants.keycardAnimations.warning.endImgIndex - duration: Constants.keycardAnimations.warning.duration - loops: Constants.keycardAnimations.warning.loops + startImgIndexForTheFirstLoop: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.startImgIndexForTheFirstLoop : + Constants.keycardAnimations.warning.startImgIndexForTheFirstLoop + startImgIndexForOtherLoops: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.startImgIndexForOtherLoops : + Constants.keycardAnimations.warning.startImgIndexForOtherLoops + endImgIndex: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.endImgIndex : + Constants.keycardAnimations.warning.endImgIndex + duration: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.duration : + Constants.keycardAnimations.warning.duration + loops: d.continuousProcessingAnimation? + Constants.keycardAnimations.processing.loops : + Constants.keycardAnimations.warning.loops } PropertyChanges { target: message diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 26615e0f30..452404579a 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -196,6 +196,15 @@ QtObject { readonly property int loops: 1 } + readonly property QtObject processing: QtObject { + readonly property string pattern: "keycard/warning/img-%1" + readonly property int startImgIndexForTheFirstLoop: 0 + readonly property int startImgIndexForOtherLoops: 18 + readonly property int endImgIndex: 47 + readonly property int duration: 1500 + readonly property int loops: -1 + } + readonly property QtObject strongError: QtObject { readonly property string pattern: "keycard/strong_error/img-%1" readonly property int startImgIndexForTheFirstLoop: 0