feat(sync): add a fallback mechanism when the pairing does't work (#15820)

Fixes #15750

When the pairing fails, the UI now let's the user use the seed phrase instead.
When they do, a call is send to the original device and both instances will show an AC notif.
When the original device accepts the pairing, the call is made to pair and sync the devices and the AC notifs get deleted
This commit is contained in:
Jonathan Rainville 2024-10-02 09:54:35 -04:00 committed by GitHub
parent 4133afb676
commit 252061d8e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 491 additions and 16 deletions

View File

@ -7,6 +7,7 @@ import ../../../../app_service/service/contacts/service as contacts_service
import ../../../../app_service/service/message/service as message_service import ../../../../app_service/service/message/service as message_service
import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/community/service as community_service
import ../../../../app_service/service/chat/service as chat_service import ../../../../app_service/service/chat/service as chat_service
import ../../../../app_service/service/devices/service as devices_service
type type
Controller* = ref object of RootObj Controller* = ref object of RootObj
@ -17,6 +18,7 @@ type
messageService: message_service.Service messageService: message_service.Service
chatService: chat_service.Service chatService: chat_service.Service
communityService: community_service.Service communityService: community_service.Service
devicesService: devices_service.Service
proc newController*( proc newController*(
delegate: io_interface.AccessInterface, delegate: io_interface.AccessInterface,
@ -26,6 +28,7 @@ proc newController*(
messageService: message_service.Service, messageService: message_service.Service,
chatService: chat_service.Service, chatService: chat_service.Service,
communityService: community_service.Service, communityService: community_service.Service,
devicesService: devices_service.Service,
): Controller = ): Controller =
result = Controller() result = Controller()
result.delegate = delegate result.delegate = delegate
@ -35,6 +38,7 @@ proc newController*(
result.messageService = messageService result.messageService = messageService
result.chatService = chatService result.chatService = chatService
result.communityService = communityService result.communityService = communityService
result.devicesService = devicesService
proc delete*(self: Controller) = proc delete*(self: Controller) =
discard discard
@ -162,3 +166,6 @@ proc setActivityCenterReadType*(self: Controller, readType: ActivityCenterReadTy
proc getActivityCenterReadType*(self: Controller): ActivityCenterReadType = proc getActivityCenterReadType*(self: Controller): ActivityCenterReadType =
return self.activityCenterService.getActivityCenterReadType() return self.activityCenterService.getActivityCenterReadType()
proc enableInstallationAndSync*(self: Controller, installationId: string) =
self.devicesService.enableInstallationAndSync(installationId)

View File

@ -110,3 +110,6 @@ method getActivityCenterReadType*(self: AccessInterface): int {.base.} =
method setActivityGroupCounters*(self: AccessInterface, counters: Table[ActivityCenterGroup, int]) {.base.} = method setActivityGroupCounters*(self: AccessInterface, counters: Table[ActivityCenterGroup, int]) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method enableInstallationAndSync*(self: AccessInterface, installationId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -25,6 +25,7 @@ type Item* = ref object
repliedMessageItem: MessageItem repliedMessageItem: MessageItem
chatType: ChatType chatType: ChatType
tokenDataItem: TokenDataItem tokenDataItem: TokenDataItem
installationId: string
proc initItem*( proc initItem*(
id: string, id: string,
@ -43,7 +44,8 @@ proc initItem*(
messageItem: MessageItem, messageItem: MessageItem,
repliedMessageItem: MessageItem, repliedMessageItem: MessageItem,
chatType: ChatType, chatType: ChatType,
tokenDataItem: TokenDataItem tokenDataItem: TokenDataItem,
installationId: string
): Item = ): Item =
result = Item() result = Item()
result.id = id result.id = id
@ -63,6 +65,7 @@ proc initItem*(
result.repliedMessageItem = repliedMessageItem result.repliedMessageItem = repliedMessageItem
result.chatType = chatType result.chatType = chatType
result.tokenDataItem = tokenDataItem result.tokenDataItem = tokenDataItem
result.installationId = installationId
proc `$`*(self: Item): string = proc `$`*(self: Item): string =
result = fmt"""activity_center/Item( result = fmt"""activity_center/Item(
@ -74,6 +77,7 @@ proc `$`*(self: Item): string =
verificationStatus: {$self.verificationStatus.int}, verificationStatus: {$self.verificationStatus.int},
sectionId: {$self.sectionId}, sectionId: {$self.sectionId},
author: {$self.author}, author: {$self.author},
installationId: {$self.installationId},
notificationType: {$self.notificationType.int}, notificationType: {$self.notificationType.int},
timestamp: {$self.timestamp}, timestamp: {$self.timestamp},
read: {$self.read}, read: {$self.read},
@ -93,6 +97,9 @@ proc name*(self: Item): string =
proc author*(self: Item): string = proc author*(self: Item): string =
return self.author return self.author
proc installationId*(self: Item): string =
return self.installationId
proc chatId*(self: Item): string = proc chatId*(self: Item): string =
return self.chatId return self.chatId

View File

@ -21,6 +21,7 @@ type
RepliedMessage RepliedMessage
ChatType ChatType
TokenData TokenData
InstallationId
QtObject: QtObject:
type type
@ -89,6 +90,7 @@ QtObject:
of NotifRoles.RepliedMessage: result = newQVariant(activityNotificationItem.repliedMessageItem) of NotifRoles.RepliedMessage: result = newQVariant(activityNotificationItem.repliedMessageItem)
of NotifRoles.ChatType: result = newQVariant(activityNotificationItem.chatType.int) of NotifRoles.ChatType: result = newQVariant(activityNotificationItem.chatType.int)
of NotifRoles.TokenData: result = newQVariant(activityNotificationItem.tokenDataItem) of NotifRoles.TokenData: result = newQVariant(activityNotificationItem.tokenDataItem)
of NotifRoles.InstallationId: result = newQVariant(activityNotificationItem.installationId)
method roleNames(self: Model): Table[int, string] = method roleNames(self: Model): Table[int, string] =
{ {
@ -109,7 +111,8 @@ QtObject:
NotifRoles.Accepted.int: "accepted", NotifRoles.Accepted.int: "accepted",
NotifRoles.RepliedMessage.int: "repliedMessage", NotifRoles.RepliedMessage.int: "repliedMessage",
NotifRoles.ChatType.int: "chatType", NotifRoles.ChatType.int: "chatType",
NotifRoles.TokenData.int: "tokenData" NotifRoles.TokenData.int: "tokenData",
NotifRoles.InstallationId.int: "installationId",
}.toTable }.toTable
proc findNotificationIndex(self: Model, notificationId: string): int = proc findNotificationIndex(self: Model, notificationId: string): int =

View File

@ -14,6 +14,7 @@ import ../../../../app_service/service/contacts/service as contacts_service
import ../../../../app_service/service/message/service as message_service import ../../../../app_service/service/message/service as message_service
import ../../../../app_service/service/chat/service as chat_service import ../../../../app_service/service/chat/service as chat_service
import ../../../../app_service/service/community/service as community_service import ../../../../app_service/service/community/service as community_service
import ../../../../app_service/service/devices/service as devices_service
export io_interface export io_interface
@ -33,7 +34,8 @@ proc newModule*(
contactsService: contacts_service.Service, contactsService: contacts_service.Service,
messageService: message_service.Service, messageService: message_service.Service,
chatService: chat_service.Service, chatService: chat_service.Service,
communityService: community_service.Service communityService: community_service.Service,
devicesService: devices_service.Service,
): Module = ): Module =
result = Module() result = Module()
result.delegate = delegate result.delegate = delegate
@ -46,7 +48,8 @@ proc newModule*(
contactsService, contactsService,
messageService, messageService,
chatService, chatService,
communityService communityService,
devicesService,
) )
result.moduleLoaded = false result.moduleLoaded = false
@ -216,7 +219,8 @@ method convertToItems*(
messageItem, messageItem,
repliedMessageItem, repliedMessageItem,
chatDetails.chatType, chatDetails.chatType,
tokenDataItem tokenDataItem,
notification.installationId,
) )
) )
@ -323,3 +327,6 @@ method getActivityCenterReadType*(self: Module): int =
method setActivityGroupCounters*(self: Module, counters: Table[ActivityCenterGroup, int]) = method setActivityGroupCounters*(self: Module, counters: Table[ActivityCenterGroup, int]) =
self.view.setActivityGroupCounters(counters) self.view.setActivityGroupCounters(counters)
method enableInstallationAndSync*(self: Module, installationId: string) =
self.controller.enableInstallationAndSync(installationId)

View File

@ -211,3 +211,6 @@ QtObject:
proc setActivityGroupCounters*(self: View, counters: Table[ActivityCenterGroup, int]) = proc setActivityGroupCounters*(self: View, counters: Table[ActivityCenterGroup, int]) =
self.groupCounters = counters self.groupCounters = counters
self.groupCountersChanged() self.groupCountersChanged()
proc enableInstallationAndSync*(self: View, installationId: string) {.slot.} =
self.delegate.enableInstallationAndSync(installationId)

View File

@ -225,7 +225,7 @@ proc newModule*[T](
networkService, tokenService) networkService, tokenService)
result.gifsModule = gifs_module.newModule(result, events, gifService) result.gifsModule = gifs_module.newModule(result, events, gifService)
result.activityCenterModule = activity_center_module.newModule(result, events, activityCenterService, contactsService, result.activityCenterModule = activity_center_module.newModule(result, events, activityCenterService, contactsService,
messageService, chatService, communityService) messageService, chatService, communityService, devicesService)
result.communitiesModule = communities_module.newModule(result, events, communityService, contactsService, communityTokensService, result.communitiesModule = communities_module.newModule(result, events, communityService, contactsService, communityTokensService,
networkService, transactionService, tokenService, chatService, walletAccountService, keycardService) networkService, transactionService, tokenService, chatService, walletAccountService, keycardService)
result.appSearchModule = app_search_module.newModule(result, events, contactsService, chatService, communityService, result.appSearchModule = app_search_module.newModule(result, events, contactsService, chatService, communityService,
@ -468,6 +468,9 @@ proc connectForNotificationsOnly[T](self: Module[T]) =
self.view.showToastTransactionSendingComplete(args.chainId, args.transactionHash, args.data, args.success, self.view.showToastTransactionSendingComplete(args.chainId, args.transactionHash, args.data, args.success,
ord(args.txType), args.fromAddress, args.toAddress, args.fromTokenKey, args.fromAmount, args.toTokenKey, args.toAmount) ord(args.txType), args.fromAddress, args.toAddress, args.fromTokenKey, args.fromAmount, args.toTokenKey, args.toAmount)
self.events.on(SIGNAL_PAIRING_FALLBACK_COMPLETED) do(e:Args):
self.view.showToastPairingFallbackCompleted()
method load*[T]( method load*[T](
self: Module[T], self: Module[T],
events: EventEmitter, events: EventEmitter,

View File

@ -344,6 +344,7 @@ QtObject:
txType: int, fromAddr: string, toAddr: string, fromTokenKey: string, fromAmount: string, toTokenKey: string, toAmount: string) {.signal.} txType: int, fromAddr: string, toAddr: string, fromTokenKey: string, fromAmount: string, toTokenKey: string, toAmount: string) {.signal.}
proc showToastTransactionSendingComplete*(self: View, chainId: int, txHash: string, data: string, success: bool, proc showToastTransactionSendingComplete*(self: View, chainId: int, txHash: string, data: string, success: bool,
txType: int, fromAddr: string, toAddr: string, fromTokenKey: string, fromAmount: string, toTokenKey: string, toAmount: string) {.signal.} txType: int, fromAddr: string, toAddr: string, fromTokenKey: string, fromAmount: string, toTokenKey: string, toAmount: string) {.signal.}
proc showToastPairingFallbackCompleted*(self: View) {.signal.}
## Used in test env only, for testing keycard flows ## Used in test env only, for testing keycard flows
proc registerMockedKeycard*(self: View, cardIndex: int, readerState: int, keycardState: int, proc registerMockedKeycard*(self: View, cardIndex: int, readerState: int, keycardState: int,

View File

@ -137,10 +137,6 @@ proc init*(self: Controller) =
self.delegate.emitLogOut() self.delegate.emitLogOut()
self.connectionIds.add(handlerId) self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SignalType.NodeReady.event) do(e:Args):
self.events.emit("nodeReady", Args())
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args): handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
let args = KeycardLibArgs(e) let args = KeycardLibArgs(e)
self.delegate.onKeycardResponse(args.flowType, args.flowEvent) self.delegate.onKeycardResponse(args.flowType, args.flowEvent)
@ -642,3 +638,6 @@ proc notificationsNeedsEnable*(self: Controller): bool =
proc proceedToApp*(self: Controller) = proc proceedToApp*(self: Controller) =
self.delegate.finishAppLoading() self.delegate.finishAppLoading()
proc finishPairingThroughSeedPhraseProcess*(self: Controller, installationId: string) =
self.devicesService.finishPairingThroughSeedPhraseProcess(installationId)

View File

@ -10,3 +10,9 @@ proc delete*(self: SyncDeviceResultState) =
method executePrimaryCommand*(self: SyncDeviceResultState, controller: Controller) = method executePrimaryCommand*(self: SyncDeviceResultState, controller: Controller) =
controller.loginLocalPairingAccount() controller.loginLocalPairingAccount()
method getNextSecondaryState*(self: SyncDeviceResultState, controller: Controller): State =
return createState(StateType.UserProfileEnterSeedPhrase, FlowType.FirstRunOldUserImportSeedPhrase, self)
method getNextTertiaryState*(self: SyncDeviceResultState, controller: Controller): State =
return createState(StateType.SyncDeviceWithSyncCode, FlowType.FirstRunOldUserSyncCode, self)

View File

@ -325,6 +325,8 @@ method finishAppLoading*[T](self: Module[T]) =
method checkFetchingStatusAndProceed*[T](self: Module[T]) = method checkFetchingStatusAndProceed*[T](self: Module[T]) =
if self.view.fetchingDataModel().isEntityLoaded(FetchingFromWakuProfile): if self.view.fetchingDataModel().isEntityLoaded(FetchingFromWakuProfile):
if self.view.getLocalPairingInstallationId() != "":
self.controller.finishPairingThroughSeedPhraseProcess(self.view.getLocalPairingInstallationId())
self.finishAppLoading() self.finishAppLoading()
return return
let currStateObj = self.view.currentStartupStateObj() let currStateObj = self.view.currentStartupStateObj()
@ -530,6 +532,8 @@ method addToKeycardUidPairsToCheckForAChangeAfterLogin*[T](self: Module[T], oldK
method removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*[T](self: Module[T]) = method removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*[T](self: Module[T]) =
self.delegate.removeAllKeycardUidPairsForCheckingForAChangeAfterLogin() self.delegate.removeAllKeycardUidPairsForCheckingForAChangeAfterLogin()
if self.view.getLocalPairingInstallationId() != "":
self.controller.finishPairingThroughSeedPhraseProcess(self.view.getLocalPairingInstallationId())
method getConnectionString*[T](self: Module[T]): string = method getConnectionString*[T](self: Module[T]): string =
return self.controller.getConnectionString() return self.controller.getConnectionString()

View File

@ -32,6 +32,8 @@ type ActivityCenterNotificationType* {.pure.}= enum
FirstCommunityTokenReceived = 20 FirstCommunityTokenReceived = 20
CommunityBanned = 21 CommunityBanned = 21
CommunityUnbanned = 22 CommunityUnbanned = 22
NewInstallationReceived = 23
NewInstallationCreated = 24
type ActivityCenterGroup* {.pure.}= enum type ActivityCenterGroup* {.pure.}= enum
All = 0, All = 0,
@ -65,6 +67,7 @@ type ActivityCenterNotificationDto* = ref object of RootObj
verificationStatus*: VerificationStatus verificationStatus*: VerificationStatus
name*: string name*: string
author*: string author*: string
installationId*: string
notificationType*: ActivityCenterNotificationType notificationType*: ActivityCenterNotificationType
message*: MessageDto message*: MessageDto
replyMessage*: MessageDto replyMessage*: MessageDto
@ -84,6 +87,7 @@ proc `$`*(self: ActivityCenterNotificationDto): string =
membershipStatus: {self.membershipStatus}, membershipStatus: {self.membershipStatus},
contactVerificationStatus: {self.verificationStatus}, contactVerificationStatus: {self.verificationStatus},
author: {self.author}, author: {self.author},
installationId: {self.installationId},
notificationType: {$self.notificationType.int}, notificationType: {$self.notificationType.int},
timestamp: {self.timestamp}, timestamp: {self.timestamp},
read: {$self.read}, read: {$self.read},
@ -117,6 +121,7 @@ proc toActivityCenterNotificationDto*(jsonObj: JsonNode): ActivityCenterNotifica
result.verificationStatus = VerificationStatus(verificationStatusInt) result.verificationStatus = VerificationStatus(verificationStatusInt)
discard jsonObj.getProp("author", result.author) discard jsonObj.getProp("author", result.author)
discard jsonObj.getProp("installationId", result.installationId)
result.notificationType = ActivityCenterNotificationType.NoType result.notificationType = ActivityCenterNotificationType.NoType
var notificationTypeInt: int var notificationTypeInt: int
@ -179,7 +184,9 @@ proc activityCenterNotificationTypesByGroup*(group: ActivityCenterGroup) : seq[i
ActivityCenterNotificationType.CommunityTokenReceived.int, ActivityCenterNotificationType.CommunityTokenReceived.int,
ActivityCenterNotificationType.FirstCommunityTokenReceived.int, ActivityCenterNotificationType.FirstCommunityTokenReceived.int,
ActivityCenterNotificationType.CommunityBanned.int, ActivityCenterNotificationType.CommunityBanned.int,
ActivityCenterNotificationType.CommunityUnbanned.int ActivityCenterNotificationType.CommunityUnbanned.int,
ActivityCenterNotificationType.NewInstallationReceived.int,
ActivityCenterNotificationType.NewInstallationCreated.int,
] ]
of ActivityCenterGroup.Mentions: of ActivityCenterGroup.Mentions:
return @[ActivityCenterNotificationType.Mention.int] return @[ActivityCenterNotificationType.Mention.int]

View File

@ -88,7 +88,7 @@ QtObject:
result.chatService = chatService result.chatService = chatService
proc handleNewNotificationsLoaded(self: Service, activityCenterNotifications: seq[ActivityCenterNotificationDto]) = proc handleNewNotificationsLoaded(self: Service, activityCenterNotifications: seq[ActivityCenterNotificationDto]) =
# For now status-go notify about every notification update regardless active group so we need filter manulay on the desktop side # For now status-go notify about every notification update regardless active group so we need filter manually on the desktop side
let groupTypes = activityCenterNotificationTypesByGroup(self.activeGroup) let groupTypes = activityCenterNotificationTypesByGroup(self.activeGroup)
let filteredNotifications = filter(activityCenterNotifications, proc(notification: ActivityCenterNotificationDto): bool = let filteredNotifications = filter(activityCenterNotifications, proc(notification: ActivityCenterNotificationDto): bool =
return (self.readType == ActivityCenterReadType.All or not notification.read) and groupTypes.contains(notification.notificationType.int) return (self.readType == ActivityCenterReadType.All or not notification.read) and groupTypes.contains(notification.notificationType.int)

View File

@ -55,6 +55,8 @@ proc update*(self: LocalPairingStatus, data: LocalPairingEventArgs) =
self.chatKey = data.accountData.chatKey self.chatKey = data.accountData.chatKey
of EventReceivedInstallation: of EventReceivedInstallation:
self.installation = data.installation self.installation = data.installation
of EventCompletedAndNodeReady:
self.installation = data.installation
of EventReceivedKeystoreFiles: of EventReceivedKeystoreFiles:
self.transferredKeypairs = data.transferredKeypairs self.transferredKeypairs = data.transferredKeypairs
of EventConnectionError: of EventConnectionError:

View File

@ -9,6 +9,7 @@ import ./dto/local_pairing_status
import app_service/service/settings/service as settings_service import app_service/service/settings/service as settings_service
import app_service/service/accounts/service as accounts_service import app_service/service/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service import app_service/service/wallet_account/service as wallet_account_service
import ../../common/activity_center
import app/global/global_singleton import app/global/global_singleton
import app/core/[main] import app/core/[main]
@ -49,6 +50,7 @@ const SIGNAL_DEVICES_LOADED* = "devicesLoaded"
const SIGNAL_ERROR_LOADING_DEVICES* = "devicesErrorLoading" const SIGNAL_ERROR_LOADING_DEVICES* = "devicesErrorLoading"
const SIGNAL_LOCAL_PAIRING_STATUS_UPDATE* = "localPairingStatusUpdate" const SIGNAL_LOCAL_PAIRING_STATUS_UPDATE* = "localPairingStatusUpdate"
const SIGNAL_INSTALLATION_NAME_UPDATED* = "installationNameUpdated" const SIGNAL_INSTALLATION_NAME_UPDATED* = "installationNameUpdated"
const SIGNAL_PAIRING_FALLBACK_COMPLETED* = "pairingFallbackCompleted"
QtObject: QtObject:
type Service* = ref object of QObject type Service* = ref object of QObject
@ -174,14 +176,23 @@ QtObject:
# #
proc inputConnectionStringForBootstrappingFinished*(self: Service, responseJson: string) {.slot.} = proc inputConnectionStringForBootstrappingFinished*(self: Service, responseJson: string) {.slot.} =
var currentError = ""
if self.localPairingStatus.state == LocalPairingState.Error:
# The error was already returned by an event, keep it to reuse
currentError = self.localPairingStatus.error
let response = responseJson.parseJson let response = responseJson.parseJson
let errorDescription = response["error"].getStr let errorDescription = response["error"].getStr
if len(errorDescription) == 0: if len(errorDescription) == 0:
var installation = InstallationDto()
installation.id = response["installationId"].getStr # Set the installation with the ID (only info we have for now)
let data = LocalPairingEventArgs( let data = LocalPairingEventArgs(
installation: installation,
eventType: EventCompletedAndNodeReady, eventType: EventCompletedAndNodeReady,
action: ActionPairingInstallation, action: ActionPairingInstallation,
accountData: LocalPairingAccountData(), accountData: LocalPairingAccountData(),
error: "") error: currentError,
)
self.updateLocalPairingStatus(data) self.updateLocalPairingStatus(data)
return return
error "failed to start bootstrapping device", errorDescription error "failed to start bootstrapping device", errorDescription
@ -189,7 +200,8 @@ QtObject:
eventType: EventConnectionError, eventType: EventConnectionError,
action: ActionUnknown, action: ActionUnknown,
accountData: LocalPairingAccountData(), accountData: LocalPairingAccountData(),
error: errorDescription) error: errorDescription,
)
self.updateLocalPairingStatus(data) self.updateLocalPairingStatus(data)
proc validateConnectionString*(self: Service, connectionString: string): string = proc validateConnectionString*(self: Service, connectionString: string): string =
@ -360,3 +372,24 @@ QtObject:
configJSON: $configJSON configJSON: $configJSON
) )
self.threadpool.start(arg) self.threadpool.start(arg)
proc finishPairingThroughSeedPhraseProcess*(self: Service, installationId: string) =
try:
let response = status_installations.finishPairingThroughSeedPhraseProcess(installationId)
if response.error != nil:
let e = Json.decode($response.error, RpcError)
raise newException(CatchableError, e.message)
except Exception as e:
error "error: ", desription = e.msg
proc enableInstallationAndSync*(self: Service, installationId: string) =
try:
let response = status_installations.enableInstallationAndSync(installationId)
if response.error != nil:
let e = Json.decode($response.error, RpcError)
raise newException(CatchableError, e.message)
# Parse AC notif
checkAndEmitACNotificationsFromResponse(self.events, response.result{"activityCenterNotifications"})
self.events.emit(SIGNAL_PAIRING_FALLBACK_COMPLETED, Args())
except Exception as e:
error "error: ", desription = e.msg

View File

@ -35,3 +35,15 @@ proc enableInstallation*(installationId: string): RpcResponse[JsonNode] =
proc disableInstallation*(installationId: string): RpcResponse[JsonNode] = proc disableInstallation*(installationId: string): RpcResponse[JsonNode] =
let payload = %* [installationId] let payload = %* [installationId]
result = callPrivateRPC("disableInstallation".prefix, payload) result = callPrivateRPC("disableInstallation".prefix, payload)
proc finishPairingThroughSeedPhraseProcess*(installationId: string): RpcResponse[JsonNode] =
let payload = %* [{
"installationId": installationId,
}]
result = callPrivateRPC("enableInstallationAndPair".prefix, payload)
proc enableInstallationAndSync*(installationId: string): RpcResponse[JsonNode] =
let payload = %* [{
"installationId": installationId,
}]
result = callPrivateRPC("enableInstallationAndSync".prefix, payload)

View File

@ -0,0 +1,78 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.settings 1.0
import mainui.activitycenter.views 1.0
import mainui.activitycenter.stores 1.0
import Storybook 1.0
SplitView {
id: root
orientation: Qt.Vertical
Logs { id: logs }
QtObject {
id: notificationMock
property string id: "1"
property string communityId: "1"
property string sectionId: "1"
property int notificationType: 1
property int timestamp: Date.now()
property int previousTimestamp: 0
property bool read: false
property bool dismissed: false
property bool accepted: false
}
Item {
SplitView.fillHeight: true
SplitView.fillWidth: true
ActivityNotificationNewDevice {
id: notification
anchors.centerIn: parent
width: parent.width - 50
height: implicitHeight
type: ActivityNotificationNewDevice.InstallationType.Received
accountName: "bob.eth"
store: undefined
notification: notificationMock
onMoreDetailsClicked: logs.logEvent("ActivityNotificationNewDevice::onMoreDetailsClicked")
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 160
logsView.logText: logs.logText
Column {
Row {
RadioButton {
text: "Received"
checked: true
onCheckedChanged: if(checked) notification.type = ActivityNotificationNewDevice.InstallationType.Received
}
RadioButton {
text: "Created"
onCheckedChanged: if(checked) notification.type = ActivityNotificationNewDevice.InstallationType.Created
}
}
}
}
}
// category: Activity Center
// https://www.figma.com/design/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=40765-355811&m=dev

View File

@ -304,6 +304,8 @@ QtObject {
readonly property int loginType: getLoginType() readonly property int loginType: getLoginType()
property string name: userProfileInst.name
property StickersStore stickersStore: StickersStore { property StickersStore stickersStore: StickersStore {
stickersModule: stickersModuleInst stickersModule: stickersModuleInst
} }

View File

@ -22,13 +22,14 @@ Item {
QtObject { QtObject {
id: d id: d
readonly property bool finished: startupStore.localPairingState === Constants.LocalPairingState.Finished readonly property bool finished: startupStore.localPairingState === Constants.LocalPairingState.Finished
readonly property bool pairingFailed: startupStore.localPairingState === Constants.LocalPairingState.Error
} }
ColumnLayout { ColumnLayout {
id: layout id: layout
anchors.centerIn: parent anchors.centerIn: parent
spacing: 48 spacing: 24
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
@ -55,6 +56,20 @@ Item {
installationDeviceType: startupStore.localPairingInstallationDeviceType installationDeviceType: startupStore.localPairingInstallationDeviceType
} }
StatusButton {
visible: d.pairingFailed
Layout.alignment: Qt.AlignHCenter
text: qsTr("Use recovery phrase")
onClicked: root.startupStore.doSecondaryAction()
}
StatusFlatButton {
visible: d.pairingFailed
Layout.alignment: Qt.AlignHCenter
text: qsTr("Try again")
onClicked: root.startupStore.doTertiaryAction()
}
StatusButton { StatusButton {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("Sign in") text: qsTr("Sign in")

View File

@ -370,6 +370,17 @@ Item {
"" ""
) )
} }
function onShowToastPairingFallbackCompleted() {
Global.displayToastMessage(
qsTr("Device paired"),
qsTr("Sync in process. Keep device powered and app open."),
"checkmark-circle",
false,
Constants.ephemeralNotificationType.success,
""
)
}
} }
QtObject { QtObject {

View File

@ -1,11 +1,14 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15 import QtGraphicalEffects 1.15
import QtQml.Models 2.15
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Backpressure 0.1 import StatusQ.Core.Backpressure 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import shared 1.0 import shared 1.0
import shared.popups 1.0 import shared.popups 1.0
@ -149,6 +152,9 @@ Popup {
return communityUnbannedNotificationComponent return communityUnbannedNotificationComponent
case ActivityCenterStore.ActivityCenterNotificationType.NewPrivateGroupChat: case ActivityCenterStore.ActivityCenterNotificationType.NewPrivateGroupChat:
return groupChatInvitationNotificationComponent return groupChatInvitationNotificationComponent
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationReceived:
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationCreated:
return newDeviceDetectedComponent
default: default:
return null return null
} }
@ -316,6 +322,36 @@ Popup {
} }
} }
Component {
id: newDeviceDetectedComponent
ActivityNotificationNewDevice {
type: setType(notification)
filteredIndex: parent.filteredIndex
notification: parent.notification
accountName: store.name
store: root.store
activityCenterStore: root.activityCenterStore
onCloseActivityCenter: root.close()
onMoreDetailsClicked: {
switch (type) {
case ActivityNotificationNewDevice.InstallationType.Received:
Global.openPopup(pairDeviceDialog, {
name: store.name,
deviceId: notification.installationId
});
break;
case ActivityNotificationNewDevice.InstallationType.Created:
Global.openPopup(checkOtherDeviceDialog, {
deviceId: notification.installationId
});
break;
}
}
}
}
Component { Component {
id: communityTokenReceivedComponent id: communityTokenReceivedComponent
@ -369,4 +405,94 @@ Popup {
onCloseActivityCenter: root.close() onCloseActivityCenter: root.close()
} }
} }
function truncateDeviceId(deviceId) {
return deviceId.substring(0, 7).toUpperCase()
}
Component {
id: pairDeviceDialog
StatusDialog {
property string name
property string deviceId
width: 480
closePolicy: Popup.CloseOnPressOutside
destroyOnClose: true
title: qsTr("Pair new device and sync profile")
contentItem: ColumnLayout {
spacing: 16
StatusBaseText {
Layout.fillWidth: true
text: qsTr("New device with %1 profile has been detected. You can see the device ID below and on your other device. Only confirm the request if the device ID matches.")
.arg(name)
wrapMode: Text.WordWrap
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 27
font.weight: Font.Medium
font.letterSpacing: 5
text: truncateDeviceId(deviceId)
}
}
footer: StatusDialogFooter {
leftButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Cancel")
onClicked: {
close()
}
}
}
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Pair and Sync")
onClicked: {
activityCenterStore.enableInstallationAndSync(deviceId)
close()
}
}
}
}
}
}
Component {
id: checkOtherDeviceDialog
StatusDialog {
property string deviceId
width: 480
closePolicy: Popup.CloseOnPressOutside
destroyOnClose: true
title: qsTr("Pair this device and sync profile")
contentItem: ColumnLayout {
spacing: 16
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Check your other device for a pairing request. Ensure that the this device ID displayed on your other device. Only proceed with pairing and syncing if the IDs are identical.")
wrapMode: Text.WordWrap
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 27
font.weight: Font.Medium
font.letterSpacing: 5
text: truncateDeviceId(deviceId)
}
Item {
Layout.fillWidth: true
}
}
footer: null
}
}
} }

