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/community/service as community_service
import ../../../../app_service/service/chat/service as chat_service
import ../../../../app_service/service/devices/service as devices_service
type
Controller* = ref object of RootObj
@ -17,6 +18,7 @@ type
messageService: message_service.Service
chatService: chat_service.Service
communityService: community_service.Service
devicesService: devices_service.Service
proc newController*(
delegate: io_interface.AccessInterface,
@ -26,6 +28,7 @@ proc newController*(
messageService: message_service.Service,
chatService: chat_service.Service,
communityService: community_service.Service,
devicesService: devices_service.Service,
): Controller =
result = Controller()
result.delegate = delegate
@ -35,6 +38,7 @@ proc newController*(
result.messageService = messageService
result.chatService = chatService
result.communityService = communityService
result.devicesService = devicesService
proc delete*(self: Controller) =
discard
@ -162,3 +166,6 @@ proc setActivityCenterReadType*(self: Controller, readType: ActivityCenterReadTy
proc getActivityCenterReadType*(self: Controller): ActivityCenterReadType =
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.} =
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
chatType: ChatType
tokenDataItem: TokenDataItem
installationId: string
proc initItem*(
id: string,
@ -43,7 +44,8 @@ proc initItem*(
messageItem: MessageItem,
repliedMessageItem: MessageItem,
chatType: ChatType,
tokenDataItem: TokenDataItem
tokenDataItem: TokenDataItem,
installationId: string
): Item =
result = Item()
result.id = id
@ -63,6 +65,7 @@ proc initItem*(
result.repliedMessageItem = repliedMessageItem
result.chatType = chatType
result.tokenDataItem = tokenDataItem
result.installationId = installationId
proc `$`*(self: Item): string =
result = fmt"""activity_center/Item(
@ -74,6 +77,7 @@ proc `$`*(self: Item): string =
verificationStatus: {$self.verificationStatus.int},
sectionId: {$self.sectionId},
author: {$self.author},
installationId: {$self.installationId},
notificationType: {$self.notificationType.int},
timestamp: {$self.timestamp},
read: {$self.read},
@ -93,6 +97,9 @@ proc name*(self: Item): string =
proc author*(self: Item): string =
return self.author
proc installationId*(self: Item): string =
return self.installationId
proc chatId*(self: Item): string =
return self.chatId

View File

@ -21,6 +21,7 @@ type
RepliedMessage
ChatType
TokenData
InstallationId
QtObject:
type
@ -89,6 +90,7 @@ QtObject:
of NotifRoles.RepliedMessage: result = newQVariant(activityNotificationItem.repliedMessageItem)
of NotifRoles.ChatType: result = newQVariant(activityNotificationItem.chatType.int)
of NotifRoles.TokenData: result = newQVariant(activityNotificationItem.tokenDataItem)
of NotifRoles.InstallationId: result = newQVariant(activityNotificationItem.installationId)
method roleNames(self: Model): Table[int, string] =
{
@ -109,7 +111,8 @@ QtObject:
NotifRoles.Accepted.int: "accepted",
NotifRoles.RepliedMessage.int: "repliedMessage",
NotifRoles.ChatType.int: "chatType",
NotifRoles.TokenData.int: "tokenData"
NotifRoles.TokenData.int: "tokenData",
NotifRoles.InstallationId.int: "installationId",
}.toTable
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/chat/service as chat_service
import ../../../../app_service/service/community/service as community_service
import ../../../../app_service/service/devices/service as devices_service
export io_interface
@ -33,7 +34,8 @@ proc newModule*(
contactsService: contacts_service.Service,
messageService: message_service.Service,
chatService: chat_service.Service,
communityService: community_service.Service
communityService: community_service.Service,
devicesService: devices_service.Service,
): Module =
result = Module()
result.delegate = delegate
@ -46,7 +48,8 @@ proc newModule*(
contactsService,
messageService,
chatService,
communityService
communityService,
devicesService,
)
result.moduleLoaded = false
@ -216,7 +219,8 @@ method convertToItems*(
messageItem,
repliedMessageItem,
chatDetails.chatType,
tokenDataItem
tokenDataItem,
notification.installationId,
)
)
@ -323,3 +327,6 @@ method getActivityCenterReadType*(self: Module): int =
method setActivityGroupCounters*(self: Module, counters: Table[ActivityCenterGroup, int]) =
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]) =
self.groupCounters = counters
self.groupCountersChanged()
proc enableInstallationAndSync*(self: View, installationId: string) {.slot.} =
self.delegate.enableInstallationAndSync(installationId)

View File

@ -225,7 +225,7 @@ proc newModule*[T](
networkService, tokenService)
result.gifsModule = gifs_module.newModule(result, events, gifService)
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,
networkService, transactionService, tokenService, chatService, walletAccountService, keycardService)
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,
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](
self: Module[T],
events: EventEmitter,

View File

@ -344,6 +344,7 @@ QtObject:
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,
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
proc registerMockedKeycard*(self: View, cardIndex: int, readerState: int, keycardState: int,

View File

@ -137,10 +137,6 @@ proc init*(self: Controller) =
self.delegate.emitLogOut()
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):
let args = KeycardLibArgs(e)
self.delegate.onKeycardResponse(args.flowType, args.flowEvent)
@ -642,3 +638,6 @@ proc notificationsNeedsEnable*(self: Controller): bool =
proc proceedToApp*(self: Controller) =
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) =
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]) =
if self.view.fetchingDataModel().isEntityLoaded(FetchingFromWakuProfile):
if self.view.getLocalPairingInstallationId() != "":
self.controller.finishPairingThroughSeedPhraseProcess(self.view.getLocalPairingInstallationId())
self.finishAppLoading()
return
let currStateObj = self.view.currentStartupStateObj()
@ -530,6 +532,8 @@ method addToKeycardUidPairsToCheckForAChangeAfterLogin*[T](self: Module[T], oldK
method removeAllKeycardUidPairsForCheckingForAChangeAfterLogin*[T](self: Module[T]) =
self.delegate.removeAllKeycardUidPairsForCheckingForAChangeAfterLogin()
if self.view.getLocalPairingInstallationId() != "":
self.controller.finishPairingThroughSeedPhraseProcess(self.view.getLocalPairingInstallationId())
method getConnectionString*[T](self: Module[T]): string =
return self.controller.getConnectionString()

View File

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

View File

@ -88,7 +88,7 @@ QtObject:
result.chatService = chatService
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 filteredNotifications = filter(activityCenterNotifications, proc(notification: ActivityCenterNotificationDto): bool =
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
of EventReceivedInstallation:
self.installation = data.installation
of EventCompletedAndNodeReady:
self.installation = data.installation
of EventReceivedKeystoreFiles:
self.transferredKeypairs = data.transferredKeypairs
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/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service
import ../../common/activity_center
import app/global/global_singleton
import app/core/[main]
@ -49,6 +50,7 @@ const SIGNAL_DEVICES_LOADED* = "devicesLoaded"
const SIGNAL_ERROR_LOADING_DEVICES* = "devicesErrorLoading"
const SIGNAL_LOCAL_PAIRING_STATUS_UPDATE* = "localPairingStatusUpdate"
const SIGNAL_INSTALLATION_NAME_UPDATED* = "installationNameUpdated"
const SIGNAL_PAIRING_FALLBACK_COMPLETED* = "pairingFallbackCompleted"
QtObject:
type Service* = ref object of QObject
@ -174,14 +176,23 @@ QtObject:
#
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 errorDescription = response["error"].getStr
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(
installation: installation,
eventType: EventCompletedAndNodeReady,
action: ActionPairingInstallation,
accountData: LocalPairingAccountData(),
error: "")
error: currentError,
)
self.updateLocalPairingStatus(data)
return
error "failed to start bootstrapping device", errorDescription
@ -189,7 +200,8 @@ QtObject:
eventType: EventConnectionError,
action: ActionUnknown,
accountData: LocalPairingAccountData(),
error: errorDescription)
error: errorDescription,
)
self.updateLocalPairingStatus(data)
proc validateConnectionString*(self: Service, connectionString: string): string =
@ -360,3 +372,24 @@ QtObject:
configJSON: $configJSON
)
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] =
let payload = %* [installationId]
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()
property string name: userProfileInst.name
property StickersStore stickersStore: StickersStore {
stickersModule: stickersModuleInst
}

View File

@ -22,13 +22,14 @@ Item {
QtObject {
id: d
readonly property bool finished: startupStore.localPairingState === Constants.LocalPairingState.Finished
readonly property bool pairingFailed: startupStore.localPairingState === Constants.LocalPairingState.Error
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: 48
spacing: 24
StatusBaseText {
Layout.fillWidth: true
@ -55,6 +56,20 @@ Item {
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 {
Layout.alignment: Qt.AlignHCenter
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 {

View File

@ -1,11 +1,14 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import QtQml.Models 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Backpressure 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import shared 1.0
import shared.popups 1.0
@ -149,6 +152,9 @@ Popup {
return communityUnbannedNotificationComponent
case ActivityCenterStore.ActivityCenterNotificationType.NewPrivateGroupChat:
return groupChatInvitationNotificationComponent
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationReceived:
case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationCreated:
return newDeviceDetectedComponent
default:
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 {
id: communityTokenReceivedComponent
@ -369,4 +405,94 @@ Popup {
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,
FirstCommunityTokenReceived = 20,
CommunityBanned = 21,
CommunityUnbanned = 22
CommunityUnbanned = 22,
NewInstallationReceived = 23,
NewInstallationCreated = 24
}
enum ActivityCenterReadType {
@ -118,4 +120,8 @@ QtObject {
function dismissActivityCenterNotification(notification) {
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
ActivityNotificationCommunityShareAddresses 1.0 ActivityNotificationCommunityShareAddresses.qml
ActivityNotificationCommunityTokenReceived 1.0 ActivityNotificationCommunityTokenReceived.qml
ActivityNotificationNewDevice 1.0 ActivityNotificationNewDevice.qml

View File

@ -27,6 +27,18 @@ Rectangle {
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 {
id: layout