fix: Device syncing

- Added local pairing signals
- Remove slash ending from keystorePath
- Implemented localPairingState. Fixed sync new device workflow. 
- Error message view design update 
- Moved local pairing status to devices service
- ConnectionString automatic validation
- Async inputConnectionString
- Added all installation properties to model. Minor renaming.
- Removed emoji and color customization
- Show display name, colorhash and color in device being synced
- Add timeout to pairing server
- Add device type
Fix `DeviceSyncingView` sizing. Fix `inputConnectionString` async task slot.
This commit is contained in:
Igor Sirotin 2023-03-14 15:52:16 +13:00 committed by Igor Sirotin
parent d2aa9e97bf
commit 33d38a4081
71 changed files with 2627 additions and 407 deletions

View File

@ -205,7 +205,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.privacyService = privacy_service.newService(statusFoundation.events, result.settingsService,
result.accountsService)
result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService, result.settingsService)
result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService)
result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService)
result.mailserversService = mailservers_service.newService(statusFoundation.events, statusFoundation.threadpool,
result.settingsService, result.nodeConfigurationService, statusFoundation.fleetConfiguration)
result.nodeService = node_service.newService(statusFoundation.events, result.settingsService, result.nodeConfigurationService)
@ -225,7 +225,8 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
result.accountsService,
result.generalService,
result.profileService,
result.keycardService
result.keycardService,
result.devicesService,
)
result.mainModule = main_module.newModule[AppController](
result,
@ -383,6 +384,7 @@ proc start*(self: AppController) =
self.keychainService.init()
self.generalService.init()
self.accountsService.init()
self.devicesService.init()
self.startupModule.load()
@ -405,7 +407,6 @@ proc load(self: AppController) =
self.activityCenterService.init()
self.savedAddressService.init()
self.aboutService.init()
self.devicesService.init()
self.ensService.init()
self.tokensService.init()
self.gifService.init()

View File

@ -8,7 +8,7 @@ import ../../../../app_service/service/bookmarks/dto/[bookmark]
import ../../../../app_service/service/community/dto/[community]
import ../../../../app_service/service/activity_center/dto/[notification]
import ../../../../app_service/service/contacts/dto/[contacts, status_update]
import ../../../../app_service/service/devices/dto/[device]
import ../../../../app_service/service/devices/dto/[installation]
import ../../../../app_service/service/settings/dto/[settings]
import ../../../../app_service/service/saved_address/[dto]
import ../../../../app_service/service/wallet_account/[key_pair_dto]
@ -19,7 +19,7 @@ type MessageSignal* = ref object of Signal
pinnedMessages*: seq[PinnedMessageUpdateDto]
chats*: seq[ChatDto]
contacts*: seq[ContactsDto]
devices*: seq[DeviceDto]
installations*: seq[InstallationDto]
emojiReactions*: seq[ReactionDto]
communities*: seq[CommunityDto]
communitiesSettings*: seq[CommunitySettingsDto]
@ -87,7 +87,7 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
if event["event"]{"installations"} != nil:
for jsonDevice in event["event"]["installations"]:
signal.devices.add(jsonDevice.toDeviceDto())
signal.installations.add(jsonDevice.toInstallationDto())
if event["event"]{"emojiReactions"} != nil:
for jsonReaction in event["event"]["emojiReactions"]:

View File

@ -0,0 +1,23 @@
import json, tables
import base
import ../../../../app_service/service/accounts/dto/accounts
type LocalPairingSignal* = ref object of Signal
eventType*: string
action*: int
error*: string
account*: AccountDto
proc fromEvent*(T: type LocalPairingSignal, event: JsonNode): LocalPairingSignal =
result = LocalPairingSignal()
let e = event["event"]
if e.contains("type"):
result.eventType = e["type"].getStr
if e.contains("action"):
result.action = e["action"].getInt
if e.contains("error"):
result.error = e["error"].getStr
if e.contains("data"):
result.account = e["data"].toAccountDto()

View File

@ -51,6 +51,7 @@ type SignalType* {.pure.} = enum
WakuBackedUpProfile = "waku.backedup.profile"
WakuBackedUpSettings = "waku.backedup.settings"
WakuBackedUpKeycards = "waku.backedup.keycards"
LocalPairing = "localPairing"
Unknown
proc event*(self:SignalType):string =

View File

@ -106,6 +106,8 @@ QtObject:
of SignalType.WakuBackedUpProfile: WakuBackedUpProfileSignal.fromEvent(jsonSignal)
of SignalType.WakuBackedUpSettings: WakuBackedUpSettingsSignal.fromEvent(jsonSignal)
of SignalType.WakuBackedUpKeycards: WakuBackedUpKeycardsSignal.fromEvent(jsonSignal)
# pairing
of SignalType.LocalPairing: LocalPairingSignal.fromEvent(jsonSignal)
else: Signal()
result.signalType = signalType

View File

@ -2,8 +2,8 @@
import ./remote_signals/[base, chronicles_logs, community, discovery_summary, envelope, expired, mailserver, messages,
peerstats, signal_type, stats, wallet, whisper_filter, keycard, update_available, status_updates, waku_backed_up_profile,
waku_backed_up_settings, waku_backed_up_keycards, waku_fetching_backup_progress]
waku_backed_up_settings, waku_backed_up_keycards, waku_fetching_backup_progress, pairing]
export base, chronicles_logs, community, discovery_summary, envelope, expired, mailserver, messages, peerstats,
signal_type, stats, wallet, whisper_filter, keycard, update_available, status_updates, waku_backed_up_profile,
waku_backed_up_settings, waku_backed_up_keycards, waku_fetching_backup_progress
waku_backed_up_settings, waku_backed_up_keycards, waku_fetching_backup_progress, pairing

View File

@ -5,6 +5,10 @@ import ../../../../core/eventemitter
import ../../../../../app_service/service/settings/service as settings_service
import ../../../../../app_service/service/devices/service as devices_service
import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
const UNIQUE_SYNCING_SECTION_ACCOUNTS_MODULE_AUTH_IDENTIFIER* = "SyncingSection-AccountsModule-Authentication"
logScope:
topics = "profile-section-devices-module-controller"
@ -37,8 +41,23 @@ proc init*(self: Controller) =
self.delegate.onDevicesLoadingErrored()
self.events.on(SIGNAL_UPDATE_DEVICE) do(e: Args):
var args = UpdateDeviceArgs(e)
self.delegate.updateOrAddDevice(args.deviceId, args.name, args.enabled)
var args = UpdateInstallationArgs(e)
self.delegate.updateOrAddDevice(args.installation)
self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_SYNCING_SECTION_ACCOUNTS_MODULE_AUTH_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
self.events.on(SIGNAL_LOCAL_PAIRING_EVENT) do(e: Args):
var args = LocalPairingEventArgs(e)
self.delegate.onLocalPairingEvent(args.eventType, args.action, args.error)
self.events.on(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e: Args):
var args = LocalPairingStatus(e)
self.delegate.onLocalPairingStatusUpdate(args)
proc getMyInstallationId*(self: Controller): string =
return self.settingsService.getInstallationId()
@ -46,6 +65,9 @@ proc getMyInstallationId*(self: Controller): string =
proc asyncLoadDevices*(self: Controller) =
self.devicesService.asyncLoadDevices()
proc getAllDevices*(self: Controller): seq[InstallationDto] =
return self.devicesService.getAllDevices()
proc setDeviceName*(self: Controller, name: string) =
self.devicesService.setDeviceName(name)
@ -60,3 +82,26 @@ proc enableDevice*(self: Controller, deviceId: string, enable: bool) =
self.devicesService.enable(deviceId)
else:
self.devicesService.disable(deviceId)
#
# Pairing status
#
proc authenticateUser*(self: Controller, keyUid: string) =
let data = SharedKeycarModuleAuthenticationArgs(
uniqueIdentifier: UNIQUE_SYNCING_SECTION_ACCOUNTS_MODULE_AUTH_IDENTIFIER,
keyUid: keyUid)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
#
# Backend actions
#
proc validateConnectionString*(self: Controller, connectionString: string): string =
return self.devicesService.validateConnectionString(connectionString)
proc getConnectionStringForBootstrappingAnotherDevice*(self: Controller, keyUid: string, password: string): string =
return self.devicesService.getConnectionStringForBootstrappingAnotherDevice(keyUid, password)
proc inputConnectionStringForBootstrapping*(self: Controller, connectionString: string): string =
return self.devicesService.inputConnectionStringForBootstrapping(connectionString)

View File

