fix(communities): deltas between designs and build for join token gated community flow

This commit:
- improves selection of addresses to reveal
- keeps the selection state for the popup lifetime
- brings higher granularity in terms of signed requests by keypairs
- meets new requirements from the latest related Figma
- merges edit shared addresses feature and request to join community features
into a single component, cause the flow is logically the same, with the only
difference that when editing revealed addresses we don't show the community
intro screen

Fixes at least points 3 and 4 from #13988
This commit is contained in:
Sale Djenic 2024-03-19 09:41:41 +01:00 committed by saledjenic
parent ec4e2f3fc8
commit 10a8469b9e
21 changed files with 940 additions and 351 deletions

View File

@ -224,7 +224,7 @@ method prepareKeypairsForSigning*(self: AccessInterface, communityId: string, en
airdropAddress: string, editMode: bool) {.base.} = airdropAddress: string, editMode: bool) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method signSharedAddressesForAllNonKeycardKeypairs*(self: AccessInterface) {.base.} = method signProfileKeypairAndAllNonKeycardKeypairs*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method signSharedAddressesForKeypair*(self: AccessInterface, keyUid: string, pin: string) {.base.} = method signSharedAddressesForKeypair*(self: AccessInterface, keyUid: string, pin: string) {.base.} =

View File

@ -675,7 +675,7 @@ method shareCommunityChannelUrlWithChatKey*(self: Module, communityId: string, c
method shareCommunityChannelUrlWithData*(self: Module, communityId: string, chatId: string): string = method shareCommunityChannelUrlWithData*(self: Module, communityId: string, chatId: string): string =
return self.controller.shareCommunityChannelUrlWithData(communityId, chatId) return self.controller.shareCommunityChannelUrlWithData(communityId, chatId)
proc signRevealedAddressesThatBelongToRegularKeypairs(self: Module): bool = proc signRevealedAddressesForNonKeycardKeypairs(self: Module): bool =
var signingParams: seq[SignParamsDto] var signingParams: seq[SignParamsDto]
for address, details in self.joiningCommunityDetails.addressesToShare.pairs: for address, details in self.joiningCommunityDetails.addressesToShare.pairs:
if details.signature.len > 0: if details.signature.len > 0:
@ -702,8 +702,24 @@ proc signRevealedAddressesThatBelongToRegularKeypairs(self: Module): bool =
let signatures = self.controller.signCommunityRequests(self.joiningCommunityDetails.communityId, signingParams) let signatures = self.controller.signCommunityRequests(self.joiningCommunityDetails.communityId, signingParams)
for i in 0 ..< len(signingParams): for i in 0 ..< len(signingParams):
self.joiningCommunityDetails.addressesToShare[signingParams[i].address].signature = signatures[i] self.joiningCommunityDetails.addressesToShare[signingParams[i].address].signature = signatures[i]
self.view.keypairsSigningModel().setOwnershipVerified(self.joiningCommunityDetails.addressesToShare[signingParams[i].address].keyUid, true)
return true return true
proc signRevealedAddressesForNonKeycardKeypairsAndEmitSignal(self: Module) =
if self.signRevealedAddressesForNonKeycardKeypairs() and self.joiningCommunityDetails.allSigned():
self.view.sendAllSharedAddressesSignedSignal()
proc anyProfileKeyPairAddressSelectedToBeRevealed(self: Module): bool =
let profileKeypair = self.controller.getKeypairByKeyUid(singletonInstance.userProfile.getKeyUid())
if profileKeypair.isNil:
error "profile keypair not found"
return false
for acc in profileKeypair.accounts:
for addrToReveal in self.joiningCommunityDetails.addressesToShare.keys:
if cmpIgnoreCase(addrToReveal, acc.address) == 0:
return true
return false
method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid: string) = method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid: string) =
if password == "" and pin == "": if password == "" and pin == "":
info "unsuccesful authentication" info "unsuccesful authentication"
@ -712,8 +728,16 @@ method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid:
self.joiningCommunityDetails.profilePassword = password self.joiningCommunityDetails.profilePassword = password
self.joiningCommunityDetails.profilePin = pin self.joiningCommunityDetails.profilePin = pin
if self.signRevealedAddressesThatBelongToRegularKeypairs():
self.view.sendSharedAddressesForAllNonKeycardKeypairsSignedSignal() # If any profile keypair address selected to be revealed and if the profile is a keycard user, we need to sign the request
# for revealed profile addresses first, then using pubic encryption key to sign other non keycard key pairs.
# If the profile is not a keycard user, we sign the request for it calling `signRevealedAddressesForNonKeycardKeypairs` function.
if keyUid == singletonInstance.userProfile.getKeyUid() and
singletonInstance.userProfile.getIsKeycardUser() and
self.anyProfileKeyPairAddressSelectedToBeRevealed():
self.signSharedAddressesForKeypair(keyUid, pin)
return
self.signRevealedAddressesForNonKeycardKeypairsAndEmitSignal()
method onDataSigned*(self: Module, keyUid: string, path: string, r: string, s: string, v: string, pin: string) = method onDataSigned*(self: Module, keyUid: string, path: string, r: string, s: string, v: string, pin: string) =
if keyUid.len == 0 or path.len == 0 or r.len == 0 or s.len == 0 or v.len == 0 or pin.len == 0: if keyUid.len == 0 or path.len == 0 or r.len == 0 or s.len == 0 or v.len == 0 or pin.len == 0:
@ -727,6 +751,11 @@ method onDataSigned*(self: Module, keyUid: string, path: string, r: string, s: s
break break
self.signSharedAddressesForKeypair(keyUid, pin) self.signSharedAddressesForKeypair(keyUid, pin)
# Only if the signed request is for the profile revealed addresses, we need to try to sign other revealed addresses
# for non profile key pairs. If they are already signed or moved to keycard we skip them (handled in signRevealedAddressesForNonKeycardKeypairsAndEmitSignal)
if keyUid == singletonInstance.userProfile.getKeyUid():
self.signRevealedAddressesForNonKeycardKeypairsAndEmitSignal()
method prepareKeypairsForSigning*(self: Module, communityId, ensName: string, addresses: string, method prepareKeypairsForSigning*(self: Module, communityId, ensName: string, addresses: string,
airdropAddress: string, editMode: bool) = airdropAddress: string, editMode: bool) =
var addressesToShare: seq[string] var addressesToShare: seq[string]
@ -778,7 +807,7 @@ method prepareKeypairsForSigning*(self: Module, communityId, ensName: string, ad
) )
self.joiningCommunityDetails.addressesToShare[param.address] = details self.joiningCommunityDetails.addressesToShare[param.address] = details
method signSharedAddressesForAllNonKeycardKeypairs*(self: Module) = method signProfileKeypairAndAllNonKeycardKeypairs*(self: Module) =
self.controller.authenticate() self.controller.authenticate()
# if pin is provided we're signing on a keycard silently # if pin is provided we're signing on a keycard silently
@ -796,6 +825,8 @@ method signSharedAddressesForKeypair*(self: Module, keyUid: string, pin: string)
self.controller.runSigningOnKeycard(keyUid, details.path, details.messageToBeSigned, pin) self.controller.runSigningOnKeycard(keyUid, details.path, details.messageToBeSigned, pin)
return return
self.view.keypairsSigningModel().setOwnershipVerified(keyUid, true) self.view.keypairsSigningModel().setOwnershipVerified(keyUid, true)
if self.joiningCommunityDetails.allSigned():
self.view.sendAllSharedAddressesSignedSignal()
method joinCommunityOrEditSharedAddresses*(self: Module) = method joinCommunityOrEditSharedAddresses*(self: Module) =
if not self.joiningCommunityDetails.allSigned(): if not self.joiningCommunityDetails.allSigned():

View File

@ -339,8 +339,8 @@ QtObject:
proc prepareTokenModelForCommunity(self: View, communityId: string) {.slot.} = proc prepareTokenModelForCommunity(self: View, communityId: string) {.slot.} =
self.delegate.prepareTokenModelForCommunity(communityId) self.delegate.prepareTokenModelForCommunity(communityId)
proc signSharedAddressesForAllNonKeycardKeypairs*(self: View) {.slot.} = proc signProfileKeypairAndAllNonKeycardKeypairs*(self: View) {.slot.} =
self.delegate.signSharedAddressesForAllNonKeycardKeypairs() self.delegate.signProfileKeypairAndAllNonKeycardKeypairs()
proc signSharedAddressesForKeypair*(self: View, keyUid: string) {.slot.} = proc signSharedAddressesForKeypair*(self: View, keyUid: string) {.slot.} =
self.delegate.signSharedAddressesForKeypair(keyUid, pin = "") self.delegate.signSharedAddressesForKeypair(keyUid, pin = "")
@ -815,9 +815,9 @@ QtObject:
self.keypairsSigningModel.setItems(items) self.keypairsSigningModel.setItems(items)
self.keypairsSigningModelChanged() self.keypairsSigningModelChanged()
proc sharedAddressesForAllNonKeycardKeypairsSigned(self: View) {.signal.} proc allSharedAddressesSigned*(self: View) {.signal.}
proc sendSharedAddressesForAllNonKeycardKeypairsSignedSignal*(self: View) = proc sendAllSharedAddressesSignedSignal*(self: View) =
self.sharedAddressesForAllNonKeycardKeypairsSigned() self.allSharedAddressesSigned()
proc promoteSelfToControlNode*(self: View, communityId: string) {.slot.} = proc promoteSelfToControlNode*(self: View, communityId: string) {.slot.} =
self.delegate.promoteSelfToControlNode(communityId) self.delegate.promoteSelfToControlNode(communityId)

View File