View File

@ -40,7 +40,9 @@ QtObject {
CommunityTokenReceived = 19, CommunityTokenReceived = 19,
FirstCommunityTokenReceived = 20, FirstCommunityTokenReceived = 20,
CommunityBanned = 21, CommunityBanned = 21,
CommunityUnbanned = 22 CommunityUnbanned = 22,
NewInstallationReceived = 23,
NewInstallationCreated = 24
} }
enum ActivityCenterReadType { enum ActivityCenterReadType {
@ -118,4 +120,8 @@ QtObject {
function dismissActivityCenterNotification(notification) { function dismissActivityCenterNotification(notification) {
root.activityCenterModuleInst.dismissActivityCenterNotification(notification.id) root.activityCenterModuleInst.dismissActivityCenterNotification(notification.id)
} }
function enableInstallationAndSync(installationId) {
root.activityCenterModuleInst.enableInstallationAndSync(installationId)
}
} }

View File

@ -0,0 +1,127 @@
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 StatusQ.Components 0.1
import shared 1.0
import shared.panels 1.0
import utils 1.0
import mainui.activitycenter.stores 1.0
ActivityNotificationBase {
id: root
required property string accountName
required property int type // Possible values [InstallationType]
signal moreDetailsClicked
function setType(notification) {
if (notification) {
switch (notification.notificationType) {
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationReceived:
return ActivityNotificationNewDevice.InstallationType.Received
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationCreated:
return ActivityNotificationNewDevice.InstallationType.Created
}
}
return ActivityNotificationNewDevice.InstallationType.Unknown
}
enum InstallationType {
Unknown,
Received,
Created
}
QtObject {
id: d
property string title: ""
property string info: ""
property string assetColor: Theme.palette.primaryColor1
property string assetName: d.desktopAssetName
property string assetBgColor: Theme.palette.primaryColor3
property string ctaText: qsTr("More details")
readonly property string desktopAssetName: "desktop"
}
bodyComponent: RowLayout {
spacing: 8
StatusSmartIdenticon {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignTop
Layout.leftMargin: Style.current.padding
Layout.topMargin: 2
asset {
width: 24
height: width
name: d.assetName
color: d.assetColor
bgWidth: 40
bgHeight: 40
bgColor: d.assetBgColor
}
}
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
StatusMessageHeader {
Layout.fillWidth: true
displayNameLabel.text: d.title
timestamp: root.notification.timestamp
}
RowLayout {
spacing: Style.current.padding
StatusBaseText {
Layout.fillWidth: true
text: d.info
font.italic: true
wrapMode: Text.WordWrap
color: Theme.palette.baseColor1
}
}
}
}
ctaComponent: StatusFlatButton {
size: StatusBaseButton.Size.Small
text: d.ctaText
onClicked: {
root.moreDetailsClicked()
}
}
states: [
State {
when: root.type === ActivityNotificationNewDevice.InstallationType.Received
PropertyChanges {
target: d
title: qsTr("New device detected")
info: qsTr("New device with %1 profile has been detected.").arg(accountName)
}
},
State {
when: root.type === ActivityNotificationNewDevice.InstallationType.Created
PropertyChanges {
target: d
title: qsTr("Sync your profile")
info: qsTr("Check your other device for a pairing request.")
}
}
]
}

View File

@ -2,3 +2,4 @@ ActivityNotificationCommunityMembershipRequest 1.0 ActivityNotificationCommunity
ActivityNotificationTransferOwnership 1.0 ActivityNotificationTransferOwnership.qml ActivityNotificationTransferOwnership 1.0 ActivityNotificationTransferOwnership.qml
ActivityNotificationCommunityShareAddresses 1.0 ActivityNotificationCommunityShareAddresses.qml ActivityNotificationCommunityShareAddresses 1.0 ActivityNotificationCommunityShareAddresses.qml
ActivityNotificationCommunityTokenReceived 1.0 ActivityNotificationCommunityTokenReceived.qml ActivityNotificationCommunityTokenReceived 1.0 ActivityNotificationCommunityTokenReceived.qml
ActivityNotificationNewDevice 1.0 ActivityNotificationNewDevice.qml

View File

@ -27,6 +27,18 @@ Rectangle {
property bool detailsVisible: false property bool detailsVisible: false
} }
CopyButton {
width: 20
height: 20
visible: d.detailsVisible
color: Theme.palette.baseColor1
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 8
anchors.rightMargin: 8
textToCopy: root.details
}
ColumnLayout { ColumnLayout {
id: layout id: layout