@ -1,5 +1,5 @@
import NimQml
import ../../../../../app_service/service/devices/service as devices_service
import ../../../../../app_service/service/devices/service
type
@ -17,7 +17,7 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method updateOrAddDevice*(self: AccessInterface, installationId: string, name: string, enabled: bool) {.base.} =
method updateOrAddDevice*(self: AccessInterface, installation: InstallationDto) {.base.} =
raise newException(ValueError, "No implementation available")
method viewDidLoad*(self: AccessInterface) {.base.} =
@ -26,7 +26,7 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method getMyInstallationId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method onDevicesLoaded*(self: AccessInterface, allDevices: seq[DeviceDto]) {.base.} =
method onDevicesLoaded*(self: AccessInterface, allDevices: seq[InstallationDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method onDevicesLoadingErrored*(self: AccessInterface) {.base.} =
@ -46,3 +46,24 @@ method advertise*(self: AccessInterface) {.base.} =
method enableDevice*(self: AccessInterface, installationId: string, enable: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method authenticateUser*(self: AccessInterface, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onUserAuthenticated*(self: AccessInterface, pin: string, password: string, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")
proc validateConnectionString*(self: AccessInterface, connectionString: string): string =
raise newException(ValueError, "No implementation available")
method getConnectionStringForBootstrappingAnotherDevice*(self: AccessInterface, keyUid: string, password: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method inputConnectionStringForBootstrapping*(self: AccessInterface, connectionString: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method onLocalPairingEvent*(self: AccessInterface, eventType: EventType, action: Action, error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onLocalPairingStatusUpdate*(self: AccessInterface, status: LocalPairingStatus) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,31 +1,33 @@
import ../../../../../app_service/service/devices/service as devices_service
import ../../../../../app_service/service/devices/dto/[installation]
type
Item* = ref object
installationId: string
name: string
enabled: bool
installation: InstallationDto
isCurrentDevice: bool
proc initItem*(installationId, name: string, enabled, isCurrentDevice: bool): Item =
proc initItem*(installation: InstallationDto, isCurrentDevice: bool): Item =
result = Item()
result.installationId = installationId
result.name = name
result.enabled = enabled
result.installation = installation
result.isCurrentDevice = isCurrentDevice
proc installationId*(self: Item): string =
self.installationId
proc installation*(self: Item): InstallationDto =
return self.installation
proc `installation=`*(self: Item, installation: InstallationDto) =
self.installation = installation
proc name*(self: Item): string =
self.name
self.installation.metadata.name
proc `name=`*(self: Item, value: string) =
self.name = value
self.installation.metadata.name = value
proc enabled*(self: Item): bool =
self.enabled
self.installation.enabled
proc `enabled=`*(self: Item, value: bool) =
self.enabled = value
self.installation.enabled = value
proc isCurrentDevice*(self: Item): bool =
self.isCurrentDevice

View File

@ -1,12 +1,18 @@
import NimQml, Tables, sequtils
import item
import ../../../../../app_service/service/devices/dto/[installation]
type
ModelRole {.pure.} = enum
Name = UserRole + 1,
InstallationId
IsCurrentDevice
InstallationId = UserRole + 1,
Identity
Version
Enabled
Timestamp
Name
DeviceType
FcmToken
IsCurrentDevice
QtObject:
type Model* = ref object of QAbstractListModel
@ -35,10 +41,15 @@ QtObject:
method roleNames(self: Model): Table[int, string] =
{
ModelRole.Name.int:"name",
ModelRole.InstallationId.int:"installationId",
ModelRole.Identity.int:"identity",
ModelRole.Version.int:"version",
ModelRole.Enabled.int:"enabled",
ModelRole.Timestamp.int:"timestamp",
ModelRole.Name.int:"name",
ModelRole.DeviceType.int:"deviceType",
ModelRole.FcmToken.int:"fcmToken",
ModelRole.IsCurrentDevice.int:"isCurrentDevice",
ModelRole.Enabled.int:"enabled"
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -50,14 +61,24 @@ QtObject:
let enumRole = role.ModelRole
case enumRole:
of ModelRole.Name:
result = newQVariant(item.name)
of ModelRole.InstallationId:
result = newQVariant(item.installationId)
result = newQVariant(item.installation.id)
of ModelRole.Identity:
result = newQVariant(item.installation.identity)
of ModelRole.Version:
result = newQVariant(item.installation.version)
of ModelRole.Enabled:
result = newQVariant(item.installation.enabled)
of ModelRole.Timestamp:
result = newQVariant(item.installation.timestamp)
of ModelRole.Name:
result = newQVariant(item.installation.metadata.name)
of ModelRole.DeviceType:
result = newQVariant(item.installation.metadata.deviceType)
of ModelRole.FcmToken:
result = newQVariant(item.installation.metadata.fcmToken)
of ModelRole.IsCurrentDevice:
result = newQVariant(item.isCurrentDevice)
of ModelRole.Enabled:
result = newQVariant(item.enabled)
proc addItems*(self: Model, items: seq[Item]) =
if(items.len == 0):
@ -84,23 +105,22 @@ QtObject:
proc findIndexByInstallationId(self: Model, installationId: string): int =
for i in 0..<self.items.len:
if installationId == self.items[i].installationId():
if installationId == self.items[i].installation.id:
return i
return -1
proc isItemWithInstallationIdAdded*(self: Model, installationId: string): bool =
return self.findIndexByInstallationId(installationId) != -1
proc updateItem*(self: Model, installationId: string, name: string, enabled: bool) =
var i = self.findIndexByInstallationId(installationId)
proc updateItem*(self: Model, installation: InstallationDto) =
var i = self.findIndexByInstallationId(installation.id)
if(i == -1):
return
let first = self.createIndex(i, 0, nil)
let last = self.createIndex(i, 0, nil)
self.items[i].name = name
self.items[i].enabled = enabled
self.dataChanged(first, last, @[ModelRole.Name.int, ModelRole.Enabled.int])
self.items[i].installation = installation
self.dataChanged(first, last, @[])
proc getIsDeviceSetup*(self: Model, installationId: string): bool =
return anyIt(self.items, it.installationId == installationId and it.name != "")
return anyIt(self.items, it.installation.id == installationId and it.name != "")

View File

@ -2,6 +2,7 @@ import NimQml, chronicles
import io_interface
import ../io_interface as delegate_interface
import view, controller, model, item
import logging
import ../../../../core/eventemitter
import ../../../../../app_service/service/settings/service as settings_service
@ -58,10 +59,10 @@ method onDevicesLoadingErrored*(self: Module) =
self.view.setDevicesLoading(false)
self.view.setDevicesLoadingError(true)
method onDevicesLoaded*(self: Module, allDevices: seq[DeviceDto]) =
method onDevicesLoaded*(self: Module, allDevices: seq[InstallationDto]) =
var items: seq[Item]
for d in allDevices:
let item = initItem(d.id, d.metadata.name, d.enabled, self.isMyDevice(d.id))
let item = initItem(d, self.isMyDevice(d.id))
items.add(item)
self.view.model().addItems(items)
self.view.setDevicesLoading(false)
@ -86,12 +87,34 @@ method syncAllDevices*(self: Module) =
method advertise*(self: Module) =
self.controller.advertise()
method enableDevice*(self: Module, deviceId: string, enable: bool) =
self.controller.enableDevice(deviceId, enable)
method enableDevice*(self: Module, installationId: string, enable: bool) =
self.controller.enableDevice(installationId, enable)
method updateOrAddDevice*(self: Module, deviceId: string, name: string, enabled: bool) =
if(self.view.model().isItemWithInstallationIdAdded(deviceId)):
self.view.model().updateItem(deviceId, name, enabled)
method updateOrAddDevice*(self: Module, installation: InstallationDto) =
if(self.view.model().isItemWithInstallationIdAdded(installation.id)):
self.view.model().updateItem(installation)
else:
let item = initItem(deviceId, name, enabled, self.isMyDevice(deviceId))
let item = initItem(installation, self.isMyDevice(installation.id))
self.view.model().addItem(item)
method authenticateUser*(self: Module, keyUid: string) =
self.controller.authenticateUser(keyUid)
method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid: string) =
self.view.emitUserAuthenticated(pin, password, keyUid)
proc validateConnectionString*(self: Module, connectionString: string): string =
return self.controller.validateConnectionString(connectionString)
method getConnectionStringForBootstrappingAnotherDevice*(self: Module, keyUid: string, password: string): string =
return self.controller.getConnectionStringForBootstrappingAnotherDevice(keyUid, password)
method inputConnectionStringForBootstrapping*(self: Module, connectionString: string): string =
return self.controller.inputConnectionStringForBootstrapping(connectionString)
method onLocalPairingEvent*(self: Module, eventType: EventType, action: Action, error: string) =
self.view.onLocalPairingEvent(eventType, action, error)
method onLocalPairingStatusUpdate*(self: Module, status: LocalPairingStatus) =
self.view.onLocalPairingStatusUpdate(status)

View File

@ -1,5 +1,7 @@
import NimQml
import io_interface, model
import ../../../../../app_service/service/devices/service
QtObject:
type
@ -9,11 +11,13 @@ QtObject:
modelVariant: QVariant
devicesLoading: bool
devicesLoadingError: bool
localPairingStatus: LocalPairingStatus
proc delete*(self: View) =
self.model.delete
self.modelVariant.delete
self.QObject.delete
self.localPairingStatus.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
@ -23,6 +27,7 @@ QtObject:
result.devicesLoadingError = false
result.model = newModel()
result.modelVariant = newQVariant(result.model)
result.localPairingStatus = newLocalPairingStatus()
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -82,3 +87,45 @@ QtObject:
proc enableDevice*(self: View, installationId: string, enable: bool) {.slot.} =
self.delegate.enableDevice(installationId, enable)
# LocalPairing status
proc localPairingStatusChanged*(self: View) {.signal.}
proc getLocalPairingState*(self: View): int {.slot.} =
return self.localPairingStatus.state.int
QtProperty[int] localPairingState:
read = getLocalPairingState
notify = localPairingStatusChanged
proc getLocalPairingError*(self: View): string {.slot.} =
return self.localPairingStatus.error
proc localPairingPairingErrorChanged*(self: View) {.signal.}
QtProperty[string] localPairingError:
read = getLocalPairingError
notify = localPairingStatusChanged
proc localPairingEvent(self: View, eventType: int, action: int, error: string) {.signal.}
proc onLocalPairingEvent*(self: View, eventType: EventType, action: Action, error: string) =
self.localPairingEvent(ord(eventType), ord(action), error)
proc onLocalPairingStatusUpdate*(self: View, status: LocalPairingStatus) =
self.localPairingStatus = status
self.localPairingStatusChanged()
# LocalPairing actions
proc userAuthenticated*(self: View, pin: string, password: string, keyUid: string) {.signal.}
proc emitUserAuthenticated*(self: View, pin: string, password: string, keyUid: string) =
self.userAuthenticated(pin, password, keyUid)
proc authenticateUser*(self: View, keyUid: string) {.slot.} =
self.delegate.authenticateUser(keyUid)
proc validateConnectionString*(self: View, connectionString: string): string {.slot.} =
return self.delegate.validateConnectionString(connectionString)
proc getConnectionStringForBootstrappingAnotherDevice*(self: View, keyUid: string, password: string): string {.slot.} =
return self.delegate.getConnectionStringForBootstrappingAnotherDevice(keyUid, password)
proc inputConnectionStringForBootstrapping*(self: View, connectionString: string): string {.slot.} =
return self.delegate.inputConnectionStringForBootstrapping(connectionString)

View File

@ -11,6 +11,7 @@ import ../../../app_service/service/accounts/service as accounts_service
import ../../../app_service/service/keychain/service as keychain_service
import ../../../app_service/service/profile/service as profile_service
import ../../../app_service/service/keycard/service as keycard_service
import ../../../app_service/service/devices/service as devices_service
import ../../../app_service/common/account_constants
import ../shared_modules/keycard_popup/io_interface as keycard_shared_module
@ -35,6 +36,7 @@ type
keychainService: keychain_service.Service
profileService: profile_service.Service
keycardService: keycard_service.Service
devicesService: devices_service.Service
connectionIds: seq[UUID]
keychainConnectionIds: seq[UUID]
tmpProfileImageDetails: ProfileImageDetails
@ -53,6 +55,7 @@ type
tmpCardMetadata: CardMetadata
tmpKeychainErrorOccurred: bool
tmpRecoverUsingSeedPhraseWhileLogin: bool
tmpConnectionString: string
proc newController*(delegate: io_interface.AccessInterface,
events: EventEmitter,
@ -60,7 +63,8 @@ proc newController*(delegate: io_interface.AccessInterface,
accountsService: accounts_service.Service,
keychainService: keychain_service.Service,
profileService: profile_service.Service,
keycardService: keycard_service.Service):
keycardService: keycard_service.Service,
devicesService: devices_service.Service):
Controller =
result = Controller()
result.delegate = delegate
@ -70,6 +74,7 @@ proc newController*(delegate: io_interface.AccessInterface,
result.keychainService = keychainService
result.profileService = profileService
result.keycardService = keycardService
result.devicesService = devicesService
result.tmpPinMatch = false
result.tmpSeedPhraseLength = 0
result.tmpKeychainErrorOccurred = false
@ -160,6 +165,11 @@ proc init*(self: Controller) =
self.delegate.onDisplayKeycardSharedModuleFlow()
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e: Args):
let args = LocalPairingStatus(e)
self.delegate.onLocalPairingStatusUpdate(args)
self.connectionIds.add(handlerId)
proc shouldStartWithOnboardingScreen*(self: Controller): bool =
return self.accountsService.openedAccounts().len == 0
@ -543,4 +553,16 @@ proc generateRandomPUK*(self: Controller): string =
proc storeMetadataForNewKeycardUser(self: Controller) =
## Stores metadata, default Status account only, to the keycard for a newly created keycard user.
let paths = @[account_constants.PATH_DEFAULT_WALLET]
self.runStoreMetadataFlow(self.getDisplayName(), self.getPin(), paths)
self.runStoreMetadataFlow(self.getDisplayName(), self.getPin(), paths)
proc getConnectionString*(self: Controller): string =
return self.tmpConnectionString
proc setConnectionString*(self: Controller, connectionString: string) =
self.tmpConnectionString = connectionString
proc validateLocalPairingConnectionString*(self: Controller, connectionString: string): string =
return self.devicesService.validateConnectionString(connectionString)
proc inputConnectionStringForBootstrapping*(self: Controller, connectionString: string): string =
return self.devicesService.inputConnectionStringForBootstrapping(connectionString)

View File

@ -79,6 +79,8 @@ type StateType* {.pure.} = enum
ProfileFetchingTimeout = "ProfileFetchingTimeout"
ProfileFetchingAnnouncement = "ProfileFetchingAnnouncement"
LostKeycardOptions = "LostKeycardOptions"
SyncDeviceWithSyncCode = "SyncDeviceWithSyncCode"
SyncDeviceResult = "SyncDeviceResult"
## This is the base class for all state we may have in onboarding/login flow.

View File

@ -87,6 +87,8 @@ include profile_fetching_timeout_state
include profile_fetching_announcement_state
include recover_old_user_state
include lost_keycard_options_state
include sync_device_with_sync_code
include sync_device_result
include state_factory_general_implementation
include state_factory_onboarding_implementation

View File

@ -121,7 +121,10 @@ proc createState*(stateToBeCreated: StateType, flowType: FlowType, backState: St
return newRecoverOldUserState(flowType, backState)
if stateToBeCreated == StateType.LostKeycardOptions:
return newLostKeycardOptionsState(flowType, backState)
if stateToBeCreated == StateType.SyncDeviceWithSyncCode:
return newSyncDeviceWithSyncCodeState(flowType, backState)
if stateToBeCreated == StateType.SyncDeviceResult:
return newSyncDeviceResultState(flowType, backState)
error "No implementation available for state ", state=stateToBeCreated
proc findBackStateWithTargetedStateType*(currentState: State, targetedStateType: StateType): State =

View File

@ -0,0 +1,12 @@
type
SyncDeviceResultState* = ref object of State
proc newSyncDeviceResultState*(flowType: FlowType, backState: State): SyncDeviceResultState =
result = SyncDeviceResultState()
result.setup(flowType, StateType.SyncDeviceResult, backState)
proc delete*(self: SyncDeviceResultState) =
self.State.delete
method executePrimaryCommand*(self: SyncDeviceResultState, controller: Controller) =
controller.login()

View File

@ -0,0 +1,16 @@
type
SyncDeviceWithSyncCodeState* = ref object of State
proc newSyncDeviceWithSyncCodeState*(flowType: FlowType, backState: State): SyncDeviceWithSyncCodeState =
result = SyncDeviceWithSyncCodeState()
result.setup(flowType, StateType.SyncDeviceWithSyncCode, backState)
proc delete*(self: SyncDeviceWithSyncCodeState) =
self.State.delete
method executePrimaryCommand*(self: SyncDeviceWithSyncCodeState, controller: Controller) =
let connectionString = controller.getConnectionString()
discard controller.inputConnectionStringForBootstrapping(connectionString)
method getNextPrimaryState*(self: SyncDeviceWithSyncCodeState, controller: Controller): State =
return createState(StateType.SyncDeviceResult, self.flowType, nil)

View File

@ -1,4 +1,4 @@
type
type
WelcomeStateNewUser* = ref object of State
proc newWelcomeStateNewUser*(flowType: FlowType, backState: State): WelcomeStateNewUser =

View File

@ -15,8 +15,7 @@ method executeBackCommand*(self: WelcomeStateOldUser, controller: Controller) =
controller.runLoginFlow()
method getNextPrimaryState*(self: WelcomeStateOldUser, controller: Controller): State =
# We will handle here a click on `Scan sync code`
discard
return createState(StateType.SyncDeviceWithSyncCode, FlowType.FirstRunOldUserSyncCode, self)
method getNextSecondaryState*(self: WelcomeStateOldUser, controller: Controller): State =
return createState(StateType.RecoverOldUser, self.flowType, self)

View File

@ -2,6 +2,7 @@ import NimQml
import ../../../app_service/service/accounts/service as accounts_service
import models/login_account_item as login_acc_item
from ../../../app_service/service/keycard/service import KeycardEvent, KeyDetails
from ../../../app_service/service/devices/dto/local_pairing_status import LocalPairingStatus
const UNIQUE_STARTUP_MODULE_IDENTIFIER* = "SartupModule"
@ -168,6 +169,18 @@ method checkFetchingStatusAndProceedWithAppLoading*(self: AccessInterface) {.bas
method startAppAfterDelay*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getConnectionString*(self: AccessInterface): string {.base} =
raise newException(ValueError, "No implementation available")
method setConnectionString*(self: AccessInterface, connectionString: string) {.base} =
raise newException(ValueError, "No implementation available")
method validateLocalPairingConnectionString*(self: AccessInterface, connectionString: 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

View File

@ -16,6 +16,7 @@ import ../../../app_service/service/accounts/service as accounts_service
import ../../../app_service/service/general/service as general_service
import ../../../app_service/service/profile/service as profile_service
import ../../../app_service/service/keycard/service as keycard_service
import ../../../app_service/service/devices/service as devices_service
import ../shared_modules/keycard_popup/module as keycard_shared_module
@ -45,6 +46,7 @@ type
keycardService: keycard_service.Service
accountsService: accounts_service.Service
keychainService: keychain_service.Service
devicesService: devices_service.Service
keycardSharedModule: keycard_shared_module.AccessInterface
proc newModule*[T](delegate: T,
@ -53,7 +55,8 @@ proc newModule*[T](delegate: T,
accountsService: accounts_service.Service,
generalService: general_service.Service,
profileService: profile_service.Service,
keycardService: keycard_service.Service):
keycardService: keycard_service.Service,
devicesService: devices_service.Service):
Module[T] =
result = Module[T]()
result.delegate = delegate
@ -61,10 +64,11 @@ proc newModule*[T](delegate: T,
result.keycardService = keycardService
result.accountsService = accountsService
result.keychainService = keychainService
result.devicesService = devicesService
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, generalService, accountsService, keychainService,
profileService, keycardService)
profileService, keycardService, devicesService)
method delete*[T](self: Module[T]) =
singletonInstance.engine.setRootContextProperty("startupModule", newQVariant())
@ -98,9 +102,9 @@ method load*[T](self: Module[T]) =
if(self.controller.shouldStartWithOnboardingScreen()):
if main_constants.IS_MACOS:
self.view.setCurrentStartupState(newNotificationState(FlowType.General, nil))
self.view.setCurrentStartupState(newNotificationState(state.FlowType.General, nil))
else:
self.view.setCurrentStartupState(newWelcomeState(FlowType.General, nil))
self.view.setCurrentStartupState(newWelcomeState(state.FlowType.General, nil))
else:
let openedAccounts = self.controller.getOpenedAccounts()
var items: seq[login_acc_item.Item]
@ -282,15 +286,15 @@ method setSelectedLoginAccount*[T](self: Module[T], item: login_acc_item.Item) =
self.controller.cancelCurrentFlow()
self.controller.setSelectedLoginAccount(item.getKeyUid(), item.getKeycardCreatedAccount())
if item.getKeycardCreatedAccount():
self.view.setCurrentStartupState(newLoginState(FlowType.AppLogin, nil))
self.view.setCurrentStartupState(newLoginState(state.FlowType.AppLogin, nil))
self.controller.runLoginFlow()
else:
let value = singletonInstance.localAccountSettings.getStoreToKeychainValue()
if value == LS_VALUE_STORE:
self.view.setCurrentStartupState(newLoginState(FlowType.AppLogin, nil))
self.view.setCurrentStartupState(newLoginState(state.FlowType.AppLogin, nil))
self.controller.tryToObtainDataFromKeychain()
else:
self.view.setCurrentStartupState(newLoginKeycardEnterPasswordState(FlowType.AppLogin, nil))
self.view.setCurrentStartupState(newLoginKeycardEnterPasswordState(state.FlowType.AppLogin, nil))
self.view.setSelectedLoginAccount(item)
method emitAccountLoginError*[T](self: Module[T], error: string) =
@ -323,8 +327,8 @@ method onFetchingFromWakuMessageReceived*[T](self: Module[T], section: string, t
if currStateObj.isNil:
error "cannot resolve current state for fetching data model update"
return
if currStateObj.flowType() != FlowType.FirstRunOldUserImportSeedPhrase and
currStateObj.flowType() != FlowType.FirstRunOldUserKeycardImport:
if currStateObj.flowType() != state.FlowType.FirstRunOldUserImportSeedPhrase and
currStateObj.flowType() != state.FlowType.FirstRunOldUserKeycardImport:
error "update fetching data model is out of context for the flow", flow=currStateObj.flowType()
return
if totalMessages > 0:
@ -365,7 +369,7 @@ proc logoutAndDisplayError[T](self: Module[T], error: string, errType: StartupEr
self.delegate.logout()
if self.controller.isSelectedLoginAccountKeycardAccount() and
errType == StartupErrorType.ConvertToRegularAccError:
self.view.setCurrentStartupState(newLoginState(FlowType.AppLogin, nil))
self.view.setCurrentStartupState(newLoginState(state.FlowType.AppLogin, nil))
self.controller.runLoginFlow()
self.moveToStartupState()
self.emitStartupError(error, errType)
@ -380,8 +384,8 @@ method onNodeLogin*[T](self: Module[T], error: string) =
quit() # quit the app
if error.len == 0:
if currStateObj.flowType() == FlowType.FirstRunOldUserImportSeedPhrase or
currStateObj.flowType() == FlowType.FirstRunOldUserKeycardImport:
if currStateObj.flowType() == state.FlowType.FirstRunOldUserImportSeedPhrase or
currStateObj.flowType() == state.FlowType.FirstRunOldUserKeycardImport:
self.prepareAndInitFetchingData()
self.controller.connectToFetchingFromWakuEvents()
self.delayStartingApp()
@ -389,7 +393,7 @@ method onNodeLogin*[T](self: Module[T], error: string) =
if err.len > 0:
self.logoutAndDisplayError(err, StartupErrorType.UnknownType)
return
elif currStateObj.flowType() == FlowType.LostKeycardConvertToRegularAccount:
elif currStateObj.flowType() == state.FlowType.LostKeycardConvertToRegularAccount:
let err = self.controller.convertToRegularAccount()
if err.len > 0:
self.logoutAndDisplayError(err, StartupErrorType.ConvertToRegularAccError)
@ -408,7 +412,7 @@ method onNodeLogin*[T](self: Module[T], error: string) =
self.delegate.finishAppLoading()
else:
self.moveToStartupState()
if currStateObj.flowType() == FlowType.AppLogin:
if currStateObj.flowType() == state.FlowType.AppLogin:
self.emitAccountLoginError(error)
else:
self.emitStartupError(error, StartupErrorType.SetupAccError)
@ -477,9 +481,9 @@ method onSharedKeycarModuleFlowTerminated*[T](self: Module[T], lastStepInTheCurr
if currStateObj.isNil:
error "cannot resolve current state for onboarding/login flow continuation"
return
if currStateObj.flowType() == FlowType.FirstRunNewUserNewKeycardKeys or
currStateObj.flowType() == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard or
currStateObj.flowType() == FlowType.LostKeycardReplacement:
if currStateObj.flowType() == state.FlowType.FirstRunNewUserNewKeycardKeys or
currStateObj.flowType() == state.FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard or
currStateObj.flowType() == state.FlowType.LostKeycardReplacement:
let newState = currStateObj.getBackState()
if newState.isNil:
error "cannot resolve new state for onboarding/login flow continuation after shared flow is terminated"
@ -498,3 +502,15 @@ method addToKeycardUidPairsToCheckForAChangeAfterLogin*[T](self: Module[T], oldK
method removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*[T](self: Module[T]) =
self.delegate.removeAllKeycardUidPairsForCheckingForAChangeAfterLogin()
method getConnectionString*[T](self: Module[T]): string =
return self.controller.getConnectionString()
method setConnectionString*[T](self: Module[T], connectionString: string) =
self.controller.setConnectionString(connectionString)
method validateLocalPairingConnectionString*[T](self: Module[T], connectionString: string): string =
return self.controller.validateLocalPairingConnectionString(connectionString)
method onLocalPairingStatusUpdate*[T](self: Module[T], status: LocalPairingStatus) =
self.view.onLocalPairingStatusUpdate(status)

View File

@ -8,6 +8,9 @@ import models/login_account_model as login_acc_model
import models/login_account_item as login_acc_item
import models/fetching_data_model as fetch_model
import ../../../app_service/service/devices/dto/local_pairing_status as local_pairing_status
import ../../../app_service/service/visual_identity/dto as visual_identity
type
AppState* {.pure.} = enum
StartupState = 0
@ -32,6 +35,7 @@ QtObject:
remainingAttempts: int
fetchingDataModel: fetch_model.Model
fetchingDataModelVariant: QVariant
localPairingStatus: LocalPairingStatus
proc delete*(self: View) =
self.currentStartupStateVariant.delete
@ -46,6 +50,7 @@ QtObject:
self.fetchingDataModel.delete
if not self.fetchingDataModelVariant.isNil:
self.fetchingDataModelVariant.delete
self.localPairingStatus.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
@ -63,6 +68,7 @@ QtObject:
result.loginAccountsModel = login_acc_model.newModel()
result.loginAccountsModelVariant = newQVariant(result.loginAccountsModel)
result.remainingAttempts = -1
result.localPairingStatus = newLocalPairingStatus()
signalConnect(result.currentStartupState, "backActionClicked()", result, "onBackActionClicked()", 2)
signalConnect(result.currentStartupState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2)
@ -295,4 +301,56 @@ QtObject:
if self.fetchingDataModelVariant.isNil:
self.fetchingDataModelVariant = newQVariant(self.fetchingDataModel)
self.fetchingDataModel.init(sections)
self.fetchingDataModelChanged()
self.fetchingDataModelChanged()
proc setConnectionString*(self: View, connectionString: string) {.slot.} =
self.delegate.setConnectionString(connectionString)
proc getConnectionString*(self: View): string {.slot.} =
return self.delegate.getConnectionString()
proc localPairingStatusChanged*(self: View) {.signal.}
proc getLocalPairingState*(self: View): int {.slot.} =
return self.localPairingStatus.state.int
QtProperty[int] localPairingState:
read = getLocalPairingState
notify = localPairingStatusChanged
proc getLocalPairingError*(self: View): string {.slot.} =
return self.localPairingStatus.error
QtProperty[string] localPairingError:
read = getLocalPairingError
notify = localPairingStatusChanged
proc getLocalPairingName*(self: View): string {.slot.} =
return self.localPairingStatus.account.name
QtProperty[string] localPairingName:
read = getLocalPairingName
notify = localPairingStatusChanged
proc getLocalPairingColorId*(self: View): int {.slot.} =
return self.localPairingStatus.account.colorId
QtProperty[int] localPairingColorId:
read = getLocalPairingColorId
notify = localPairingStatusChanged
proc getLocalPairingColorHash*(self: View): string {.slot.} =
return self.localPairingStatus.account.colorHash.toJson()
QtProperty[string] localPairingColorHash:
read = getLocalPairingColorHash
notify = localPairingStatusChanged
proc getLocalPairingImage*(self: View): string {.slot.} =
if self.localPairingStatus.account.images.len != 0:
return self.localPairingStatus.account.images[0].uri
QtProperty[string] localPairingImage:
read = getLocalPairingImage
notify = localPairingStatusChanged
proc onLocalPairingStatusUpdate*(self: View, status: LocalPairingStatus) =
self.localPairingStatus = status
self.localPairingStatusChanged()
proc validateLocalPairingConnectionString*(self: View, connectionString: string): string {.slot.} =
return self.delegate.validateLocalPairingConnectionString(connectionString)

View File

@ -1,8 +1,20 @@
include ../../common/json_utils
include ../../../app/core/tasks/common
type
AsyncLoadDevicesTaskArg = ref object of QObjectTaskArg
type
AsyncInputConnectionStringArg = ref object of QObjectTaskArg
connectionString: string
configJSON: string
const asyncLoadDevicesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncLoadDevicesTaskArg](argEncoded)
let response = status_installations.getOurInstallations()
arg.finish(response)
const asyncInputConnectionStringTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncInputConnectionStringArg](argEncoded)
let response = status_go.inputConnectionStringForBootstrapping(arg.connectionString, arg.configJSON)
arg.finish(response)

View File

@ -4,30 +4,37 @@ import json
include ../../../common/[json_utils]
type Metadata* = object
# NOTE: DeviceType equeals to:
# - on Desktop (`hostOS` from system.nim):
# "windows", "macosx", "linux", "netbsd", "freebsd",
# "openbsd", "solaris", "aix", "haiku", "standalone".
# - on Mobile (from platform.cljs):
# "android", "ios"
type InstallationMetadata* = object
name*: string
deviceType*: string
fcmToken*: string
type DeviceDto* = object
type InstallationDto* = object
id*: string
identity*: string
version*: int
enabled*: bool
timestamp*: int64
metadata*: Metadata
metadata*: InstallationMetadata
proc isEmpty*(self: Metadata): bool =
proc isEmpty*(self: InstallationMetadata): bool =
return self.name.len == 0
proc toMetadata(jsonObj: JsonNode): Metadata =
result = Metadata()
proc toMetadata(jsonObj: JsonNode): InstallationMetadata =
result = InstallationMetadata()
discard jsonObj.getProp("name", result.name)
discard jsonObj.getProp("deviceType", result.deviceType)
discard jsonObj.getProp("fcmToken", result.fcmToken)
proc toDeviceDto*(jsonObj: JsonNode): DeviceDto =
result = DeviceDto()
proc toInstallationDto*(jsonObj: JsonNode): InstallationDto =
result = InstallationDto()
discard jsonObj.getProp("id", result.id)
discard jsonObj.getProp("identity", result.identity)
discard jsonObj.getProp("version", result.version)

View File

@ -0,0 +1,57 @@
import ../../../../app/core/eventemitter
import ../../accounts/dto/accounts
type
EventType* {.pure.} = enum
EventUnknown = -1,
EventConnectionError = 0,
EventConnectionSuccess = 1,
EventTransferError = 2,
EventTransferSuccess = 3,
EventReceivedAccount = 4,
EventProcessSuccess = 5,
EventProcessError = 6
type
Action* {.pure.} = enum
ActionUnknown = 0
ActionConnect = 1,
ActionPairingAccount = 2,
ActionSyncDevice = 3,
type
LocalPairingEventArgs* = ref object of Args
eventType*: EventType
action*: Action
error*: string
account*: AccountDTO
proc parse*(self: string): EventType =
case self:
of "connection-error":
return EventConnectionError
of "connection-success":
return EventConnectionSuccess
of "transfer-error":
return EventTransferError
of "transfer-success":
return EventTransferSuccess
of "process-success":
return EventProcessSuccess
of "process-error":
return EventProcessError
of "received-account":
return EventReceivedAccount
else:
return EventUnknown
proc parse*(self: int): Action =
case self:
of 1:
return ActionConnect
of 2:
return ActionPairingAccount
of 3:
return ActionSyncDevice
else:
return ActionUnknown

View File

@ -0,0 +1,66 @@
import ../../../../app/core/eventemitter
import ../../accounts/dto/accounts
import local_pairing_event
type
LocalPairingState* {.pure.} = enum
Idle = 0
WaitingForConnection
Transferring
Error
Finished
type
LocalPairingMode* {.pure.} = enum
Idle = 0
BootstrapingOtherDevice
BootstrapingThisDevice
type
LocalPairingStatus* = ref object of Args
mode*: LocalPairingMode
state*: LocalPairingState
account*: AccountDTO
error*: string
proc reset*(self: LocalPairingStatus) =
self.mode = LocalPairingMode.Idle
self.state = LocalPairingState.Idle
self.error = ""
proc setup(self: LocalPairingStatus) =
self.reset()
proc delete*(self: LocalPairingStatus) =
discard
proc newLocalPairingStatus*(): LocalPairingStatus =
new(result, delete)
result.setup()
proc update*(self: LocalPairingStatus, eventType: EventType, action: Action, account: AccountDTO, error: string) =
case eventType:
of EventConnectionSuccess:
self.state = LocalPairingState.WaitingForConnection
of EventTransferSuccess:
self.state = case self.mode:
of LocalPairingMode.BootstrapingOtherDevice:
LocalPairingState.Finished # For servers, `transfer` is last event
of LocalPairingMode.BootstrapingThisDevice:
LocalPairingState.Transferring # For clients, `process` is last event
else:
LocalPairingState.Idle
of EventProcessSuccess:
self.state = LocalPairingState.Finished
of EventConnectionError:
self.state = LocalPairingState.Error
of EventTransferError:
self.state = LocalPairingState.Error
of EventProcessError:
self.state = LocalPairingState.Error
of EventReceivedAccount:
self.account = account
else:
discard
self.error = error

View File

@ -1,65 +1,92 @@
import NimQml, json, sequtils, system, chronicles
import NimQml, json, sequtils, system, chronicles, uuids
import std/os
import ./dto/installation as Installation_dto
import ./dto/local_pairing_event
import ./dto/local_pairing_status
import ../../../app/core/[main]
import ../../../app/core/tasks/[qt, threadpool]
import ./dto/device as device_dto
import ../settings/service as settings_service
import ../accounts/service as accounts_service
import ../../../app/global/global_singleton
import ../../../app/core/[main]
import ../../../app/core/signals/types
import ../../../app/core/eventemitter
import ../../../app/core/tasks/[qt, threadpool]
import ../../../backend/installations as status_installations
import ../../common/utils as utils
import ../../../constants as main_constants
import status_go
export Installation_dto
export local_pairing_event
export local_pairing_status
export device_dto
include async_tasks
logScope:
topics = "devices-service"
type
UpdateDeviceArgs* = ref object of Args
deviceId*: string
name*: string
enabled*: bool
UpdateInstallationArgs* = ref object of Args
installation*: InstallationDto
type
DevicesArg* = ref object of Args
devices*: seq[DeviceDto]
devices*: seq[InstallationDto]
# Signals which may be emitted by this service:
const SIGNAL_UPDATE_DEVICE* = "updateDevice"
const SIGNAL_DEVICES_LOADED* = "devicesLoaded"
const SIGNAL_ERROR_LOADING_DEVICES* = "devicesErrorLoading"
const SIGNAL_LOCAL_PAIRING_EVENT* = "localPairingEvent"
const SIGNAL_LOCAL_PAIRING_STATUS_UPDATE* = "localPairingStatusUpdate"
QtObject:
type Service* = ref object of QObject
events: EventEmitter
threadpool: ThreadPool
settingsService: settings_service.Service
accountsService: accounts_service.Service
localPairingStatus: LocalPairingStatus
proc delete*(self: Service) =
self.QObject.delete
self.localPairingStatus.delete
proc newService*(
events: EventEmitter,
threadpool: ThreadPool,
settingsService: settings_service.Service,
): Service =
proc newService*(events: EventEmitter,
threadpool: ThreadPool,
settingsService: settings_service.Service,
accountsService: accounts_service.Service): Service =
new(result, delete)
result.QObject.setup
result.events = events
result.settingsService = settingsService
result.threadpool = threadpool
result.settingsService = settingsService
result.accountsService = accountsService
result.localPairingStatus = newLocalPairingStatus()
proc doConnect(self: Service) =
self.events.on(SignalType.Message.event) do(e:Args):
var receivedData = MessageSignal(e)
if(receivedData.devices.len > 0):
for d in receivedData.devices:
let data = UpdateDeviceArgs(
deviceId: d.id,
name: d.metadata.name,
enabled: d.enabled)
self.events.emit(SIGNAL_UPDATE_DEVICE, data)
let receivedData = MessageSignal(e)
for dto in receivedData.installations:
let data = UpdateInstallationArgs(
installation: dto)
self.events.emit(SIGNAL_UPDATE_DEVICE, data)
self.events.on(SignalType.LocalPairing.event) do(e:Args):
let signalData = LocalPairingSignal(e)
let data = LocalPairingEventArgs(
eventType: signalData.eventType.parse(),
action: signalData.action.parse(),
account: signalData.account,
error: signalData.error)
self.events.emit(SIGNAL_LOCAL_PAIRING_EVENT, data)
self.localPairingStatus.update(data.eventType, data.action, data.account, data.error)
self.events.emit(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE, self.localPairingStatus)
proc init*(self: Service) =
self.doConnect()
@ -75,17 +102,17 @@ QtObject:
proc asyncDevicesLoaded*(self: Service, response: string) {.slot.} =
try:
let rpcResponse = Json.decode(response, RpcResponse[JsonNode])
let installations = map(rpcResponse.result.getElems(), proc(x: JsonNode): DeviceDto = x.toDeviceDto())
let installations = map(rpcResponse.result.getElems(), proc(x: JsonNode): InstallationDto = x.toInstallationDto())
self.events.emit(SIGNAL_DEVICES_LOADED, DevicesArg(devices: installations))
except Exception as e:
let errDesription = e.msg
error "error loading devices: ", errDesription
self.events.emit(SIGNAL_ERROR_LOADING_DEVICES, Args())
proc getAllDevices*(self: Service): seq[DeviceDto] =
proc getAllDevices*(self: Service): seq[InstallationDto] =
try:
let response = status_installations.getOurInstallations()
return map(response.result.getElems(), proc(x: JsonNode): DeviceDto = x.toDeviceDto())
return map(response.result.getElems(), proc(x: JsonNode): InstallationDto = x.toInstallationDto())
except Exception as e:
let errDesription = e.msg
error "error: ", errDesription
@ -112,3 +139,46 @@ QtObject:
proc disable*(self: Service, deviceId: string) =
# Once we get more info from `status-go` we may emit success/failed signal from here.
discard status_installations.disableInstallation(deviceId)
#
# Local Pairing
#
proc inputConnectionStringForBootstrappingFinished(self: Service, result: string) =
discard
proc validateConnectionString*(self: Service, connectionString: string): string =
return status_go.validateConnectionString(connectionString)
proc getConnectionStringForBootstrappingAnotherDevice*(self: Service, keyUid: string, password: string): string =
let configJSON = %* {
"keyUID": keyUid,
"keystorePath": joinPath(main_constants.ROOTKEYSTOREDIR, keyUid),
"deviceType": hostOs,
"password": utils.hashPassword(password),
"timeout": 5 * 60 * 1000,
}
self.localPairingStatus.mode = LocalPairingMode.BootstrapingOtherDevice
return status_go.getConnectionStringForBootstrappingAnotherDevice($configJSON)
proc inputConnectionStringForBootstrapping*(self: Service, connectionString: string): string =
let installationId = $genUUID()
let nodeConfigJson = self.accountsService.getDefaultNodeConfig(installationId)
let configJSON = %* {
"keystorePath": main_constants.ROOTKEYSTOREDIR,
"nodeConfig": nodeConfigJson,
"deviceType": hostOs,
"RootDataDir": main_constants.STATUSGODIR
}
self.localPairingStatus.mode = LocalPairingMode.BootstrapingThisDevice
let arg = AsyncInputConnectionStringArg(
tptr: cast[ByteAddress](asyncInputConnectionStringTask),
vptr: cast[ByteAddress](self.vptr),
slot: "inputConnectionStringForBootstrappingFinished",
connectionString: connectionString,
configJSON: $configJSON
)
self.threadpool.start(arg)

View File

@ -16,5 +16,11 @@ proc toColorHashDto*(jsonObj: JsonNode): ColorHashDto =
)
return
proc toJson*(self: ColorHashDto): string =
let json = newJArray()
for segment in self:
json.add(%* {"segmentLength": segment.len, "colorId": segment.colorIdx})
return $json
proc toColorId*(jsonObj: JsonNode): int =
return jsonObj.getInt()

View File

@ -48,8 +48,5 @@ proc getEmojiHashAsJson*(publicKey: string): string =
return $$emojiHashOf(publicKey)
proc getColorHashAsJson*(publicKey: string): string =
let colorHash = colorHashOf(publicKey)
let json = newJArray()
for segment in colorHash:
json.add(%* {"segmentLength": segment.len, "colorId": segment.colorIdx})
return $json
return colorHashOf(publicKey).toJson()

View File

@ -138,8 +138,8 @@ Loader {
LoadingComponent {
anchors.centerIn: parent
radius: width/2
height: root.asset.height
width: root.asset.width
height: root.asset.isImage ? root.asset.height : root.asset.bgHeight
width: root.asset.isImage ? root.asset.width : root.asset.bgWidth
}
}

View File

@ -0,0 +1,81 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Utils 0.1
StatusListItem {
id: root
property string deviceName: ""
property string deviceType: ""
property bool isCurrentDevice: false
property int timestamp: 0
signal itemClicked
signal setupSyncingButtonClicked
title: root.deviceName || qsTr("No device name")
asset.name: Utils.deviceIcon(root.deviceType)
asset.bgColor: Theme.palette.primaryColor3
asset.color: Theme.palette.primaryColor1
asset.isLetterIdenticon: false
subTitle: {
if (root.isCurrentDevice)
return qsTr("This device");
if (d.secondsFromSync <= 120)
return qsTr("Online now");
if (d.minutesFromSync <= 60)
return qsTr("Online %n minutes(s) ago", "", d.minutesFromSync);
if (d.daysFromSync == 0)
return qsTr("Last seen earlier today");
if (d.daysFromSync == 1)
return qsTr("Last online yesterday");
if (d.daysFromSync <= 6)
return qsTr("Last online [%1]").arg(Qt.locale().dayName[d.lastSyncDate.getDay()]);
return qsTr("Last online %1").arg(LocaleUtils.formatDate(lastSyncDate))
}
QtObject {
id: d
readonly property var lastSyncDate: new Date(root.timestamp)
readonly property int millisecondsFromSync: lastSyncDate - Date.now()
readonly property int secondsFromSync: millisecondsFromSync / 1000
readonly property int minutesFromSync: secondsFromSync / 60
readonly property int daysFromSync: new Date().getDay() - lastSyncDate.getDay()
}
components: [
StatusButton {
anchors.verticalCenter: parent.verticalCenter
visible: root.enabled && !root.isCurrentDevice
text: qsTr("Setup syncing")
size: StatusBaseButton.Size.Small
onClicked: {
root.setupSyncingButtonClicked()
}
},
StatusIcon {
anchors.verticalCenter: parent.verticalCenter
visible: root.enabled
icon: "chevron-down"
rotation: 270
color: Theme.palette.baseColor1
}
]
}

View File

@ -50,3 +50,4 @@ StatusChartPanel 0.1 StatusChartPanel.qml
StatusStepper 0.1 StatusStepper.qml
LoadingComponent 0.1 LoadingComponent.qml
StatusQrCodeScanner 0.1 StatusQrCodeScanner.qml
StatusSyncDeviceDelegate 0.1 StatusSyncDeviceDelegate.qml

View File

@ -262,7 +262,7 @@ Item {
return
}
statusBaseInput.valid = true
let valid = true
const rawText = statusBaseInput.edit.getText(0, statusBaseInput.edit.length)
if (validators.length) {
for (let idx in validators) {
@ -270,7 +270,7 @@ Item {
let result = validator.validate(rawText)
if (typeof result === "boolean" && result) {
statusBaseInput.valid = statusBaseInput.valid && true
valid = valid && true
delete errors[validator.name]
} else {
if (!errors) {
@ -287,8 +287,7 @@ Item {
errors = errors
result.errorMessage = validator.errorMessage
statusBaseInput.valid = statusBaseInput.valid && false
valid = false
}
}
if (errors){
@ -313,6 +312,8 @@ Item {
_previousText = text
}
statusBaseInput.valid = valid
if (asyncValidators.length && !Object.values(errors).length) {
for (let idx in asyncValidators) {
let asyncValidator = asyncValidators[idx]

View File

@ -155,13 +155,6 @@ ThemePalette {
property color secondaryBackgroundColor: "#414141"
}
statusSwitchTab: QtObject {
property color buttonBackgroundColor: primaryColor1
property color barBackgroundColor: primaryColor3
property color selectedTextColor: white
property color textColor: primaryColor1
}
statusSelect: QtObject {
property color menuItemBackgroundColor: baseColor2
property color menuItemHoverBackgroundColor: directColor7

View File

@ -153,13 +153,6 @@ ThemePalette {
property color secondaryBackgroundColor: "#E2E6E8"
}
statusSwitchTab: QtObject {
property color buttonBackgroundColor: primaryColor1
property color barBackgroundColor: primaryColor3
property color selectedTextColor: white
property color textColor: primaryColor1
}
statusSelect: QtObject {
property color menuItemBackgroundColor: white
property color menuItemHoverBackgroundColor: baseColor2

View File

@ -241,11 +241,11 @@ QtObject {
property color secondaryBackgroundColor
}
property QtObject statusSwitchTab: QtObject {
property color buttonBackgroundColor
property color barBackgroundColor
property color selectedTextColor
property color textColor
readonly property QtObject statusSwitchTab: QtObject {
property color buttonBackgroundColor: primaryColor1
property color barBackgroundColor: primaryColor3
property color selectedTextColor: indirectColor1
property color textColor: primaryColor1
}
property QtObject statusSelect: QtObject {

View File

@ -266,6 +266,11 @@ QtObject {
function encodeUtf8(str){
return unescape(encodeURIComponent(str));
}
function deviceIcon(deviceType) {
const isMobileDevice = deviceType === "ios" || deviceType === "android"
return isMobileDevice ? "mobile" : "desktop"
}
}

View File

@ -12,6 +12,7 @@ import shared.popups.keycard 1.0
import "controls"
import "views"
import "stores"
import "../Profile/stores"
OnboardingBasePage {
id: root
@ -132,6 +133,12 @@ OnboardingBasePage {
case Constants.startupState.profileFetchingSuccess:
case Constants.startupState.profileFetchingTimeout:
return fetchingDataViewComponent
case Constants.startupState.syncDeviceWithSyncCode:
return syncDeviceViewComponent
case Constants.startupState.syncDeviceResult:
return syncDeviceResultComponent
}
return undefined
@ -318,6 +325,20 @@ following the \"Add existing Status user\" flow, using your seed phrase.")
}
}
Component {
id: syncDeviceViewComponent
SyncCodeView {
startupStore: root.startupStore
}
}
Component {
id: syncDeviceResultComponent
SyncDeviceResult {
startupStore: root.startupStore
}
}
Loader {
id: keycardPopup
active: false

View File

@ -11,6 +11,13 @@ QtObject {
property var fetchingDataModel: startupModuleInst ? startupModuleInst.fetchingDataModel
: null
readonly property int localPairingState: startupModuleInst ? startupModuleInst.localPairingState : -1
readonly property string localPairingError: startupModuleInst ? startupModuleInst.localPairingError : ""
readonly property string localPairingName: startupModuleInst ? startupModuleInst.localPairingName : ""
readonly property string localPairingImage: startupModuleInst ? startupModuleInst.localPairingImage : ""
readonly property int localPairingColorId: startupModuleInst ? startupModuleInst.localPairingColorId : 0
readonly property string localPairingColorHash: startupModuleInst ? startupModuleInst.localPairingColorHash : ""
function backAction() {
root.currentStartupState.backAction()
}
@ -103,4 +110,12 @@ QtObject {
function getSeedPhrase() {
return root.startupModuleInst.getSeedPhrase()
}
function validateLocalPairingConnectionString(connectionString) {
return root.startupModuleInst.validateLocalPairingConnectionString(connectionString)
}
function setConnectionString(connectionString) {
root.startupModuleInst.setConnectionString(connectionString)
}
}

View File

@ -305,7 +305,7 @@ Item {
}
PropertyChanges {
target: txtTitle
text: qsTr("Sync to other device")
text: qsTr("Sign in by syncing")
}
PropertyChanges {
target: txtDesc
@ -314,8 +314,7 @@ Item {
}
PropertyChanges {
target: button1
text: qsTr("Scan sync code")
enabled: false // TODO: we don't have the sync flow developed yet
text: qsTr("Scan or enter a sync code")
}
PropertyChanges {
target: button2

View File

@ -0,0 +1,164 @@
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.Universal 2.12
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.controls 1.0
import "../popups"
import "../controls"
import "../stores"
import "sync"
import "../../Profile/stores"
import utils 1.0
Item {
id: root
property StartupStore startupStore
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
QtObject {
id: d
property string connectionString
function validateConnectionString(connectionString) {
const result = root.startupStore.validateLocalPairingConnectionString(connectionString)
return result === ""
}
}
Timer {
id: nextStateDelay
interval: 1000
repeat: false
onTriggered: {
root.startupStore.setConnectionString(d.connectionString)
root.startupStore.doPrimaryAction()
}
}
Column {
id: layout
anchors.centerIn: parent
width: 400
spacing: 24
StatusBaseText {
id: headlineText
width: parent.width
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 22
font.weight: Font.Bold
color: Theme.palette.directColor1
text: qsTr("Sign in by syncing")
}
StatusSwitchTabBar {
id: switchTabBar
anchors.horizontalCenter: parent.horizontalCenter
currentIndex: 0
StatusSwitchTabButton {
text: qsTr("Scan QR code")
}
StatusSwitchTabButton {
text: qsTr("Enter sync code")
}
}
StackLayout {
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.max(mobileSync.implicitWidth, desktopSync.implicitWidth)
implicitHeight: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight)
currentIndex: switchTabBar.currentIndex
clip: true
// StackLayout doesn't support alignment, so we create an `Item` wrappers
Item {
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: mobileSync.implicitWidth
implicitHeight: mobileSync.implicitHeight
SyncDeviceFromMobile {
id: mobileSync
anchors {
verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
}
onConnectionStringFound: {
d.processConnectionString(connectionString)
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: desktopSync.implicitWidth
implicitHeight: desktopSync.implicitHeight
SyncDeviceFromDesktop {
id: desktopSync
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
input.readOnly: nextStateDelay.running
input.validators: [
StatusValidator {
name: "asyncConnectionString"
errorMessage: qsTr("This does not look like a sync code")
validate: (value) => {
return d.validateConnectionString(value)
}
}
]
input.onValidChanged: {
if (!input.valid) {
d.connectionString = ""
return
}
d.connectionString = desktopSync.input.text
nextStateDelay.start()
}
}
}
}
StatusFlatButton {
text: qsTr("How to get a sync code")
anchors.horizontalCenter: parent.horizontalCenter
onClicked: {
instructionsPopup.open()
}
}
}
GetSyncCodeInstructionsPopup {
id: instructionsPopup
}
}

View File

@ -0,0 +1,73 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import shared.views 1.0
import utils 1.0
import "../stores"
import "../../Profile/stores"
Item {
id: root
property StartupStore startupStore
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
QtObject {
id: d
readonly property bool finished: startupStore.localPairingState === Constants.LocalPairingState.Finished
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: 48
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 22
font.weight: Font.Bold
color: Theme.palette.directColor1
text: qsTr("Sign in by syncing")
}
DeviceSyncingView {
Layout.alignment: Qt.AlignHCenter
localPairingState: startupStore.localPairingState
localPairingError: startupStore.localPairingError
userDisplayName: startupStore.localPairingName
userImage: startupStore.localPairingImage
userColorId: startupStore.localPairingColorId
userColorHash: startupStore.localPairingColorHash
}
StatusButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Sign in")
opacity: d.finished ? 1 : 0
enabled: d.finished
onClicked: {
// NOTE: Current status-go implementation automatically signs in
// So we don't actually ever use this button.
// I leave this code here for further implementaion by design.
//const keyUid = "TODO: Get keyUid somehow"
//root.startupStore.setSelectedLoginAccountByKeyUid(keyUid)
}
Behavior on opacity {
NumberAnimation { duration: 250 }
}
}
}
}

View File

@ -0,0 +1,16 @@
import QtQuick 2.14
import StatusQ.Controls 0.1
import shared.controls 1.0
Column {
id: root
property alias input: codeInput
StatusSyncCodeInput {
id: codeInput
implicitWidth: 400
mode: StatusSyncCodeInput.WriteMode
}
}

View File

@ -0,0 +1,31 @@
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
Rectangle {
id: root
signal connectionStringFound(connectionString: string)
implicitWidth: 330
implicitHeight: 330
radius: 16
color: Theme.palette.baseColor4
StatusBaseText {
id: text
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 15
color: Theme.palette.dangerColor1
text: qsTr("QR code scanning is not available yet")
}
}

View File

@ -219,13 +219,14 @@ StatusSectionLayout {
Loader {
active: false
asynchronous: true
sourceComponent: DevicesView {
sourceComponent: SyncingView {
implicitWidth: parent.width
implicitHeight: parent.height
profileStore: root.store.profileStore
devicesStore: root.store.devicesStore
privacyStore: root.store.privacyStore
sectionTitle: root.store.getNameForSubsection(Constants.settingsSubsection.devicesSettings)
sectionTitle: root.store.getNameForSubsection(Constants.settingsSubsection.syncingSettings)
contentWidth: d.contentWidth
}
}

View File

@ -0,0 +1,253 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.14
import QtQml.StateMachine 1.14 as DSM
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
import shared.controls 1.0
import shared.views 1.0
import "setupsyncing" as Views
import "../stores"
StatusDialog {
id: root
property string password
property string keyUid
property DevicesStore devicesStore
property ProfileStore profileStore
width: 480
padding: 16
modal: true
title: qsTr("Sync a New Device")
QtObject {
id: d
signal generatingConnectionStringFailed
signal connectionStringGenerated
signal localPairingStarted
signal localPairingFailed
signal localPairingFinished
property string localPairingErrorMessage
property string connectionString
property string errorMessage
function generateConnectionString() {
d.connectionString = ""
d.errorMessage = ""
const result = root.devicesStore.getConnectionStringForBootstrappingAnotherDevice(root.keyUid, root.password)
try {
const json = JSON.parse(result)
d.errorMessage = json.error
} catch (e) {
d.connectionString = result
}
if (d.errorMessage !== "") {
d.generatingConnectionStringFailed()
return
}
displaySyncCodeView.secondsTimeout = 5 * 60 // This timeout should be moved to status-go.
displaySyncCodeView.start()
}
}
Connections {
target: root.devicesStore
function onLocalPairingStateChanged() {
switch (root.devicesStore.localPairingState) {
case Constants.LocalPairingState.WaitingForConnection:
break;
case Constants.LocalPairingState.Transferring:
d.localPairingStarted()
break
case Constants.LocalPairingState.Error:
d.localPairingFailed()
break
case Constants.LocalPairingState.Finished:
d.localPairingFinished()
break
}
}
}
DSM.StateMachine {
id: stateMachine
running: root.visible
initialState: displaySyncCodeState
DSM.State {
id: displaySyncCodeState
onEntered: {
d.generateConnectionString()
}
DSM.SignalTransition {
targetState: errorState
signal: d.generatingConnectionStringFailed
}
DSM.SignalTransition {
targetState: localPairingBaseState
signal: d.localPairingStarted
}
DSM.SignalTransition {
targetState: finalState
signal: nextButton.clicked
}
// Next 2 transitions are here temporarily.
// TODO: Remove when server notifies with ProcessSuccess/ProcessError event.
DSM.SignalTransition {
targetState: localPairingFailedState
signal: d.localPairingFailed
}
DSM.SignalTransition {
targetState: localPairingSuccessState
signal: d.localPairingFinished
}
}
DSM.State {
id: localPairingBaseState
initialState: localPairingInProgressState
DSM.State {
id: localPairingInProgressState
DSM.SignalTransition {
targetState: localPairingFailedState
signal: d.localPairingFailed
}
DSM.SignalTransition {
targetState: localPairingSuccessState
signal: d.localPairingFinished
}
}
DSM.State {
id: localPairingFailedState
DSM.SignalTransition {
targetState: finalState
signal: nextButton.clicked
}
}
DSM.State {
id: localPairingSuccessState
DSM.SignalTransition {
targetState: finalState
signal: nextButton.clicked
}
}
}
DSM.State {
id: errorState
DSM.SignalTransition {
targetState: finalState
signal: nextButton.clicked
}
}
DSM.FinalState {
id: finalState
onEntered: {
root.close()
}
}
}
contentItem: Item {
implicitWidth: Math.max(displaySyncCodeView.implicitWidth,
localPairingView.implicitWidth,
errorView.implicitWidth)
implicitHeight: Math.max(displaySyncCodeView.implicitHeight,
localPairingView.implicitHeight,
errorView.implicitHeight)
Views.DisplaySyncCode {
id: displaySyncCodeView
anchors.fill: parent
visible: displaySyncCodeState.active
connectionString: d.connectionString
secondsTimeout: 5 * 60
onRequestConnectionString: {
d.generateConnectionString()
}
}
DeviceSyncingView {
id: localPairingView
anchors.fill: parent
visible: localPairingBaseState.active
devicesModel: root.devicesStore.devicesModel
userDisplayName: root.profileStore.displayName
userPublicKey: root.profileStore.pubkey
userImage: root.profileStore.icon
localPairingState: root.devicesStore.localPairingState
localPairingError: root.devicesStore.localPairingError
}
Views.ErrorMessage {
id: errorView
anchors.fill: parent
visible: errorState.active
primaryText: qsTr("Failed to generate sync code")
secondaryText: d.errorMessage
}
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
id: nextButton
visible: !!text
enabled: !localPairingInProgressState.active
text: {
if (displaySyncCodeState.active
|| localPairingInProgressState.active
|| localPairingSuccessState.active)
return qsTr("Done");
if (localPairingFailedState.active
|| errorState.active)
return qsTr("Close");
return ""
}
}
}
}
}

View File

@ -0,0 +1,59 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Utils 0.1
import shared.controls 1.0
import "../stores"
StatusDialog {
id: root
property DevicesStore devicesStore
property var deviceModel
readonly property string deviceName: d.deviceName
title: qsTr("Personalize %1").arg(deviceModel.name)
width: implicitWidth
padding: 16
QtObject {
id: d
property string deviceName: ""
}
onOpened: {
nameInput.text = deviceModel.name
}
contentItem: ColumnLayout {
StatusInput {
id: nameInput
Layout.fillWidth: true
label: qsTr("Device name")
}
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Done")
enabled: nameInput.text !== ""
onClicked : {
root.devicesStore.setName(nameInput.text.trim())
root.close();
}
}
}
}
}

View File

@ -1 +1,2 @@
BackupSeedModal 1.0 BackupSeedModal.qml
SetupSyncingPopup 1.0 SetupSyncingPopup.qml

View File

@ -0,0 +1,238 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import shared.controls 1.0
import shared.panels 1.0
import utils 1.0
ColumnLayout {
id: root
property int secondsTimeout: 5 * 60
property string connectionString: ""
signal requestConnectionString()
function start() {
d.qrBlurred = true
d.codeExpired = false
d.secondsLeft = root.secondsTimeout
expireTimer.start()
}
spacing: 0
QtObject {
id: d
property int secondsLeft: root.secondsTimeout
property int secondsRatio: 1 // This property can be used to speed up testing of syncCode expiration
property bool qrBlurred: true
property bool codeExpired: false
onCodeExpiredChanged: {
if (codeExpired)
syncCodeInput.showPassword = false
}
}
Timer {
id: expireTimer
interval: root.secondsTimeout * 1000 / d.secondsRatio
onTriggered: {
d.codeExpired = true
}
}
Timer {
id: timeLeftUpdateTimer
interval: 1000 / d.secondsRatio
repeat: true
running: expireTimer.running
onTriggered: {
d.secondsLeft = Math.max(0, --d.secondsLeft)
}
}
Item {
Layout.alignment: Qt.AlignHCenter
implicitWidth: 254
implicitHeight: 254
Image {
id: qrCode
anchors.fill: parent
visible: false
asynchronous: true
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: false
source: globalUtils.qrCode(root.connectionString)
}
FastBlur {
anchors.fill: qrCode
source: qrCode
radius: d.codeExpired || d.qrBlurred ? 40 : 0
transparentBorder: true
Behavior on radius {
NumberAnimation { duration: 500 }
}
}
StatusButton {
id: revealButton
anchors.centerIn: parent
visible: !d.codeExpired && d.qrBlurred
normalColor: Theme.palette.primaryColor1
hoverColor: Theme.palette.miscColor1;
textColor: Theme.palette.indirectColor1
font.weight: Font.Medium
icon.name: "show"
text: qsTr("Reveal QR")
onClicked: {
d.qrBlurred = !d.qrBlurred
}
}
StatusButton {
id: regenerateButton
anchors.centerIn: parent
visible: d.codeExpired
normalColor: Theme.palette.primaryColor1
hoverColor: Theme.palette.miscColor1;
textColor: Theme.palette.indirectColor1
font.weight: Font.Medium
icon.name: "refresh"
text: qsTr("Regenerate")
onClicked: {
root.requestConnectionString()
}
}
}
Row {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
StatusBaseText {
font.pixelSize: 17
text: qsTr("Code valid for: ")
}
StatusBaseText {
id: timeoutText
width: fontMetrics.advanceWidth("10:00")
horizontalAlignment: Text.AlignLeft
font.pixelSize: 17
color: d.secondsLeft < 60 ? Theme.palette.dangerColor1 : Theme.palette.directColor1
text: {
const minutes = Math.floor(d.secondsLeft / 60);
const seconds = d.secondsLeft % 60;
return `${minutes}:${String(seconds).padStart(2,'0')}`;
}
FontMetrics {
id: fontMetrics
font: timeoutText.font
}
}
}
// TODO: Extract this to a component.
// Also used in `PasswordView` and several other files.
// https://github.com/status-im/status-desktop/issues/6136
StyledText {
id: inputLabel
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 7
text: qsTr("Sync code")
font.weight: Font.Medium
font.pixelSize: 13
color: Theme.palette.directColor1
}
Input {
id: syncCodeInput
property bool showPassword
readonly property bool effectiveShowPassword: showPassword && !d.codeExpired
Layout.fillWidth: true
Layout.bottomMargin: 24
readOnly: true
keepHeight: true
textField.echoMode: effectiveShowPassword ? TextInput.Normal : TextInput.Password
textField.rightPadding: syncCodeButtons.width + Style.current.padding / 2
textField.color: Style.current.textColor
textField.selectByMouse: !d.codeExpired
text: root.connectionString
Row {
id: syncCodeButtons
anchors.verticalCenter: syncCodeInput.verticalCenter
anchors.right: parent.right
spacing: 8
rightPadding: 8
leftPadding: 8
StatusFlatRoundButton {
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
icon.name: syncCodeInput.effectiveShowPassword ? "hide" : "show"
icon.color: Theme.palette.baseColor1
enabled: !d.codeExpired
onClicked: {
syncCodeInput.showPassword = !syncCodeInput.showPassword
}
}
StatusButton {
anchors.verticalCenter: parent.verticalCenter
size: StatusBaseButton.Size.Tiny
enabled: !d.codeExpired
text: qsTr("Copy")
onClicked: {
const showPassword = syncCodeInput.showPassword
syncCodeInput.showPassword = true
syncCodeInput.textField.selectAll()
syncCodeInput.textField.copy()
syncCodeInput.textField.deselect()
syncCodeInput.showPassword = showPassword
}
}
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
horizontalAlignment: Text.AlignHCenter
visible: !d.codeExpired
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("On your other device, navigate to the Syncing<br>screen and select Enter Sync Code.")
}
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
horizontalAlignment: Text.AlignHCenter
visible: d.codeExpired
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("Your QR and Sync Code has expired.")
}
}

View File

@ -0,0 +1,52 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Utils 0.1
import shared.controls 1.0
ColumnLayout {
id: root
property string primaryText
property string secondaryText
spacing: 12
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: parent.height / 2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
text: root.primaryText
font.pixelSize: 17
color: Theme.palette.dangerColor1
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 60
Layout.rightMargin: 60
Layout.preferredWidth: 360
Layout.preferredHeight: parent.height / 2
Layout.minimumHeight: detailsView.implicitHeight
ErrorDetails {
id: detailsView
anchors {
top: parent.top
left: parent.left
right: parent.right
}
title: qsTr("Failed to start pairing server")
details: root.secondaryText
}
}
}

View File

@ -6,10 +6,13 @@ QtObject {
property var devicesModule
property var devicesModel: devicesModule.model
property var devicesModel: devicesModule ? devicesModule.model : null
// Module Properties
property bool isDeviceSetup: devicesModule.isDeviceSetup
property bool isDeviceSetup: devicesModule ? devicesModule.isDeviceSetup : false
readonly property int localPairingState: devicesModule ? devicesModule.localPairingState : -1
readonly property string localPairingError: devicesModule ? devicesModule.localPairingError : ""
function loadDevices() {
return root.devicesModule.loadDevices()
@ -30,4 +33,21 @@ QtObject {
function enableDevice(installationId, enable) {
root.devicesModule.enableDevice(installationId, enable)
}
function authenticateUser() {
const keyUid = "" // TODO: Support Keycard
root.devicesModule.authenticateUser(keyUid)
}
function validateConnectionString(connectionString) {
return root.devicesModule.validateConnectionString(connectionString)
}
function getConnectionStringForBootstrappingAnotherDevice(keyUid, password) {
return root.devicesModule.getConnectionStringForBootstrappingAnotherDevice(keyUid, password)
}
function inputConnectionStringForBootstrapping(connectionString) {
return root.devicesModule.inputConnectionStringForBootstrapping(connectionString)
}
}

View File

@ -88,6 +88,9 @@ QtObject {
append({subsection: Constants.settingsSubsection.ensUsernames,
text: qsTr("ENS usernames"),
icon: "username"})
append({subsection: Constants.settingsSubsection.syncingSettings,
text: qsTr("Syncing"),
icon: "rotate"})
}
}
@ -119,9 +122,6 @@ QtObject {
append({subsection: Constants.settingsSubsection.language,
text: qsTr("Language & Currency"),
icon: "language"})
append({subsection: Constants.settingsSubsection.devicesSettings,
text: qsTr("Devices settings"),
icon: "mobile"})
append({subsection: Constants.settingsSubsection.advanced,
text: qsTr("Advanced"),
icon: "settings"})

View File

@ -15,6 +15,8 @@ QtObject {
property string icon: !!Global.userProfile? Global.userProfile.icon : ""
property bool userDeclinedBackupBanner: Global.appIsReady? localAccountSensitiveSettings.userDeclinedBackupBanner : false
property var privacyStore: profileSectionModule.privacyModule
readonly property string keyUid: userProfile.keyUid
readonly property bool isKeycardUser: userProfile.isKeycardUser
readonly property string bio: profileModule.bio
readonly property string socialLinksJson: profileModule.socialLinksJson

View File

@ -1,251 +0,0 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import utils 1.0
import shared.panels 1.0
import shared.controls 1.0
import "../stores"
SettingsContentBase {
id: root
property DevicesStore devicesStore
property PrivacyStore privacyStore
property bool isSyncing: false
Component.onCompleted: {
root.devicesStore.loadDevices()
}
Column {
width: root.contentWidth
height: !!parent ? parent.height : 0
spacing: Style.current.padding
Item {
id: firstTimeSetup
width: parent.width
visible: !root.devicesStore.isDeviceSetup
height: visible ? childrenRect.height : 0
StatusBaseText {
id: deviceNameLbl
text: qsTr("Please set a name for your device.")
font.pixelSize: 14
color: Theme.palette.directColor1
}
Input {
id: deviceNameTxt
anchors.left: parent.left
anchors.right: parent.right
anchors.top: deviceNameLbl.bottom
anchors.topMargin: Style.current.padding
placeholderText: qsTr("Specify a name")
}
// TODO: replace with StatusQ component
StatusButton {
anchors.top: deviceNameTxt.bottom
anchors.topMargin: 10
anchors.right: deviceNameTxt.right
text: qsTr("Continue")
enabled: deviceNameTxt.text !== ""
onClicked : root.devicesStore.setName(deviceNameTxt.text.trim())
}
}
Item {
id: advertiseDeviceItem
width: parent.width
visible: root.devicesStore.isDeviceSetup
height: visible ? advertiseDevice.height + learnMoreText.height + Style.current.padding : 0
Item {
id: advertiseDevice
height: childrenRect.height
width: 500
SVGImage {
id: advertiseImg
height: 32
width: 32
anchors.left: parent.left
fillMode: Image.PreserveAspectFit
source: Style.svg("messageActive")
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.blue
}
}
StatusBaseText {
id: advertiseDeviceTitle
text: qsTr("Advertise device")
font.pixelSize: 18
font.weight: Font.Bold
color: Theme.palette.primaryColor1
anchors.left: advertiseImg.right
anchors.leftMargin: Style.current.padding
}
StatusBaseText {
id: advertiseDeviceDesk
text: qsTr("Pair your devices to sync contacts and chats between them")
font.pixelSize: 14
anchors.top: advertiseDeviceTitle.bottom
anchors.topMargin: 6
anchors.left: advertiseImg.right
anchors.leftMargin: Style.current.padding
color: Theme.palette.directColor1
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: advertiseDevice
onClicked: root.devicesStore.advertise()
}
StatusBaseText {
id: learnMoreText
anchors.top: advertiseDevice.bottom
anchors.topMargin: Style.current.padding
text: qsTr("Learn more")
font.pixelSize: 16
color: Theme.palette.primaryColor1
anchors.left: parent.left
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: Global.openLink("https://status.im/user_guides/pairing_devices.html")
}
}
}
StatusBaseText {
visible: root.devicesStore.devicesModule.devicesLoading
text: qsTr("Loading devices...")
}
StatusBaseText {
visible: root.devicesStore.devicesModule.devicesLoadingError
text: qsTr("Error loading devices. Please try again later.")
}
Item {
id: deviceListItem
width: parent.width
height: childrenRect.height
visible: !root.devicesStore.devicesModule.devicesLoading &&
!root.devicesStore.devicesModule.devicesLoadingError &&
root.devicesStore.isDeviceSetup
StatusBaseText {
id: deviceListLbl
text: qsTr("Paired devices")
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.palette.directColor1
}
StatusListView {
id: listView
anchors.top: deviceListLbl.bottom
anchors.topMargin: Style.current.padding
// This is a placeholder fix to the display. This whole page will be redesigned
height: 300
spacing: 5
width: parent.width
model: root.devicesStore.devicesModel
// TODO: replace with StatusQ component
delegate: Item {
height: childrenRect.height
SVGImage {
id: enabledIcon
source: Style.svg("messageActive")
height: 24
width: 24
ColorOverlay {
anchors.fill: parent
source: parent
color: devicePairedSwitch.checked ? Style.current.blue : Style.current.darkGrey
}
}
StatusBaseText {
id: deviceItemLbl
text: {
let deviceId = model.installationId.split("-")[0].substr(0, 5)
let labelText = `${model.name || qsTr("No info")} ` +
`(${model.isCurrentDevice ? qsTr("you") + ", ": ""}${deviceId})`;
return labelText;
}
elide: Text.ElideRight
font.pixelSize: 14
anchors.left: enabledIcon.right
anchors.leftMargin: Style.current.padding
color: Theme.palette.directColor1
}
StatusSwitch {
id: devicePairedSwitch
visible: !model.isCurrentDevice
checked: model.enabled
anchors.left: deviceItemLbl.right
anchors.leftMargin: Style.current.padding
anchors.top: deviceItemLbl.top
onClicked: root.devicesStore.enableDevice(model.installationId, devicePairedSwitch)
}
}
}
StatusButton {
id: syncAllBtn
anchors.top: listView.bottom
anchors.topMargin: Style.current.padding
// anchors.bottom: parent.bottom
// anchors.bottomMargin: Style.current.padding
anchors.horizontalCenter: listView.horizontalCenter
text: isSyncing ?
qsTr("Syncing...") :
qsTr("Sync all devices")
enabled: !isSyncing
onClicked : {
isSyncing = true;
root.devicesStore.syncAll()
// Currently we don't know how long it takes, so we just disable for 10s, to avoid spamming
timer.setTimeout(function(){
isSyncing = false
}, 10000);
}
}
StatusButton {
id: backupBtn
anchors.top: syncAllBtn.bottom
anchors.topMargin: Style.current.padding
anchors.horizontalCenter: listView.horizontalCenter
text: qsTr("Backup Data")
onClicked : {
let lastUpdate = root.privacyStore.backupData() * 1000
console.log("Backup done at: ", LocaleUtils.formatDateTime(lastUpdate))
}
}
}
Timer {
id: timer
}
}
}

View File

@ -0,0 +1,273 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtQml.Models 2.14
import QtGraphicalEffects 1.13
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import utils 1.0
import shared.panels 1.0
import shared.controls 1.0
import shared.controls.chat 1.0
import "../stores"
import "../popups"
import "../controls"
import "../../stores"
SettingsContentBase {
id: root
property DevicesStore devicesStore
property ProfileStore profileStore
property PrivacyStore privacyStore
property bool isSyncing: false
Component.onCompleted: {
root.devicesStore.loadDevices()
}
ColumnLayout {
width: root.contentWidth
spacing: Style.current.padding
QtObject {
id: d
/*
Device INFO:
id: "abcdabcd-1234-5678-9012-12a34b5cd678",
identity: ""
version: 1
enabled: true
timestamp: 0
metadata:
name: "MacBook-1"
deviceType: "macosx"
fcmToken: ""
*/
readonly property var instructionsModel: [
qsTr("Verify your login with password or KeyCard"),
qsTr("Reveal a temporary QR and Sync Code") + "*",
qsTr("Share that information with your new device"),
]
function personalizeDevice(model) {
Global.openPopup(personalizeDevicePopup, {
"deviceModel": model
})
}
function setupSyncing() {
const keyUid = root.profileStore.isKeycardUser ? root.profileStore.keyUid : ""
root.devicesStore.authenticateUser(keyUid)
}
}
Connections {
target: devicesStore.devicesModule
function onUserAuthenticated(pin, password, keyUid) {
if (!password)
return
// Authentication flow returns empty keyUid for non-keycard user.
const effectiveKeyUid = root.profileStore.isKeycardUser
? keyUid
: root.profileStore.keyUid
Global.openPopup(setupSyncingPopup, {
password,
keyUid: effectiveKeyUid
})
}
}
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Devices")
font.pixelSize: 15
}
StatusBaseText {
Layout.fillWidth: true
visible: root.devicesStore.devicesModule.devicesLoading
text: qsTr("Loading devices...")
}
StatusBaseText {
Layout.fillWidth: true
visible: root.devicesStore.devicesModule.devicesLoadingError
text: qsTr("Error loading devices. Please try again later.")
}
ListView {
id: listView
Layout.fillWidth: true
Layout.topMargin: 17
Layout.bottomMargin: 17
implicitHeight: contentHeight
spacing: Style.current.padding
model: root.devicesStore.devicesModel
visible: !root.devicesStore.devicesModule.devicesLoading &&
!root.devicesStore.devicesModule.devicesLoadingError &&
root.devicesStore.isDeviceSetup
delegate: StatusSyncDeviceDelegate {
width: ListView.view.width
deviceName: model.name
deviceType: model.deviceType
timestamp: model.timestamp
isCurrentDevice: model.isCurrentDevice
onSetupSyncingButtonClicked: {
d.setupSyncing(SetupSyncingPopup.GenerateSyncCode)
}
onClicked: {
d.personalizeDevice(model)
}
}
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 17
implicitWidth: instructionsLayout.implicitWidth
+ instructionsLayout.anchors.leftMargin
+ instructionsLayout.anchors.rightMargin
implicitHeight: instructionsLayout.implicitHeight
+ instructionsLayout.anchors.topMargin
+ instructionsLayout.anchors.bottomMargin
color: Theme.palette.primaryColor3
radius: 8
ColumnLayout {
id: instructionsLayout
anchors {
fill: parent
topMargin: 24
bottomMargin: 24
leftMargin: 16
rightMargin: 16
}
spacing: 17
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: -8
horizontalAlignment: Text.AlignHCenter
color: Theme.palette.primaryColor1
font.pixelSize: 17
font.weight: Font.Bold
text: qsTr("Sync a New Device")
}
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
color: Theme.palette.baseColor1
font.pixelSize: 15
font.weight: Font.Medium
text: qsTr("You own your data. Sync it among your devices.")
}
GridLayout {
Layout.alignment: Qt.AlignHCenter
rows: d.instructionsModel.length
flow: GridLayout.TopToBottom
Repeater {
model: d.instructionsModel
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
color: Theme.palette.baseColor1
font.pixelSize: 13
font.weight: Font.Medium
text: index + 1
}
}
Repeater {
model: d.instructionsModel
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
color: Theme.palette.directColor1
font.pixelSize: 15
text: modelData
}
}
}
StatusButton {
// type: StatusRoundButton.Type.Secondary
Layout.alignment: Qt.AlignHCenter
normalColor: Theme.palette.primaryColor1
hoverColor: Theme.palette.miscColor1;
textColor: Theme.palette.indirectColor1
font.weight: Font.Medium
text: qsTr("Setup Syncing")
onClicked: {
d.setupSyncing()
}
}
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
color: Theme.palette.baseColor1
font.pixelSize: 13
text: "* " + qsTr("This is best done in private. The code will grant access to your profile.")
}
}
}
StatusButton {
id: backupBtn
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 17
text: qsTr("Backup Data")
onClicked : {
let lastUpdate = root.privacyStore.backupData() * 1000
console.log("Backup done at: ", LocaleUtils.formatDateTime(lastUpdate))
}
}
Component {
id: personalizeDevicePopup
SyncDeviceCustomizationPopup {
anchors.centerIn: parent
devicesStore: root.devicesStore
}
}
Component {
id: setupSyncingPopup
SetupSyncingPopup {
anchors.centerIn: parent
devicesStore: root.devicesStore
profileStore: root.profileStore
}
}
}
}

View File

@ -0,0 +1,71 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
Rectangle {
id: root
property string title
property string details
readonly property string detailsVisible: d.detailsVisible
implicitWidth: layout.implicitWidthj
+ layout.anchors.leftMargin
+ layout.anchors.rigthMargin
implicitHeight: layout.implicitHeight
+ layout.anchors.topMargin
+ layout.anchors.bottomMargin
radius: 8
color: Theme.palette.baseColor4
QtObject {
id: d
property bool detailsVisible: false
}
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: 10
spacing: 4
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Qt.AlignHCenter
text: root.title
font.pixelSize: 13
font.weight: Font.Medium
}
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Qt.AlignHCenter
visible: !d.detailsVisible
text: qsTr("Show error details")
color: Theme.palette.primaryColor1
font.pixelSize: 12
MouseArea {
anchors.fill: parent
onClicked: {
d.detailsVisible = true
}
}
}
StatusBaseText {
Layout.fillWidth: true
horizontalAlignment: Qt.AlignHCenter
visible: d.detailsVisible
text: root.details
color: Theme.palette.baseColor1
font.pixelSize: 12
wrapMode: Text.WordWrap
}
}
}

View File

@ -0,0 +1,98 @@
import QtQuick 2.0
import QtQuick.Layouts 1.12
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
Column {
id: root
spacing: 4
QtObject {
id: d
readonly property int listItemHeight: 40
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("1. Open Status App on your desktop device")
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("2. Open")
}
StatusRoundIcon {
asset.name: "settings"
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.directColor1
text: qsTr("Settings")
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("3. Navigate to the ")
}
StatusRoundIcon {
asset.name: "rotate"
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Syncing tab")
font.pixelSize: 15
color: Theme.palette.directColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("4. Click")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Setup Syncing")
font.pixelSize: 15
color: Theme.palette.directColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("5. Scan or enter the code ")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
}
}

View File

@ -0,0 +1,98 @@
import QtQuick 2.0
import QtQuick.Layouts 1.12
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
Column {
id: root
spacing: 4
QtObject {
id: d
readonly property int listItemHeight: 40
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("1. Open Status App on your mobile device")
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("2. Open your")
}
StatusRoundIcon {
asset.name: "profile"
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.directColor1
text: qsTr("Profile")
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("3. Go to")
}
StatusRoundIcon {
asset.name: "rotate"
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Syncing")
font.pixelSize: 15
color: Theme.palette.directColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("4. Tap")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Sync new device")
font.pixelSize: 15
color: Theme.palette.directColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("5. Scan or enter the code ")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
}
}

View File

@ -0,0 +1,77 @@
import QtQuick 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
StatusInput {
id: root
// TODO: Use https://github.com/status-im/status-desktop/issues/6136
enum Mode {
WriteMode,
ReadMode
}
property int mode: StatusSyncCodeInput.Mode.WriteMode
property bool readOnly: false
QtObject {
id: d
readonly property bool writeMode: root.mode === StatusSyncCodeInput.Mode.WriteMode
}
label: d.writeMode ? qsTr("Paste sync code") : qsTr("Sync code")
input.edit.readOnly: root.readOnly
input.placeholderText: d.writeMode ? qsTr("eg. %1").arg("0x2Ef19") : ""
input.font: Theme.palette.monoFont.name
input.placeholderFont: root.input.font
input.rightComponent: {
switch (root.mode) {
case StatusSyncCodeInput.Mode.WriteMode:
return root.valid ? validCodeIconComponent
: pasteButtonComponent
case StatusSyncCodeInput.Mode.ReadMode:
return copyButtonComponent
}
}
Component {
id: copyButtonComponent
StatusButton {
size: StatusBaseButton.Size.Tiny
text: qsTr("Copy")
onClicked: {
root.input.edit.selectAll();
root.input.edit.copy();
root.input.edit.deselect();
}
}
}
Component {
id: pasteButtonComponent
StatusButton {
size: StatusBaseButton.Size.Tiny
enabled: !root.readOnly && root.input.edit.canPaste
text: qsTr("Paste")
onClicked: {
root.input.edit.selectAll();
root.input.edit.paste();
}
}
}
Component {
id: validCodeIconComponent
StatusIcon {
icon: "tiny/checkmark"
color: Theme.palette.successColor1
}
}
}

View File

@ -47,7 +47,7 @@ Item {
value: icon
}
height: visible ? contentContainer.height : 0
implicitWidth: contentContainer.implicitWidth
implicitHeight: contentContainer.implicitHeight
QtObject {

View File

@ -16,6 +16,7 @@ SearchBox 1.0 SearchBox.qml
SeedPhraseTextArea 1.0 SeedPhraseTextArea.qml
SendToContractWarning 1.0 SendToContractWarning.qml
SettingsRadioButton 1.0 SettingsRadioButton.qml
StatusSyncingInstructions 1.0 StatusSyncingInstructions.qml
StyledButton 1.0 StyledButton.qml
StyledTextArea 1.0 StyledTextArea.qml
StyledTextEdit 1.0 StyledTextEdit.qml
@ -32,3 +33,7 @@ TransactionDetailsHeader.qml 1.0 TransactionDetailsHeader.qml
TokenDelegate 1.0 TokenDelegate.qml
StyledTextEditWithLoadingState 1.0 StyledTextEditWithLoadingState.qml
LoadingTokenDelegate 1.0 LoadingTokenDelegate.qml
StatusSyncCodeInput 1.0 StatusSyncCodeInput.qml
GetSyncCodeMobileInstructions 1.0 GetSyncCodeMobileInstructions.qml
GetSyncCodeDesktopInstructions 1.0 GetSyncCodeDesktopInstructions.qml
ErrorDetails 1.0 ErrorDetails.qml

View File

@ -0,0 +1,68 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import shared.controls 1.0
StatusDialog {
id: root
enum Source {
Mobile,
Desktop
}
title: qsTr("How to get a sync code on...")
padding: 40
footer: null
ColumnLayout {
anchors.fill: parent
spacing: 0
StatusSwitchTabBar {
id: switchTabBar
Layout.fillWidth: true
Layout.minimumWidth: 400
currentIndex: 0
StatusSwitchTabButton {
text: qsTr("Mobile")
}
StatusSwitchTabButton {
text: qsTr("Desktop")
}
}
Item {
Layout.fillWidth: true
implicitHeight: 41
}
StackLayout {
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.max(mobileSync.implicitWidth, desktopSync.implicitWidth)
implicitHeight: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight)
currentIndex: switchTabBar.currentIndex
GetSyncCodeMobileInstructions {
id: mobileSync
Layout.fillHeight: true
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
}
GetSyncCodeDesktopInstructions {
id: desktopSync
Layout.fillHeight: true
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
}
}
}
}

View File

@ -24,3 +24,4 @@ ImportCommunityPopup 1.0 ImportCommunityPopup.qml
DisplayNamePopup 1.0 DisplayNamePopup.qml
SendContactRequestModal 1.0 SendContactRequestModal.qml
AccountsModalHeader 1.0 AccountsModalHeader.qml
GetSyncCodeInstructionsPopup 1.0 GetSyncCodeInstructionsPopup.qml

View File

@ -0,0 +1,188 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import shared.controls 1.0
import shared.controls.chat 1.0
import utils 1.0
Item {
id: root
property alias devicesModel: listView.model
property string userDisplayName
property string userColorId
property string userColorHash
property string userPublicKey
property string userImage
property int localPairingState: Constants.LocalPairingState.Idle
property string localPairingError
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
QtObject {
id: d
readonly property int deviceDelegateWidth: 220
readonly property bool pairingFailed: root.localPairingState === Constants.LocalPairingState.Error
readonly property bool pairingSuccess: root.localPairingState === Constants.LocalPairingState.Finished
readonly property bool pairingInProgress: !d.pairingFailed && !d.pairingSuccess
}
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 0
// This is used in the profile section. The user's pubkey is available
// so we can calculate the hash and colorId
Loader {
id: profileSectionUserImage
active: root.userPublicKey != ""
Layout.alignment: Qt.AlignHCenter
sourceComponent: UserImage {
name: root.userDisplayName
pubkey: root.userPublicKey
image: root.userImage
interactive: false
imageWidth: 80
imageHeight: 80
}
}
// This is used in the onboarding once a sync code is received. The
// user's pubkey is unknown, but we have the multiaccount information
// available (from the plaintext accounts db), so we use the colorHash
// and colorId directly
Loader {
id: colorUserImage
active: root.userPublicKey == ""
Layout.alignment: Qt.AlignHCenter
sourceComponent: UserImage {
name: root.userDisplayName
colorId: root.userColorId
colorHash: root.userColorHash
image: root.userImage
interactive: false
imageWidth: 80
imageHeight: 80
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: 8
horizontalAlignment: Text.AlignHCenter
color: Theme.palette.directColor1
font.weight: Font.Bold
font.pixelSize: 22
elide: Text.ElideRight
wrapMode: Text.Wrap
text: root.userDisplayName
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: 31
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 17
color: d.pairingFailed ? Theme.palette.dangerColor1 : Theme.palette.directColor1
text: {
if (d.pairingInProgress)
return qsTr("Device found!");
if (d.pairingSuccess)
return qsTr("Device synced!");
if (d.pairingFailed)
return qsTr("Device failed to sync");
return "";
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.bottomMargin: 25
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
visible: !!text
text: {
if (d.pairingInProgress)
return qsTr("Syncing your profile and settings preferences");
if (d.pairingSuccess)
return qsTr("Your devices are now in sync");
return "";
}
}
StatusSyncDeviceDelegate {
Layout.alignment: Qt.AlignHCenter
implicitWidth: d.deviceDelegateWidth
visible: !d.pairingFailed
subTitle: qsTr("Synced device")
enabled: false
loading: d.pairingInProgress
deviceName: qsTr("No device name")
isCurrentDevice: false
}
ErrorDetails {
Layout.fillWidth: true
Layout.leftMargin: 60
Layout.rightMargin: 60
Layout.preferredWidth: 360
Layout.topMargin: 12
visible: d.pairingFailed
title: qsTr("Failed to sync devices")
details: root.localPairingError
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 25
Layout.bottomMargin: 25
implicitHeight: 1
implicitWidth: d.deviceDelegateWidth
color: Theme.palette.baseColor4
opacity: listView.count ? 1 : 0
}
StatusScrollView {
id: scrollView
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
clip: true
padding: 0
contentWidth: d.deviceDelegateWidth
contentHeight: listView.contentHeight
implicitWidth: contentWidth + leftPadding + rightPadding
implicitHeight: contentHeight + topPadding + bottomPadding
ListView {
id: listView
width: scrollView.availableWidth
height: scrollView.availableHeight
spacing: 4
clip: true
delegate: StatusSyncDeviceDelegate {
width: ListView.view.width
enabled: false
deviceName: model.name
deviceType: model.deviceType
timestamp: model.timestamp
isCurrentDevice: model.isCurrentDevice
}
}
}
}
}

View File

@ -168,8 +168,9 @@ StatusMenu {
}
ProfileHeader {
width: parent.width
visible: root.isProfile
width: parent.width
height: visible ? implicitHeight : 0
displayNameVisible: false
displayNamePlusIconsVisible: true

View File

@ -10,3 +10,4 @@ PasswordView 1.0 PasswordView.qml
ProfileDialogView 1.0 ProfileDialogView.qml
AssetsView 1.0 AssetsView.qml
HistoryView 1.0 HistoryView.qml
DeviceSyncingView 1.0 DeviceSyncingView.qml

View File

@ -90,6 +90,8 @@ QtObject {
readonly property string profileFetchingTimeout: "ProfileFetchingTimeout"
readonly property string profileFetchingAnnouncement: "ProfileFetchingAnnouncement"
readonly property string lostKeycardOptions: "LostKeycardOptions"
readonly property string syncDeviceWithSyncCode: "SyncDeviceWithSyncCode"
readonly property string syncDeviceResult: "SyncDeviceResult"
}
readonly property QtObject predefinedKeycardData: QtObject {
@ -325,7 +327,7 @@ QtObject {
property int appearance: 5
property int language: 6
property int notifications: 7
property int devicesSettings: 8
property int syncingSettings: 8
property int browserSettings: 9
property int advanced: 10
property int about: 11
@ -585,6 +587,32 @@ QtObject {
readonly property int telegram: 6
}
readonly property QtObject localPairingEventType: QtObject {
readonly property int eventUnknown: -1
readonly property int eventConnectionError: 0
readonly property int eventConnectionSuccess: 1
readonly property int eventTransferError: 2
readonly property int eventTransferSuccess: 3
readonly property int eventReceivedAccount: 4
readonly property int eventProcessSuccess: 5
readonly property int eventProcessError: 6
}
readonly property QtObject localPairingAction: QtObject {
readonly property int actionUnknown: 0
readonly property int actionConnect: 1
readonly property int actionPairingAccount: 2
readonly property int actionSyncDevice: 3
}
enum LocalPairingState {
Idle = 0,
WaitingForConnection = 1,
Transferring = 2,
Error = 3,
Finished = 4
}
readonly property QtObject regularExpressions: QtObject {
readonly property var alphanumericalExpanded: /^$|^[a-zA-Z0-9\-_ ]+$/
}

@ -1 +1 @@
Subproject commit 4d2d359aec6a9db5fb684c97ebd52b82065d60f3
Subproject commit 73fe79616ce6c7a6e5e79f6425d20cd74b788ffd

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 5ecb7b68eefada3725c360f68fba7ac92b612c82
Subproject commit bac7eb08ca233a4ab9177ac16fa4fe0c2b68f79e