@ -120,3 +120,6 @@ method getRpcStats*(self: AccessInterface): string {.base.} =
method resetRpcStats*(self: AccessInterface) {.base.} = method resetRpcStats*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method canProfileProveOwnershipOfProvidedAddresses*(self: AccessInterface, addresses: string): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,4 +1,4 @@
import NimQml, chronicles, sequtils, strutils, sugar import NimQml, json, chronicles, sequtils, strutils, sugar
import ./controller, ./view, ./filter import ./controller, ./view, ./filter
import ./io_interface as io_interface import ./io_interface as io_interface
@ -471,3 +471,21 @@ method getRpcStats*(self: Module): string =
method resetRpcStats*(self: Module) = method resetRpcStats*(self: Module) =
self.view.resetRpcStats() self.view.resetRpcStats()
method canProfileProveOwnershipOfProvidedAddresses*(self: Module, addresses: string): bool =
var addressesForProvingOwnership: seq[string]
try:
addressesForProvingOwnership = map(parseJson(addresses).getElems(), proc(x:JsonNode):string = x.getStr())
except Exception as e:
error "Failed to parse addresses for proving ownership: ", msg=e.msg
return false
for address in addressesForProvingOwnership:
let keypair = self.controller.getKeypairByAccountAddress(address)
if keypair.isNil:
return false
if keypair.keyUid == singletonInstance.userProfile.getKeyUid():
continue
if keypair.migratedToKeycard():
return false
return true

View File

@ -10,6 +10,7 @@ QtObject:
canSend: bool canSend: bool
proc setup*(self: AccountItem, proc setup*(self: AccountItem,
keyUid: string,
name: string, name: string,
address: string, address: string,
colorId: string, colorId: string,
@ -29,7 +30,7 @@ QtObject:
emoji, emoji,
walletType, walletType,
path = "", path = "",
keyUid = "", keyUid = keyUid,
keycardAccount = false, keycardAccount = false,
position, position,
operability = wa_dto.AccountFullyOperable, operability = wa_dto.AccountFullyOperable,
@ -43,6 +44,7 @@ QtObject:
self.QObject.delete self.QObject.delete
proc newAccountItem*( proc newAccountItem*(
keyUid: string = "",
name: string = "", name: string = "",
address: string = "", address: string = "",
colorId: string = "", colorId: string = "",
@ -56,7 +58,7 @@ QtObject:
canSend: bool = true, canSend: bool = true,
): AccountItem = ): AccountItem =
new(result, delete) new(result, delete)
result.setup(name, address, colorId, emoji, walletType, currencyBalance, position, areTestNetworksEnabled, prodPreferredChainIds, testPreferredChainIds, canSend) result.setup(keyUid, name, address, colorId, emoji, walletType, currencyBalance, position, areTestNetworksEnabled, prodPreferredChainIds, testPreferredChainIds, canSend)
proc `$`*(self: AccountItem): string = proc `$`*(self: AccountItem): string =
result = "WalletSection-Send-Item(" result = "WalletSection-Send-Item("

View File

@ -5,13 +5,14 @@ import ../../../shared_models/currency_amount
type type
ModelRole {.pure.} = enum ModelRole {.pure.} = enum
Name = UserRole + 1, KeyUid = UserRole + 1
Address, Name
ColorId, Address
WalletType, ColorId
Emoji, WalletType
CurrencyBalance, Emoji
Position, CurrencyBalance
Position
PreferredSharingChainIds PreferredSharingChainIds
QtObject: QtObject:
@ -48,6 +49,7 @@ QtObject:
method roleNames(self: AccountsModel): Table[int, string] = method roleNames(self: AccountsModel): Table[int, string] =
{ {
ModelRole.KeyUid.int: "keyUid",
ModelRole.Name.int:"name", ModelRole.Name.int:"name",
ModelRole.Address.int:"address", ModelRole.Address.int:"address",
ModelRole.ColorId.int:"colorId", ModelRole.ColorId.int:"colorId",
@ -75,6 +77,8 @@ QtObject:
let enumRole = role.ModelRole let enumRole = role.ModelRole
case enumRole: case enumRole:
of ModelRole.KeyUid:
result = newQVariant(item.keyUid())
of ModelRole.Name: of ModelRole.Name:
result = newQVariant(item.name()) result = newQVariant(item.name())
of ModelRole.Address: of ModelRole.Address:

View File

@ -7,7 +7,7 @@ import ./io_interface
import ../../shared_models/currency_amount import ../../shared_models/currency_amount
import ./wallet_connect/controller as wcc import ./wallet_connect/controller as wcc
type type
ActivityControllerArray* = array[2, activityc.Controller] ActivityControllerArray* = array[2, activityc.Controller]
QtObject: QtObject:
@ -36,11 +36,11 @@ QtObject:
proc delete*(self: View) = proc delete*(self: View) =
self.QObject.delete self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface, proc newView*(delegate: io_interface.AccessInterface,
activityController: activityc.Controller, activityController: activityc.Controller,
tmpActivityControllers: ActivityControllerArray, tmpActivityControllers: ActivityControllerArray,
activityDetailsController: activity_detailsc.Controller, activityDetailsController: activity_detailsc.Controller,
collectibleDetailsController: collectible_detailsc.Controller, collectibleDetailsController: collectible_detailsc.Controller,
wcController: wcc.Controller): View = wcController: wcc.Controller): View =
new(result, delete) new(result, delete)
result.delegate = delegate result.delegate = delegate
@ -259,3 +259,6 @@ QtObject:
return self.delegate.getRpcStats() return self.delegate.getRpcStats()
proc resetRpcStats*(self: View) {.slot.} = proc resetRpcStats*(self: View) {.slot.} =
self.delegate.resetRpcStats() self.delegate.resetRpcStats()
proc canProfileProveOwnershipOfProvidedAddresses*(self: View, addresses: string): bool {.slot.} =
return self.delegate.canProfileProveOwnershipOfProvidedAddresses(addresses)

View File

@ -60,6 +60,7 @@ proc walletAccountToWalletAccountsItem*(w: WalletAccountDto, keycardAccount: boo
proc walletAccountToWalletSendAccountItem*(w: WalletAccountDto, chainIds: seq[int], enabledChainIds: seq[int], proc walletAccountToWalletSendAccountItem*(w: WalletAccountDto, chainIds: seq[int], enabledChainIds: seq[int],
currencyBalance: float64, currencyFormat: CurrencyFormatDto, areTestNetworksEnabled: bool): wallet_send_account_item.AccountItem = currencyBalance: float64, currencyFormat: CurrencyFormatDto, areTestNetworksEnabled: bool): wallet_send_account_item.AccountItem =
return wallet_send_account_item.newAccountItem( return wallet_send_account_item.newAccountItem(
w.keyUid,
w.name, w.name,
w.address, w.address,
w.colorId, w.colorId,

View File

@ -122,9 +122,9 @@ StackLayout {
Global.openPopup(communityIntroDialogPopup, { Global.openPopup(communityIntroDialogPopup, {
communityId: joinCommunityView.communityId, communityId: joinCommunityView.communityId,
isInvitationPending: joinCommunityView.isInvitationPending, isInvitationPending: joinCommunityView.isInvitationPending,
name: communityData.name, communityName: communityData.name,
introMessage: communityData.introMessage, introMessage: communityData.introMessage,
imageSrc: communityData.image, communityIcon: communityData.image,
accessType: communityData.access accessType: communityData.access
}) })
} }
@ -190,9 +190,9 @@ StackLayout {
Global.openPopup(communityIntroDialogPopup, { Global.openPopup(communityIntroDialogPopup, {
communityId: chatView.communityId, communityId: chatView.communityId,
isInvitationPending: root.rootStore.isMyCommunityRequestPending(chatView.communityId), isInvitationPending: root.rootStore.isMyCommunityRequestPending(chatView.communityId),
name: root.sectionItemModel.name, communityName: root.sectionItemModel.name,
introMessage: root.sectionItemModel.introMessage, introMessage: root.sectionItemModel.introMessage,
imageSrc: root.sectionItemModel.image, communityIcon: root.sectionItemModel.image,
accessType: root.sectionItemModel.access accessType: root.sectionItemModel.access
}) })
} }
@ -205,8 +205,8 @@ StackLayout {
Loader { Loader {
id: communitySettingsLoader id: communitySettingsLoader
active: root.rootStore.chatCommunitySectionModule.isCommunity() && active: root.rootStore.chatCommunitySectionModule.isCommunity() &&
root.isPrivilegedUser && root.isPrivilegedUser &&
(root.currentIndex === 1 || !!communitySettingsLoader.item) // lazy load and preserve state after loading (root.currentIndex === 1 || !!communitySettingsLoader.item) // lazy load and preserve state after loading
asynchronous: false // It's false on purpose. We want to load the component synchronously asynchronous: false // It's false on purpose. We want to load the component synchronously
sourceComponent: CommunitySettingsView { sourceComponent: CommunitySettingsView {
@ -272,8 +272,9 @@ StackLayout {
property string communityId property string communityId
loginType: root.rootStore.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
requirementsCheckPending: root.rootStore.requirementsCheckPending requirementsCheckPending: root.rootStore.requirementsCheckPending
permissionsModel: { permissionsModel: {
@ -293,8 +294,8 @@ StackLayout {
communityIntroDialog.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel communityIntroDialog.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel
} }
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.rootStore.signSharedAddressesForAllNonKeycardKeypairs() root.rootStore.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -321,9 +322,21 @@ StackLayout {
Connections { Connections {
target: root.rootStore.communitiesModuleInst target: root.rootStore.communitiesModuleInst
function onSharedAddressesForAllNonKeycardKeypairsSigned() { function onAllSharedAddressesSigned() {
if (communityIntroDialog.profileProvesOwnershipOfSelectedAddresses) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (communityIntroDialog.allAddressesToRevealBelongToSingleNonProfileKeypair) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (!!communityIntroDialog.replaceItem) { if (!!communityIntroDialog.replaceItem) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned() communityIntroDialog.replaceLoader.item.allSigned()
} }
} }
} }

View File

@ -403,8 +403,8 @@ QtObject {
communitiesModuleInst.prepareKeypairsForSigning(communityId, ensName, JSON.stringify(addressesToShare), airdropAddress, editMode) communitiesModuleInst.prepareKeypairsForSigning(communityId, ensName, JSON.stringify(addressesToShare), airdropAddress, editMode)
} }
function signSharedAddressesForAllNonKeycardKeypairs() { function signProfileKeypairAndAllNonKeycardKeypairs() {
communitiesModuleInst.signSharedAddressesForAllNonKeycardKeypairs() communitiesModuleInst.signProfileKeypairAndAllNonKeycardKeypairs()
} }
function signSharedAddressesForKeypair(keyUid) { function signSharedAddressesForKeypair(keyUid) {

View File

@ -15,17 +15,16 @@ import utils 1.0
StatusListView { StatusListView {
id: root id: root
required property var selectedSharedAddressesMap // Map[address, [selected, isAirdrop]]
property var walletAssetsModel property var walletAssetsModel
property bool hasPermissions property bool hasPermissions
property var uniquePermissionTokenKeys property var uniquePermissionTokenKeys
// read/write properties
property string selectedAirdropAddress
property var selectedSharedAddresses: []
property var getCurrencyAmount: function (balance, symbol){} property var getCurrencyAmount: function (balance, symbol){}
signal addressesChanged() signal toggleAddressSelection(string keyUid, string address)
signal airdropAddressSelected (string address)
leftMargin: d.absLeftMargin leftMargin: d.absLeftMargin
topMargin: Style.current.padding topMargin: Style.current.padding
@ -35,6 +34,8 @@ StatusListView {
QtObject { QtObject {
id: d id: d
readonly property int selectedSharedAddressesCount: root.selectedSharedAddressesMap.size
// UI // UI
readonly property int absLeftMargin: 12 readonly property int absLeftMargin: 12
@ -46,10 +47,6 @@ StatusListView {
exclusive: false exclusive: false
} }
function selectFirstAvailableAirdropAddress() {
root.selectedAirdropAddress = ModelUtils.modelToFlatArray(root.model, "address").find(address => selectedSharedAddresses.includes(address))
}
function getTotalBalance(balances, decimals, symbol) { function getTotalBalance(balances, decimals, symbol) {
let totalBalance = 0 let totalBalance = 0
for(let i=0; i<balances.count; i++) { for(let i=0; i<balances.count; i++) {
@ -143,12 +140,20 @@ StatusListView {
icon.color: hovered ? Theme.palette.primaryColor3 : icon.color: hovered ? Theme.palette.primaryColor3 :
checked ? Theme.palette.primaryColor1 : disabledTextColor checked ? Theme.palette.primaryColor1 : disabledTextColor
checkable: true checkable: true
checked: listItem.address === root.selectedAirdropAddress.toLowerCase() checked: {
enabled: shareAddressCheckbox.checked && root.selectedSharedAddresses.length > 1 // last cannot be unchecked let obj = root.selectedSharedAddressesMap.get(listItem.address)
if (!!obj) {
return obj.isAirdrop
}
return false
}
enabled: shareAddressCheckbox.checked && d.selectedSharedAddressesCount > 1 // last cannot be unchecked
visible: shareAddressCheckbox.checked visible: shareAddressCheckbox.checked
opacity: enabled ? 1.0 : 0.3 opacity: enabled ? 1.0 : 0.3
onCheckedChanged: if (checked) root.selectedAirdropAddress = listItem.address
onToggled: root.addressesChanged() onToggled: {
root.airdropAddressSelected(listItem.address)
}
StatusToolTip { StatusToolTip {
text: qsTr("Use this address for any Community airdrops") text: qsTr("Use this address for any Community airdrops")
@ -161,25 +166,11 @@ StatusListView {
ButtonGroup.group: d.addressesGroup ButtonGroup.group: d.addressesGroup
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checkable: true checkable: true
checked: root.selectedSharedAddresses.some((address) => address.toLowerCase() === listItem.address ) checked: root.selectedSharedAddressesMap.has(listItem.address)
enabled: !(root.selectedSharedAddresses.length === 1 && checked) // last cannot be unchecked enabled: !(d.selectedSharedAddressesCount === 1 && checked) // last cannot be unchecked
onToggled: { onToggled: {
// handle selected addresses root.toggleAddressSelection(model.keyUid, listItem.address)
const index = root.selectedSharedAddresses.findIndex((address) => address.toLowerCase() === listItem.address)
const selectedSharedAddressesCopy = Object.assign([], root.selectedSharedAddresses) // deep copy
if (index === -1) {
selectedSharedAddressesCopy.push(listItem.address)
} else {
selectedSharedAddressesCopy.splice(index, 1)
}
root.selectedSharedAddresses = selectedSharedAddressesCopy
// switch to next available airdrop address when unchecking
if (!checked && listItem.address === root.selectedAirdropAddress.toLowerCase()) {
d.selectFirstAvailableAirdropAddress()
}
root.addressesChanged()
} }
} }
] ]

View File

@ -24,13 +24,18 @@ import shared.panels 1.0
Control { Control {
id: root id: root
property bool isEditMode required property string componentUid
required property bool isEditMode
required property var selectedSharedAddressesMap // Map[address, [keyUid, selected, isAirdrop]
property var currentSharedAddressesMap // Map[address, [keyUid, selected, isAirdrop]
required property int totalNumOfAddressesForSharing
required property bool profileProvesOwnershipOfSelectedAddresses
required property bool allAddressesToRevealBelongToSingleNonProfileKeypair
property bool requirementsCheckPending: false property bool requirementsCheckPending: false
required property string communityName required property string communityName
required property string communityIcon required property string communityIcon
property int loginType: Constants.LoginType.Password
required property var walletAssetsModel required property var walletAssetsModel
required property var walletAccountsModel // name, address, emoji, colorId, assets required property var walletAccountsModel // name, address, emoji, colorId, assets
@ -38,52 +43,16 @@ Control {
required property var assetsModel required property var assetsModel
required property var collectiblesModel required property var collectiblesModel
readonly property string title: isEditMode ? qsTr("Edit which addresses you share with %1").arg(communityName) readonly property string title: isEditMode ? qsTr("Edit which addresses you share with %1").arg(root.communityName)
: qsTr("Select addresses to share with %1").arg(communityName) : qsTr("Select addresses to share with %1").arg(root.communityName)
readonly property var buttons: ObjectModel { readonly property var rightButtons: root.isEditMode? [d.cancelButton, d.saveButton] : [d.shareAddressesButton]
StatusFlatButton {
visible: root.isEditMode
borderColor: Theme.palette.baseColor2
text: qsTr("Cancel")
onClicked: root.close()
}
StatusButton {
enabled: d.dirty
type: d.lostCommunityPermission || d.lostChannelPermissions ? StatusBaseButton.Type.Danger : StatusBaseButton.Type.Normal
visible: root.isEditMode
icon.name: type === StatusBaseButton.Type.Normal && d.selectedAddressesDirty?
!root.isEditMode? Constants.authenticationIconByType[root.loginType] : ""
: ""
text: d.lostCommunityPermission ? qsTr("Save changes & leave %1").arg(root.communityName) :
d.lostChannelPermissions ? qsTr("Save changes & update my permissions")
: qsTr("Prove ownership")
onClicked: {
root.prepareForSigning(root.selectedAirdropAddress, root.selectedSharedAddresses)
}
}
StatusButton {
visible: !root.isEditMode
text: qsTr("Share selected addresses to join")
onClicked: {
root.shareSelectedAddressesClicked(root.selectedAirdropAddress, root.selectedSharedAddresses)
root.close()
}
}
// NB no more buttons after this, see property `rightButtons` below
}
readonly property var rightButtons: [buttons.get(buttons.count-1)] // "magically" used by CommunityIntroDialog StatusStackModal impl
property var selectedSharedAddresses: []
property string selectedAirdropAddress
property var getCurrencyAmount: function (balance, symbol){} property var getCurrencyAmount: function (balance, symbol){}
signal sharedAddressesChanged(string airdropAddress, var sharedAddresses) signal toggleAddressSelection(string keyUid, string address)
signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses) signal airdropAddressSelected (string address)
signal prepareForSigning(string airdropAddress, var sharedAddresses) signal shareSelectedAddressesClicked()
signal close() signal close()
padding: 0 padding: 0
@ -94,69 +63,108 @@ Control {
// internal logic // internal logic
readonly property bool hasPermissions: root.permissionsModel && root.permissionsModel.count readonly property bool hasPermissions: root.permissionsModel && root.permissionsModel.count
readonly property int selectedSharedAddressesCount: root.selectedSharedAddressesMap.size
// initial state (not bindings, we want a static snapshot of the initial state) readonly property bool dirty: {
property var initialSelectedSharedAddresses: [] if (root.currentSharedAddressesMap.size !== root.selectedSharedAddressesMap.size) {
property string initialSelectedAirdropAddress return true
}
// dirty state handling for (const [key, value] of root.currentSharedAddressesMap) {
readonly property bool selectedAddressesDirty: !SQInternal.ModelUtils.isSameArray(d.initialSelectedSharedAddresses, root.selectedSharedAddresses) const obj = root.selectedSharedAddressesMap.get(key)
readonly property bool selectedAirdropAddressDirty: root.selectedAirdropAddress !== d.initialSelectedAirdropAddress if (!obj || value.selected !== obj.selected || value.isAirdrop !== obj.isAirdrop) {
readonly property bool dirty: selectedAddressesDirty || selectedAirdropAddressDirty return true
}
}
return false
}
// warning states // warning states
readonly property bool lostCommunityPermission: root.isEditMode && permissionsView.lostPermissionToJoin readonly property bool lostCommunityPermission: root.isEditMode && permissionsView.lostPermissionToJoin
readonly property bool lostChannelPermissions: root.isEditMode && permissionsView.lostChannelPermissions readonly property bool lostChannelPermissions: root.isEditMode && permissionsView.lostChannelPermissions
}
Component.onCompleted: { readonly property var cancelButton: StatusFlatButton {
// initialize the state visible: root.isEditMode
d.initialSelectedSharedAddresses = root.selectedSharedAddresses.length ? root.selectedSharedAddresses borderColor: Theme.palette.baseColor2
: filteredAccountsModel.count ? ModelUtils.modelToFlatArray(filteredAccountsModel, "address") text: qsTr("Cancel")
: [] onClicked: root.close()
d.initialSelectedAirdropAddress = !!root.selectedAirdropAddress ? root.selectedAirdropAddress
: d.initialSelectedSharedAddresses.length ? d.initialSelectedSharedAddresses[0] : ""
root.selectedSharedAddresses = accountSelector.selectedSharedAddresses
root.selectedAirdropAddress = accountSelector.selectedAirdropAddress
}
function setOldSharedAddresses(oldSharedAddresses) {
d.initialSelectedSharedAddresses = oldSharedAddresses
accountSelector.selectedSharedAddresses = Qt.binding(() => d.initialSelectedSharedAddresses)
accountSelector.applyChange()
}
function setOldAirdropAddress(oldAirdropAddress) {
d.initialSelectedAirdropAddress = oldAirdropAddress
accountSelector.selectedAirdropAddress = Qt.binding(() => d.initialSelectedAirdropAddress)
accountSelector.applyChange()
}
SortFilterProxyModel {
id: filteredAccountsModel
sourceModel: root.walletAccountsModel
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType
inverted: true
} }
sorters: [
ExpressionSorter { readonly property var saveButton: StatusButton {
function isGenerated(modelData) { enabled: d.dirty
return modelData.walletType === Constants.generatedWalletType type: d.lostCommunityPermission || d.lostChannelPermissions ? StatusBaseButton.Type.Danger : StatusBaseButton.Type.Normal
visible: root.isEditMode
text: {
if (d.lostCommunityPermission) {
return qsTr("Save changes & leave %1").arg(root.communityName)
}
if (d.lostChannelPermissions) {
return qsTr("Save changes & update my permissions")
}
if (d.selectedSharedAddressesCount === root.totalNumOfAddressesForSharing) {
return qsTr("Reveal all addresses")
}
return qsTr("Reveal %n address(s)", "", d.selectedSharedAddressesCount)
}
icon.name: {
if (!d.lostCommunityPermission
&& !d.lostChannelPermissions
&& root.profileProvesOwnershipOfSelectedAddresses) {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
return "keycard"
} }
expression: { return ""
return isGenerated(modelLeft)
}
},
RoleSorter {
roleName: "position"
},
RoleSorter {
roleName: "name"
} }
]
onClicked: {
root.shareSelectedAddressesClicked()
}
}
readonly property var shareAddressesButton: StatusButton {
visible: !root.isEditMode
text: {
if (d.selectedSharedAddressesCount === root.totalNumOfAddressesForSharing) {
return qsTr("Share all addresses to join")
}
return qsTr("Share %n address(s) to join", "", d.selectedSharedAddressesCount)
}
icon.name: {
if (root.profileProvesOwnershipOfSelectedAddresses) {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
return "keycard"
}
return ""
}
onClicked: {
root.shareSelectedAddressesClicked()
}
}
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
@ -184,14 +192,16 @@ Control {
Layout.fillHeight: !hasPermissions Layout.fillHeight: !hasPermissions
model: root.walletAccountsModel model: root.walletAccountsModel
walletAssetsModel: root.walletAssetsModel walletAssetsModel: root.walletAssetsModel
selectedSharedAddresses: d.initialSelectedSharedAddresses selectedSharedAddressesMap: root.selectedSharedAddressesMap
selectedAirdropAddress: d.initialSelectedAirdropAddress
onAddressesChanged: accountSelector.applyChange() onToggleAddressSelection: {
function applyChange() { root.toggleAddressSelection(keyUid, address)
root.selectedSharedAddresses = selectedSharedAddresses
root.selectedAirdropAddress = selectedAirdropAddress
root.sharedAddressesChanged(selectedAirdropAddress, selectedSharedAddresses)
} }
onAirdropAddressSelected: {
root.airdropAddressSelected(address)
}
getCurrencyAmount: function (balance, symbol){ getCurrencyAmount: function (balance, symbol){
return root.getCurrencyAmount(balance, symbol) return root.getCurrencyAmount(balance, symbol)
} }

View File

@ -13,46 +13,67 @@ import SortFilterProxyModel 0.2
ColumnLayout { ColumnLayout {
id: root id: root
required property string componentUid
required property bool isEditMode
property var keypairSigningModel property var keypairSigningModel
readonly property string title: qsTr("Prove ownership of keypairs") required property var selectedSharedAddressesMap // Map[address, [keyUid, selected, isAirdrop]
required property int totalNumOfAddressesForSharing
required property string communityName
readonly property string title: root.isEditMode?
qsTr("Save addresses you share with %1").arg(root.communityName)
: qsTr("Request to join %1").arg(root.communityName)
readonly property var rightButtons: [d.rightBtn] readonly property var rightButtons: [d.rightBtn]
readonly property bool allSigned: regularKeypairs.visible == d.sharedAddressesForAllNonKeycardKeypairsSigned &&
keycardKeypairs.visible == d.allKeycardKeypairsSigned
signal joinCommunity() signal joinCommunity()
signal signSharedAddressesForAllNonKeycardKeypairs() signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid) signal signSharedAddressesForKeypair(string keyUid)
function sharedAddressesForAllNonKeycardKeypairsSigned() { function allSigned() {
d.sharedAddressesForAllNonKeycardKeypairsSigned = true d.allSigned = true
} }
QtObject { QtObject {
id: d id: d
property bool sharedAddressesForAllNonKeycardKeypairsSigned: false readonly property int selectedSharedAddressesCount: root.selectedSharedAddressesMap.size
property bool allKeycardKeypairsSigned: false
property bool allSigned: false
readonly property bool anyOfSelectedAddressesToRevealBelongToProfileKeypair: {
for (const [key, value] of root.selectedSharedAddressesMap) {
if (value.keyUid === userProfile.keyUid) {
return true
}
}
return false
}
readonly property bool thereAreMoreThanOneNonProfileRegularKeypairs: nonProfileRegularKeypairs.count > 1
readonly property bool allNonProfileRegularKeypairsSigned: {
for (let i = 0; i < nonProfileRegularKeypairs.model.count; ++i) {
const item = nonProfileRegularKeypairs.model.get(i)
if (!!item && !item.keyPair.ownershipVerified) {
return false
}
}
return true
}
readonly property var rightBtn: StatusButton { readonly property var rightBtn: StatusButton {
enabled: root.allSigned enabled: d.allSigned
text: qsTr("Share your addresses to join") text: {
if (d.selectedSharedAddressesCount === root.totalNumOfAddressesForSharing) {
return qsTr("Share all addresses to join")
}
return qsTr("Share %n address(s) to join", "", d.selectedSharedAddressesCount)
}
onClicked: { onClicked: {
root.joinCommunity() root.joinCommunity()
} }
} }
function reEvaluateSignedKeypairs() {
let allKeypairsSigned = true
for(var i = 0; i< keycardKeypairs.model.count; i++) {
if(!keycardKeypairs.model.get(i).keyPair.ownershipVerified) {
allKeypairsSigned = false
break
}
}
d.allKeycardKeypairsSigned = allKeypairsSigned
}
} }
ColumnLayout { ColumnLayout {
@ -61,43 +82,41 @@ ColumnLayout {
spacing: Style.current.padding spacing: Style.current.padding
StatusBaseText {
Layout.preferredWidth: parent.width
elide: Text.ElideRight
font.pixelSize: Constants.keycard.general.fontSize2
text: qsTr("To share %n address(s) with <b>%1</b>, authenticate the associated keypairs...", "", d.selectedSharedAddressesCount).arg(root.communityName)
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
visible: regularKeypairs.visible visible: nonKeycardProfileKeypair.visible
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Keypairs we need an authentication for") text: qsTr("Stored on device")
font.pixelSize: Constants.keycard.general.fontSize2 font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
StatusButton {
text: d.sharedAddressesForAllNonKeycardKeypairsSigned? qsTr("Authenticated") : qsTr("Authenticate")
enabled: !d.sharedAddressesForAllNonKeycardKeypairsSigned
icon.name: userProfile.usingBiometricLogin? "touch-id" : "password"
onClicked: {
root.signSharedAddressesForAllNonKeycardKeypairs()
}
}
} }
StatusListView { StatusListView {
id: regularKeypairs id: nonKeycardProfileKeypair
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: regularKeypairs.contentHeight Layout.preferredHeight: nonKeycardProfileKeypair.contentHeight
visible: regularKeypairs.model.count > 0 visible: nonKeycardProfileKeypair.model.count > 0
spacing: Style.current.padding spacing: Style.current.padding
model: SortFilterProxyModel { model: SortFilterProxyModel {
sourceModel: root.keypairSigningModel sourceModel: root.keypairSigningModel
filters: ExpressionFilter { filters: ExpressionFilter {
expression: !model.keyPair.migratedToKeycard expression: model.keyPair.keyUid === userProfile.keyUid && !userProfile.isKeycardUser
} }
} }
delegate: KeyPairItem { delegate: KeyPairItem {
id: kpOnDeviceDelegate
width: ListView.view.width width: ListView.view.width
sensor.hoverEnabled: false sensor.hoverEnabled: false
additionalInfoForProfileKeypair: "" additionalInfoForProfileKeypair: ""
@ -109,22 +128,80 @@ ColumnLayout {
keyPairImage: model.keyPair.image keyPairImage: model.keyPair.image
keyPairDerivedFrom: model.keyPair.derivedFrom keyPairDerivedFrom: model.keyPair.derivedFrom
keyPairAccounts: model.keyPair.accounts keyPairAccounts: model.keyPair.accounts
components: [
StatusButton {
text: qsTr("Authenticate")
visible: !model.keyPair.ownershipVerified
icon.name: {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
},
StatusButton {
text: qsTr("Authenticated")
visible: model.keyPair.ownershipVerified
enabled: false
normalColor: "transparent"
disabledColor: "transparent"
disabledTextColor: Theme.palette.successColor1
icon.name: "checkmark-circle"
}
]
SequentialAnimation {
running: model.keyPair.ownershipVerified
PropertyAnimation {
target: kpOnDeviceDelegate
property: "color"
to: Theme.palette.successColor3
duration: 500
}
PropertyAnimation {
target: kpOnDeviceDelegate
property: "color"
to: Theme.palette.baseColor2
duration: 1500
}
}
} }
} }
Item { Item {
visible: regularKeypairs.visible && keycardKeypairs.visible visible: nonKeycardProfileKeypair.visible
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Style.current.xlPadding Layout.preferredHeight: Style.current.xlPadding
} }
StatusBaseText { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
visible: keycardKeypairs.visible visible: keycardKeypairs.visible
text: qsTr("Keypairs that need to be singed using appropriate Keycard")
font.pixelSize: Constants.keycard.general.fontSize2 StatusBaseText {
color: Theme.palette.baseColor1 text: qsTr("Stored on keycard")
wrapMode: Text.WordWrap font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1
wrapMode: Text.WordWrap
}
StatusIcon {
Layout.preferredHeight: 20
Layout.preferredWidth: 20
color: Theme.palette.baseColor1
icon: "keycard"
}
} }
StatusListView { StatusListView {
@ -140,6 +217,7 @@ ColumnLayout {
} }
} }
delegate: KeyPairItem { delegate: KeyPairItem {
id: kpOnKeycardDelegate
width: ListView.view.width width: ListView.view.width
sensor.hoverEnabled: !model.keyPair.ownershipVerified sensor.hoverEnabled: !model.keyPair.ownershipVerified
additionalInfoForProfileKeypair: "" additionalInfoForProfileKeypair: ""
@ -153,28 +231,173 @@ ColumnLayout {
keyPairAccounts: model.keyPair.accounts keyPairAccounts: model.keyPair.accounts
components: [ components: [
StatusBaseText { StatusButton {
font.weight: Font.Medium text: qsTr("Authenticate")
font.underline: mouseArea.containsMouse visible: !model.keyPair.ownershipVerified
font.pixelSize: Theme.primaryTextFontSize icon.name: "keycard"
color: model.keyPair.ownershipVerified? Theme.palette.baseColor1 : Theme.palette.primaryColor1
text: model.keyPair.ownershipVerified? qsTr("Signed") : qsTr("Sign") onClicked: {
MouseArea { if (model.keyPair.keyUid === userProfile.keyUid) {
id: mouseArea root.signProfileKeypairAndAllNonKeycardKeypairs()
anchors.fill: parent return
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: !model.keyPair.ownershipVerified
enabled: !model.keyPair.ownershipVerified
onEnabledChanged: {
d.reEvaluateSignedKeypairs()
}
onClicked: {
root.signSharedAddressesForKeypair(model.keyPair.keyUid)
} }
root.signSharedAddressesForKeypair(model.keyPair.keyUid)
} }
},
StatusButton {
text: qsTr("Authenticated")
visible: model.keyPair.ownershipVerified
enabled: false
normalColor: "transparent"
disabledColor: "transparent"
disabledTextColor: Theme.palette.successColor1
icon.name: "checkmark-circle"
} }
] ]
SequentialAnimation {
running: model.keyPair.ownershipVerified
PropertyAnimation {
target: kpOnKeycardDelegate
property: "color"
to: Theme.palette.successColor3
duration: 500
}
PropertyAnimation {
target: kpOnKeycardDelegate
property: "color"
to: Theme.palette.baseColor2
duration: 1500
}
}
}
}
Item {
visible: keycardKeypairs.visible
Layout.fillWidth: true
Layout.preferredHeight: Style.current.xlPadding
}
RowLayout {
Layout.fillWidth: true
spacing: 8
visible: nonProfileRegularKeypairs.visible
StatusBaseText {
Layout.preferredWidth: !d.anyOfSelectedAddressesToRevealBelongToProfileKeypair &&
d.thereAreMoreThanOneNonProfileRegularKeypairs?
370
: -1
Layout.fillWidth: true
text: !d.anyOfSelectedAddressesToRevealBelongToProfileKeypair &&
d.thereAreMoreThanOneNonProfileRegularKeypairs?
qsTr("Authenticate via “%1” keypair").arg(userProfile.name)
: qsTr("The following keypairs will be authenticated via “%1” keypair").arg(userProfile.name)
font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1
wrapMode: Text.WrapAnywhere
}
StatusButton {
Layout.rightMargin: 16
text: qsTr("Authenticate")
visible: !d.anyOfSelectedAddressesToRevealBelongToProfileKeypair
&& d.thereAreMoreThanOneNonProfileRegularKeypairs
&& !d.allNonProfileRegularKeypairsSigned
icon.name: {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
}
}
StatusListView {
id: nonProfileRegularKeypairs
Layout.fillWidth: true
Layout.preferredHeight: nonProfileRegularKeypairs.contentHeight
visible: nonProfileRegularKeypairs.model.count > 0
spacing: Style.current.padding
model: SortFilterProxyModel {
sourceModel: root.keypairSigningModel
filters: ExpressionFilter {
expression: !model.keyPair.migratedToKeycard && model.keyPair.keyUid !== userProfile.keyUid
}
}
delegate: KeyPairItem {
id: dependantKpOnDeviceDelegate
width: ListView.view.width
sensor.hoverEnabled: false
additionalInfoForProfileKeypair: ""
keyPairType: model.keyPair.pairType
keyPairKeyUid: model.keyPair.keyUid
keyPairName: model.keyPair.name
keyPairIcon: model.keyPair.icon
keyPairImage: model.keyPair.image
keyPairDerivedFrom: model.keyPair.derivedFrom
keyPairAccounts: model.keyPair.accounts
components: [
StatusButton {
Layout.rightMargin: 16
text: qsTr("Authenticate")
visible: !d.anyOfSelectedAddressesToRevealBelongToProfileKeypair
&& !d.thereAreMoreThanOneNonProfileRegularKeypairs
&& !model.keyPair.ownershipVerified
icon.name: {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
},
StatusButton {
text: qsTr("Authenticated")
visible: model.keyPair.ownershipVerified
enabled: false
normalColor: "transparent"
disabledColor: "transparent"
disabledTextColor: Theme.palette.successColor1
icon.name: "checkmark-circle"
}
]
SequentialAnimation {
running: model.keyPair.ownershipVerified
PropertyAnimation {
target: dependantKpOnDeviceDelegate
property: "color"
to: Theme.palette.successColor3
duration: 500
}
PropertyAnimation {
target: dependantKpOnDeviceDelegate
property: "color"
to: Theme.palette.baseColor2
duration: 1500
}
}
} }
} }
} }

View File

@ -10,6 +10,12 @@ import AppLayouts.Communities.panels 1.0
import utils 1.0 import utils 1.0
/****************************************************
This file is not in use any more.
TODO: remove it
****************************************************/
StatusDialog { StatusDialog {
id: root id: root
@ -35,7 +41,7 @@ StatusDialog {
signal prepareForSigning(string airdropAddress, var sharedAddresses) signal prepareForSigning(string airdropAddress, var sharedAddresses)
signal editRevealedAddresses() signal editRevealedAddresses()
signal signSharedAddressesForAllNonKeycardKeypairs() signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid) signal signSharedAddressesForKeypair(string keyUid)
function setOldSharedAddresses(oldSharedAddresses) { function setOldSharedAddresses(oldSharedAddresses) {
@ -71,6 +77,8 @@ StatusDialog {
property var oldSharedAddresses property var oldSharedAddresses
property string oldAirdropAddress property string oldAirdropAddress
property int selectedSharedAddressesCount
property var selectAddressesPanelButtons: ObjectModel {} property var selectAddressesPanelButtons: ObjectModel {}
readonly property var signingPanelButtons: ObjectModel { readonly property var signingPanelButtons: ObjectModel {
StatusFlatButton { StatusFlatButton {
@ -136,6 +144,7 @@ StatusDialog {
d.displaySigningPanel = true d.displaySigningPanel = true
} }
onSharedAddressesChanged: { onSharedAddressesChanged: {
d.selectedSharedAddressesCount = sharedAddresses.length
root.sharedAddressesChanged(airdropAddress, sharedAddresses) root.sharedAddressesChanged(airdropAddress, sharedAddresses)
} }
onClose: root.close() onClose: root.close()
@ -147,12 +156,16 @@ StatusDialog {
Component { Component {
id: sharedAddressesSigningPanelComponent id: sharedAddressesSigningPanelComponent
SharedAddressesSigningPanel { SharedAddressesSigningPanel {
totalNumOfAddressesForSharing: root.walletAccountsModel.count
numOfSelectedAddressesForSharing: d.selectedSharedAddressesCount
communityName: root.communityName
keypairSigningModel: root.keypairSigningModel keypairSigningModel: root.keypairSigningModel
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.signSharedAddressesForAllNonKeycardKeypairs() root.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {

View File

@ -305,7 +305,7 @@ Item {
chatListPopupMenu: ChatContextMenuView { chatListPopupMenu: ChatContextMenuView {
id: chatContextMenuView id: chatContextMenuView
showDebugOptions: root.store.isDebugEnabledfir showDebugOptions: root.store.isDebugEnabledfir
// TODO pass the chatModel in its entirety instead of fetching the JSOn using just the id // TODO pass the chatModel in its entirety instead of fetching the JSOn using just the id
openHandler: function (id) { openHandler: function (id) {
@ -536,12 +536,14 @@ Item {
isInvitationPending: d.invitationPending isInvitationPending: d.invitationPending
requirementsCheckPending: root.store.requirementsCheckPending requirementsCheckPending: root.store.requirementsCheckPending
name: communityData.name communityName: communityData.name
introMessage: communityData.introMessage introMessage: communityData.introMessage
imageSrc: communityData.image communityIcon: communityData.image
accessType: communityData.access accessType: communityData.access
loginType: root.store.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
permissionsModel: { permissionsModel: {
root.store.prepareTokenModelForCommunity(communityData.id) root.store.prepareTokenModelForCommunity(communityData.id)
@ -560,8 +562,8 @@ Item {
communityIntroDialog.keypairSigningModel = root.store.communitiesModuleInst.keypairsSigningModel communityIntroDialog.keypairSigningModel = root.store.communitiesModuleInst.keypairsSigningModel
} }
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.store.signSharedAddressesForAllNonKeycardKeypairs() root.store.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -589,9 +591,21 @@ Item {
Connections { Connections {
target: root.store.communitiesModuleInst target: root.store.communitiesModuleInst
function onSharedAddressesForAllNonKeycardKeypairsSigned() { function onAllSharedAddressesSigned() {
if (communityIntroDialog.profileProvesOwnershipOfSelectedAddresses) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (communityIntroDialog.allAddressesToRevealBelongToSingleNonProfileKeypair) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (!!communityIntroDialog.replaceItem) { if (!!communityIntroDialog.replaceItem) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned() communityIntroDialog.replaceLoader.item.allSigned()
} }
} }
} }
@ -628,7 +642,7 @@ Item {
property int channelPosition: -1 property int channelPosition: -1
property var deleteChatConfirmationDialog property var deleteChatConfirmationDialog
onCreateCommunityChannel: function (chName, chDescription, chEmoji, chColor, onCreateCommunityChannel: function (chName, chDescription, chEmoji, chColor,
chCategoryId, hideIfPermissionsNotMet) { chCategoryId, hideIfPermissionsNotMet) {
root.store.createCommunityChannel(chName, chDescription, chEmoji, chColor, root.store.createCommunityChannel(chName, chDescription, chEmoji, chColor,
@ -649,7 +663,7 @@ Item {
onAddPermissions: function (permissions) { onAddPermissions: function (permissions) {
for (var i = 0; i < permissions.length; i++) { for (var i = 0; i < permissions.length; i++) {
root.store.permissionsStore.createPermission(permissions[i].holdingsListModel, root.store.permissionsStore.createPermission(permissions[i].holdingsListModel,
permissions[i].permissionType, permissions[i].permissionType,
permissions[i].isPrivate, permissions[i].isPrivate,
permissions[i].channelsListModel) permissions[i].channelsListModel)

View File

@ -211,9 +211,9 @@ SettingsContentBase {
Global.openPopup(communityIntroDialogPopup, { Global.openPopup(communityIntroDialogPopup, {
communityId: communityId, communityId: communityId,
isInvitationPending: root.rootStore.isMyCommunityRequestPending(communityId), isInvitationPending: root.rootStore.isMyCommunityRequestPending(communityId),
name: name, communityName: name,
introMessage: introMessage, introMessage: introMessage,
imageSrc: imageSrc, communityIcon: imageSrc,
accessType: accessType accessType: accessType
}) })
} }
@ -236,8 +236,9 @@ SettingsContentBase {
} }
} }
loginType: chatStore.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
requirementsCheckPending: root.rootStore.requirementsCheckPending requirementsCheckPending: root.rootStore.requirementsCheckPending
permissionsModel: { permissionsModel: {
@ -257,8 +258,8 @@ SettingsContentBase {
communityIntroDialog.keypairSigningModel = chatStore.communitiesModuleInst.keypairsSigningModel communityIntroDialog.keypairSigningModel = chatStore.communitiesModuleInst.keypairsSigningModel
} }
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
chatStore.signSharedAddressesForAllNonKeycardKeypairs() chatStore.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -280,9 +281,21 @@ SettingsContentBase {
Connections { Connections {
target: chatStore.communitiesModuleInst target: chatStore.communitiesModuleInst
function onSharedAddressesForAllNonKeycardKeypairsSigned() { function onAllSharedAddressesSigned() {
if (communityIntroDialog.profileProvesOwnershipOfSelectedAddresses) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (communityIntroDialog.allAddressesToRevealBelongToSingleNonProfileKeypair) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (!!communityIntroDialog.replaceItem) { if (!!communityIntroDialog.replaceItem) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned() communityIntroDialog.replaceLoader.item.allSigned()
} }
} }
} }

View File

@ -209,6 +209,10 @@ QtObject {
} }
} }
function canProfileProveOwnershipOfProvidedAddresses(addresses) {
return walletSection.canProfileProveOwnershipOfProvidedAddresses(JSON.stringify(addresses))
}
function setHideSignPhraseModal(value) { function setHideSignPhraseModal(value) {
localAccountSensitiveSettings.hideSignPhraseModal = value; localAccountSensitiveSettings.hideSignPhraseModal = value;
} }

View File

@ -241,8 +241,8 @@ QtObject {
communitiesModuleInst.prepareKeypairsForSigning(communityId, ensName, JSON.stringify(addressesToShare), airdropAddress, editMode) communitiesModuleInst.prepareKeypairsForSigning(communityId, ensName, JSON.stringify(addressesToShare), airdropAddress, editMode)
} }
function signSharedAddressesForAllNonKeycardKeypairs() { function signProfileKeypairAndAllNonKeycardKeypairs() {
communitiesModuleInst.signSharedAddressesForAllNonKeycardKeypairs() communitiesModuleInst.signProfileKeypairAndAllNonKeycardKeypairs()
} }
function signSharedAddressesForKeypair(keyUid) { function signSharedAddressesForKeypair(keyUid) {

View File

@ -263,9 +263,9 @@ QtObject {
imageSrc, accessType, isInvitationPending) { imageSrc, accessType, isInvitationPending) {
openPopup(communityIntroDialogPopup, openPopup(communityIntroDialogPopup,
{communityId: communityId, {communityId: communityId,
name: name, communityName: name,
introMessage: introMessage, introMessage: introMessage,
imageSrc: imageSrc, communityIcon: imageSrc,
accessType: accessType, accessType: accessType,
isInvitationPending: isInvitationPending isInvitationPending: isInvitationPending
}) })
@ -275,9 +275,9 @@ QtObject {
openPopup(communityIntroDialogPopup, openPopup(communityIntroDialogPopup,
{communityId: communityId, {communityId: communityId,
stackTitle: qsTr("Share addresses with %1's owner").arg(name), stackTitle: qsTr("Share addresses with %1's owner").arg(name),
name: name, communityName: name,
introMessage: qsTr("Share addresses to rejoin %1").arg(name), introMessage: qsTr("Share addresses to rejoin %1").arg(name),
imageSrc: imageSrc, communityIcon: imageSrc,
accessType: Constants.communityChatOnRequestAccess, accessType: Constants.communityChatOnRequestAccess,
isInvitationPending: false isInvitationPending: false
}) })
@ -354,7 +354,7 @@ QtObject {
tokenImage: tokenImage tokenImage: tokenImage
}) })
} }
function openConfirmHideAssetPopup(assetSymbol, assetName, assetImage, isCommunityToken) { function openConfirmHideAssetPopup(assetSymbol, assetName, assetImage, isCommunityToken) {
openPopup(confirmHideAssetPopup, { assetSymbol, assetName, assetImage, isCommunityToken }) openPopup(confirmHideAssetPopup, { assetSymbol, assetName, assetImage, isCommunityToken })
} }
@ -682,9 +682,12 @@ QtObject {
CommunityIntroDialog { CommunityIntroDialog {
id: communityIntroDialog id: communityIntroDialog
property string communityId property string communityId
loginType: root.rootStore.loginType
requirementsCheckPending: root.rootStore.requirementsCheckPending requirementsCheckPending: root.rootStore.requirementsCheckPending
walletAccountsModel: root.rootStore.walletAccountsModel walletAccountsModel: root.rootStore.walletAccountsModel
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
permissionsModel: { permissionsModel: {
root.rootStore.prepareTokenModelForCommunity(communityIntroDialog.communityId) root.rootStore.prepareTokenModelForCommunity(communityIntroDialog.communityId)
@ -703,8 +706,8 @@ QtObject {
communityIntroDialog.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel communityIntroDialog.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel
} }
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.rootStore.signSharedAddressesForAllNonKeycardKeypairs() root.rootStore.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -739,9 +742,21 @@ QtObject {
Connections { Connections {
target: root.rootStore.communitiesModuleInst target: root.rootStore.communitiesModuleInst
function onSharedAddressesForAllNonKeycardKeypairsSigned() { function onAllSharedAddressesSigned() {
if (communityIntroDialog.profileProvesOwnershipOfSelectedAddresses) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (communityIntroDialog.allAddressesToRevealBelongToSingleNonProfileKeypair) {
communityIntroDialog.joinCommunity()
communityIntroDialog.close()
return
}
if (!!communityIntroDialog.replaceItem) { if (!!communityIntroDialog.replaceItem) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned() communityIntroDialog.replaceLoader.item.allSigned()
} }
} }
} }
@ -886,24 +901,9 @@ QtObject {
Component { Component {
id: editSharedAddressesPopupComponent id: editSharedAddressesPopupComponent
SharedAddressesPopup { CommunityIntroDialog {
id: editSharedAddressesPopup id: editSharedAddressesPopup
readonly property var oldSharedAddresses: root.rootStore.myRevealedAddressesForCurrentCommunity
readonly property string oldAirdropAddress: root.rootStore.myRevealedAirdropAddressForCurrentCommunity
onOldSharedAddressesChanged: {
editSharedAddressesPopup.setOldSharedAddresses(
editSharedAddressesPopup.oldSharedAddresses
)
}
onOldAirdropAddressChanged: {
editSharedAddressesPopup.setOldAirdropAddress(
editSharedAddressesPopup.oldAirdropAddress
)
}
property string communityId property string communityId
readonly property var chatStore: ChatStore.RootStore { readonly property var chatStore: ChatStore.RootStore {
@ -914,10 +914,17 @@ QtObject {
} }
} }
isEditMode: true
currentSharedAddresses: root.rootStore.myRevealedAddressesForCurrentCommunity
currentAirdropAddress: root.rootStore.myRevealedAirdropAddressForCurrentCommunity
communityName: chatStore.sectionDetails.name communityName: chatStore.sectionDetails.name
communityIcon: chatStore.sectionDetails.image communityIcon: chatStore.sectionDetails.image
requirementsCheckPending: root.rootStore.requirementsCheckPending requirementsCheckPending: root.rootStore.requirementsCheckPending
loginType: chatStore.loginType
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAccountsModel: root.rootStore.walletAccountsModel walletAccountsModel: root.rootStore.walletAccountsModel
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
permissionsModel: { permissionsModel: {
@ -927,16 +934,22 @@ QtObject {
assetsModel: chatStore.assetsModel assetsModel: chatStore.assetsModel
collectiblesModel: chatStore.collectiblesModel collectiblesModel: chatStore.collectiblesModel
onSharedAddressesChanged: root.rootStore.updatePermissionsModel( getCurrencyAmount: function (balance, symbol) {
editSharedAddressesPopup.communityId, sharedAddresses) return root.currencyStore.getCurrencyAmount(balance, symbol)
}
onSharedAddressesUpdated: {
root.rootStore.updatePermissionsModel(editSharedAddressesPopup.communityId, sharedAddresses)
}
onPrepareForSigning: { onPrepareForSigning: {
root.rootStore.prepareKeypairsForSigning(editSharedAddressesPopup.communityId, "", sharedAddresses, airdropAddress, true) root.rootStore.prepareKeypairsForSigning(editSharedAddressesPopup.communityId, "", sharedAddresses, airdropAddress, true)
editSharedAddressesPopup.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel editSharedAddressesPopup.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel
} }
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.rootStore.signSharedAddressesForAllNonKeycardKeypairs() root.rootStore.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -952,13 +965,23 @@ QtObject {
Connections { Connections {
target: root.rootStore.communitiesModuleInst target: root.rootStore.communitiesModuleInst
function onSharedAddressesForAllNonKeycardKeypairsSigned() { function onAllSharedAddressesSigned() {
editSharedAddressesPopup.sharedAddressesForAllNonKeycardKeypairsSigned() if (editSharedAddressesPopup.profileProvesOwnershipOfSelectedAddresses) {
} editSharedAddressesPopup.editRevealedAddresses()
} editSharedAddressesPopup.close()
return
}
getCurrencyAmount: function (balance, symbol) { if (editSharedAddressesPopup.allAddressesToRevealBelongToSingleNonProfileKeypair) {
return root.currencyStore.getCurrencyAmount(balance, symbol) editSharedAddressesPopup.editRevealedAddresses()
editSharedAddressesPopup.close()
return
}
if (!!editSharedAddressesPopup.replaceItem) {
editSharedAddressesPopup.replaceLoader.item.allSigned()
}
}
} }
} }
}, },

View File

@ -18,48 +18,98 @@ import SortFilterProxyModel 0.2
StatusStackModal { StatusStackModal {
id: root id: root
property string name property bool isEditMode: false
required property string communityName
required property string communityIcon
required property bool requirementsCheckPending
property string introMessage property string introMessage
property int accessType property int accessType
property url imageSrc
property bool isInvitationPending: false property bool isInvitationPending: false
property int loginType: Constants.LoginType.Password
required property var walletAccountsModel // name, address, emoji, colorId required property var walletAccountsModel // name, address, emoji, colorId
property var walletAssetsModel required property var walletAssetsModel
required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet
required property var assetsModel required property var assetsModel
required property var collectiblesModel required property var collectiblesModel
required property bool requirementsCheckPending
property var keypairSigningModel property var keypairSigningModel
property var currentSharedAddresses: []
onCurrentSharedAddressesChanged: d.reEvaluateModels()
property string currentAirdropAddress: ""
onCurrentAirdropAddressChanged: d.reEvaluateModels()
property var getCurrencyAmount: function (balance, symbol){} property var getCurrencyAmount: function (balance, symbol){}
property var canProfileProveOwnershipOfProvidedAddressesFn: function(addresses) { return false }
readonly property bool profileProvesOwnershipOfSelectedAddresses: {
d.selectedSharedAddressesMap // needed for binding
const obj = d.getSelectedAddresses()
return root.canProfileProveOwnershipOfProvidedAddressesFn(obj.addresses)
}
readonly property bool allAddressesToRevealBelongToSingleNonProfileKeypair: {
const keyUids = new Set()
for (const [key, value] of d.selectedSharedAddressesMap) {
keyUids.add(value.keyUid)
}
return keyUids.size === 1 && !keyUids.has(userProfile.keyUid)
}
signal prepareForSigning(string airdropAddress, var sharedAddresses) signal prepareForSigning(string airdropAddress, var sharedAddresses)
signal joinCommunity() signal joinCommunity()
signal signSharedAddressesForAllNonKeycardKeypairs() signal editRevealedAddresses()
signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid) signal signSharedAddressesForKeypair(string keyUid)
signal cancelMembershipRequest() signal cancelMembershipRequest()
signal sharedAddressesUpdated(var sharedAddresses) signal sharedAddressesUpdated(var sharedAddresses)
width: 640 // by design width: 640 // by design
padding: 0 padding: 0
stackTitle: root.accessType === Constants.communityChatOnRequestAccess ? qsTr("Request to join %1").arg(name) : qsTr("Welcome to %1").arg(name) stackTitle: root.accessType === Constants.communityChatOnRequestAccess ?
qsTr("Request to join %1").arg(root.communityName)
: qsTr("Welcome to %1").arg(root.communityName)
rightButtons: [d.shareButton, finishButton] rightButtons: [d.shareButton, finishButton]
finishButton: StatusButton { finishButton: StatusButton {
text: root.isInvitationPending ? text: {
qsTr("Cancel Membership Request") if (root.isInvitationPending) {
: root.accessType === Constants.communityChatOnRequestAccess? return qsTr("Cancel Membership Request")
qsTr("Prove ownership") } else if (root.accessType === Constants.communityChatOnRequestAccess) {
: qsTr("Join %1").arg(root.name) if (d.selectedSharedAddressesCount === d.totalNumOfAddressesForSharing) {
return qsTr("Share all addresses to join")
}
return qsTr("Share %n address(s) to join", "", d.selectedSharedAddressesCount)
}
return qsTr("Join %1").arg(root.communityName)
}
type: root.isInvitationPending ? StatusBaseButton.Type.Danger type: root.isInvitationPending ? StatusBaseButton.Type.Danger
: StatusBaseButton.Type.Normal : StatusBaseButton.Type.Normal
icon.name: {
if (root.profileProvesOwnershipOfSelectedAddresses) {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
return "keycard"
}
return ""
}
onClicked: { onClicked: {
if (root.isInvitationPending) { if (root.isInvitationPending) {
root.cancelMembershipRequest() root.cancelMembershipRequest()
@ -67,15 +117,48 @@ StatusStackModal {
return return
} }
root.prepareForSigning(d.selectedAirdropAddress, d.selectedSharedAddresses) d.proceedToSigningOrSubmitRequest(d.communityIntroUid)
root.replace(sharedAddressesSigningPanelComponent)
} }
} }
backButton: StatusBackButton {
visible: !!root.replaceLoader.item
&& !(root.replaceLoader.item.componentUid === d.shareAddressesUid && root.isEditMode)
onClicked: {
if (d.backActionGoesTo === d.communityIntroUid) {
if (root.replaceItem) {
root.replaceItem = undefined
}
return
}
if (d.backActionGoesTo === d.shareAddressesUid) {
d.backActionGoesTo = d.communityIntroUid
root.replace(sharedAddressesPanelComponent)
return
}
}
Layout.minimumWidth: implicitWidth
}
QtObject { QtObject {
id: d id: d
readonly property var tempAddressesModel: SortFilterProxyModel { readonly property string communityIntroUid: "community-intro"
readonly property string shareAddressesUid: "shared-addresses"
readonly property string signingPanelUid: "signing-panel"
property string backActionGoesTo: d.communityIntroUid
readonly property int totalNumOfAddressesForSharing: root.walletAccountsModel.count
property var currentSharedAddressesMap: new Map() // Map[address, [keyUid, selected, isAirdrop]] - used in edit mode only
property var selectedSharedAddressesMap: new Map() // Map[address, [keyUid, selected, isAirdrop]]
readonly property int selectedSharedAddressesCount: d.selectedSharedAddressesMap.size
property var initialAddressesModel: SortFilterProxyModel {
sourceModel: root.walletAccountsModel sourceModel: root.walletAccountsModel
sorters: [ sorters: [
ExpressionSorter { ExpressionSorter {
@ -96,58 +179,188 @@ StatusStackModal {
] ]
} }
// all non-watched addresses by default, unless selected otherwise below in SharedAddressesPanel function proceedToSigningOrSubmitRequest(uidOfComponentThisFunctionIsCalledFrom) {
property var selectedSharedAddresses: tempAddressesModel.count ? ModelUtils.modelToFlatArray(tempAddressesModel, "address") : [] const selected = d.getSelectedAddresses()
property string selectedAirdropAddress: selectedSharedAddresses.length ? selectedSharedAddresses[0] : "" root.prepareForSigning(selected.airdropAddress, selected.addresses)
if (root.profileProvesOwnershipOfSelectedAddresses) {
root.signProfileKeypairAndAllNonKeycardKeypairs()
return
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
if (d.selectedSharedAddressesMap.size === 0) {
console.error("selected shared addresses must not be empty")
return
}
const keyUid = d.selectedSharedAddressesMap.values()[0].keyUid
root.signSharedAddressesForKeypair(keyUid)
return
}
d.backActionGoesTo = uidOfComponentThisFunctionIsCalledFrom
root.replace(sharedAddressesSigningPanelComponent)
}
// This function deletes/adds it the address from/to the map.
function toggleAddressSelection(keyUid, address) {
const tmpMap = d.selectedSharedAddressesMap
const lAddress = address.toLowerCase()
const obj = tmpMap.get(lAddress)
if (!!obj) {
if (tmpMap.size === 1) {
console.error("cannot remove the last selected address")
}
tmpMap.delete(lAddress)
if (obj.isAirdrop) {
d.selectAirdropAddressForTheFirstSelectedAddress()
}
} else {
tmpMap.set(lAddress, {keyUid: keyUid, selected: true, isAirdrop: false})
}
d.selectedSharedAddressesMap = tmpMap
}
// This function selects new airdrop address, invalidating old airdrop address selection.
function selectAirdropAddressForTheFirstSelectedAddress() {
const tmpMap = d.selectedSharedAddressesMap
// clear previous airdrop address
for (const [key, value] of tmpMap) {
if (!value.isAirdrop) {
d.selectedSharedAddressesMap.set(key, {keyUid: value.keyUid, selected: value.selected, isAirdrop: true})
break
}
}
d.selectedSharedAddressesMap = tmpMap
}
// This function selects new airdrop address, invalidating old airdrop address selection.
function selectAirdropAddress(address) {
const tmpMap = d.selectedSharedAddressesMap
// clear previous airdrop address
for (const [key, value] of tmpMap) {
if (value.isAirdrop) {
tmpMap.set(key, {keyUid: value.keyUid, selected: value.selected, isAirdrop: false})
break
}
}
// set new airdrop address
const lAddress = address.toLowerCase()
const obj = tmpMap.get(lAddress)
if (!obj) {
console.error("cannot set airdrop address for unselected address")
return
}
obj.isAirdrop = true
tmpMap.set(lAddress, obj)
d.selectedSharedAddressesMap = tmpMap
}
// Returns an object containing all selected addresses and selected airdrop address.s
function getSelectedAddresses() {
const result = {addresses: [], airdropAddress: ""}
for (const [key, value] of d.selectedSharedAddressesMap) {
if (value.selected) {
result.addresses.push(key)
}
if (value.isAirdrop) {
result.airdropAddress = key
}
}
return result
}
function reEvaluateModels() {
const tmpSharedAddressesMap = new Map()
const tmpCurrentSharedAddressesMap = new Map()
for (let i=0; i < d.initialAddressesModel.count; ++i){
const obj = d.initialAddressesModel.get(i)
if (!!obj) {
let isAirdrop = i === 0
if (root.isEditMode) {
if (root.currentSharedAddresses.indexOf(obj.address) === -1) {
continue
}
isAirdrop = obj.address.toLowerCase() === root.currentAirdropAddress.toLowerCase()
}
tmpSharedAddressesMap.set(obj.address, {keyUid: obj.keyUid, selected: true, isAirdrop: isAirdrop})
tmpCurrentSharedAddressesMap.set(obj.address, {keyUid: obj.keyUid, selected: true, isAirdrop: isAirdrop})
}
}
d.selectedSharedAddressesMap = tmpSharedAddressesMap
if (root.isEditMode) {
d.currentSharedAddressesMap = new Map(tmpCurrentSharedAddressesMap)
}
}
readonly property var shareButton: StatusFlatButton { readonly property var shareButton: StatusFlatButton {
height: finishButton.height height: finishButton.height
visible: !root.isInvitationPending && !root.replaceItem visible: !root.isInvitationPending && !root.replaceItem
borderColor: Theme.palette.baseColor2 borderColor: "transparent"
text: qsTr("Select addresses to share") text: qsTr("Select addresses to share")
onClicked: root.replace(sharedAddressesPanelComponent) onClicked: {
d.backActionGoesTo = d.communityIntroUid
root.replace(sharedAddressesPanelComponent)
}
}
}
Component.onCompleted: {
d.reEvaluateModels()
if (root.isEditMode) {
d.backActionGoesTo = d.shareAddressesUid
root.replace(sharedAddressesPanelComponent)
} }
} }
Component { Component {
id: sharedAddressesPanelComponent id: sharedAddressesPanelComponent
SharedAddressesPanel { SharedAddressesPanel {
communityName: root.name componentUid: d.shareAddressesUid
communityIcon: root.imageSrc isEditMode: root.isEditMode
loginType: root.loginType communityName: root.communityName
communityIcon: root.communityIcon
requirementsCheckPending: root.requirementsCheckPending requirementsCheckPending: root.requirementsCheckPending
walletAccountsModel: SortFilterProxyModel {
sourceModel: root.walletAccountsModel
sorters: [
ExpressionSorter {
function isGenerated(modelData) {
return modelData.walletType === Constants.generatedWalletType
}
expression: { walletAccountsModel: d.initialAddressesModel
return isGenerated(modelLeft) selectedSharedAddressesMap: d.selectedSharedAddressesMap
} currentSharedAddressesMap: d.currentSharedAddressesMap
},
RoleSorter { totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
roleName: "position" profileProvesOwnershipOfSelectedAddresses: root.profileProvesOwnershipOfSelectedAddresses
}, allAddressesToRevealBelongToSingleNonProfileKeypair: root.allAddressesToRevealBelongToSingleNonProfileKeypair
RoleSorter {
roleName: "name"
}
]
}
walletAssetsModel: root.walletAssetsModel walletAssetsModel: root.walletAssetsModel
permissionsModel: root.permissionsModel permissionsModel: root.permissionsModel
assetsModel: root.assetsModel assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel collectiblesModel: root.collectiblesModel
onClose: {
root.close()
}
onToggleAddressSelection: {
d.toggleAddressSelection(keyUid, address)
const obj = d.getSelectedAddresses()
root.sharedAddressesUpdated(obj.addresses)
}
onAirdropAddressSelected: {
d.selectAirdropAddress(address)
}
onShareSelectedAddressesClicked: { onShareSelectedAddressesClicked: {
d.selectedAirdropAddress = airdropAddress d.proceedToSigningOrSubmitRequest(d.shareAddressesUid)
d.selectedSharedAddresses = sharedAddresses
root.replaceItem = undefined // go back, unload us
}
onSharedAddressesChanged: {
root.sharedAddressesUpdated(sharedAddresses)
} }
getCurrencyAmount: function (balance, symbol){ getCurrencyAmount: function (balance, symbol){
return root.getCurrencyAmount(balance, symbol) return root.getCurrencyAmount(balance, symbol)
} }
@ -158,10 +371,16 @@ StatusStackModal {
id: sharedAddressesSigningPanelComponent id: sharedAddressesSigningPanelComponent
SharedAddressesSigningPanel { SharedAddressesSigningPanel {
componentUid: d.signingPanelUid
isEditMode: root.isEditMode
totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
selectedSharedAddressesMap: d.selectedSharedAddressesMap
communityName: root.communityName
keypairSigningModel: root.keypairSigningModel keypairSigningModel: root.keypairSigningModel
onSignSharedAddressesForAllNonKeycardKeypairs: { onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.signSharedAddressesForAllNonKeycardKeypairs() root.signProfileKeypairAndAllNonKeycardKeypairs()
} }
onSignSharedAddressesForKeypair: { onSignSharedAddressesForKeypair: {
@ -169,7 +388,11 @@ StatusStackModal {
} }
onJoinCommunity: { onJoinCommunity: {
root.joinCommunity() if (root.isEditMode) {
root.editRevealedAddresses()
} else {
root.joinCommunity()
}
root.close() root.close()
} }
} }
@ -191,12 +414,12 @@ StatusStackModal {
visible: ((image.status == Image.Loading) || visible: ((image.status == Image.Loading) ||
(image.status == Image.Ready)) && (image.status == Image.Ready)) &&
!image.isError !image.isError
image.source: root.imageSrc image.source: root.communityIcon
} }
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
text: root.introMessage || qsTr("Community <b>%1</b> has no intro message...").arg(root.name) text: root.introMessage || qsTr("Community <b>%1</b> has no intro message...").arg(root.communityName)
color: Theme.palette.directColor1 color: Theme.palette.directColor1
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }