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.} =
raise newException(ValueError, "No implementation available")
method signSharedAddressesForAllNonKeycardKeypairs*(self: AccessInterface) {.base.} =
method signProfileKeypairAndAllNonKeycardKeypairs*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
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 =
return self.controller.shareCommunityChannelUrlWithData(communityId, chatId)
proc signRevealedAddressesThatBelongToRegularKeypairs(self: Module): bool =
proc signRevealedAddressesForNonKeycardKeypairs(self: Module): bool =
var signingParams: seq[SignParamsDto]
for address, details in self.joiningCommunityDetails.addressesToShare.pairs:
if details.signature.len > 0:
@ -702,8 +702,24 @@ proc signRevealedAddressesThatBelongToRegularKeypairs(self: Module): bool =
let signatures = self.controller.signCommunityRequests(self.joiningCommunityDetails.communityId, signingParams)
for i in 0 ..< len(signingParams):
self.joiningCommunityDetails.addressesToShare[signingParams[i].address].signature = signatures[i]
self.view.keypairsSigningModel().setOwnershipVerified(self.joiningCommunityDetails.addressesToShare[signingParams[i].address].keyUid, 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) =
if password == "" and pin == "":
info "unsuccesful authentication"
@ -712,8 +728,16 @@ method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid:
self.joiningCommunityDetails.profilePassword = password
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) =
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
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,
airdropAddress: string, editMode: bool) =
var addressesToShare: seq[string]
@ -778,7 +807,7 @@ method prepareKeypairsForSigning*(self: Module, communityId, ensName: string, ad
)
self.joiningCommunityDetails.addressesToShare[param.address] = details
method signSharedAddressesForAllNonKeycardKeypairs*(self: Module) =
method signProfileKeypairAndAllNonKeycardKeypairs*(self: Module) =
self.controller.authenticate()
# 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)
return
self.view.keypairsSigningModel().setOwnershipVerified(keyUid, true)
if self.joiningCommunityDetails.allSigned():
self.view.sendAllSharedAddressesSignedSignal()
method joinCommunityOrEditSharedAddresses*(self: Module) =
if not self.joiningCommunityDetails.allSigned():

View File

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

View File

@ -120,3 +120,6 @@ method getRpcStats*(self: AccessInterface): string {.base.} =
method resetRpcStats*(self: AccessInterface) {.base.} =
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 ./io_interface as io_interface
@ -471,3 +471,21 @@ method getRpcStats*(self: Module): string =
method resetRpcStats*(self: Module) =
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
proc setup*(self: AccountItem,
keyUid: string,
name: string,
address: string,
colorId: string,
@ -29,7 +30,7 @@ QtObject:
emoji,
walletType,
path = "",
keyUid = "",
keyUid = keyUid,
keycardAccount = false,
position,
operability = wa_dto.AccountFullyOperable,
@ -43,6 +44,7 @@ QtObject:
self.QObject.delete
proc newAccountItem*(
keyUid: string = "",
name: string = "",
address: string = "",
colorId: string = "",
@ -56,7 +58,7 @@ QtObject:
canSend: bool = true,
): AccountItem =
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 =
result = "WalletSection-Send-Item("

View File

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

View File

@ -7,7 +7,7 @@ import ./io_interface
import ../../shared_models/currency_amount
import ./wallet_connect/controller as wcc
type
type
ActivityControllerArray* = array[2, activityc.Controller]
QtObject:
@ -36,11 +36,11 @@ QtObject:
proc delete*(self: View) =
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface,
activityController: activityc.Controller,
tmpActivityControllers: ActivityControllerArray,
activityDetailsController: activity_detailsc.Controller,
collectibleDetailsController: collectible_detailsc.Controller,
proc newView*(delegate: io_interface.AccessInterface,
activityController: activityc.Controller,
tmpActivityControllers: ActivityControllerArray,
activityDetailsController: activity_detailsc.Controller,
collectibleDetailsController: collectible_detailsc.Controller,
wcController: wcc.Controller): View =
new(result, delete)
result.delegate = delegate
@ -259,3 +259,6 @@ QtObject:
return self.delegate.getRpcStats()
proc resetRpcStats*(self: View) {.slot.} =
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],
currencyBalance: float64, currencyFormat: CurrencyFormatDto, areTestNetworksEnabled: bool): wallet_send_account_item.AccountItem =
return wallet_send_account_item.newAccountItem(
w.keyUid,
w.name,
w.address,
w.colorId,

View File

@ -122,9 +122,9 @@ StackLayout {
Global.openPopup(communityIntroDialogPopup, {
communityId: joinCommunityView.communityId,
isInvitationPending: joinCommunityView.isInvitationPending,
name: communityData.name,
communityName: communityData.name,
introMessage: communityData.introMessage,
imageSrc: communityData.image,
communityIcon: communityData.image,
accessType: communityData.access
})
}
@ -190,9 +190,9 @@ StackLayout {
Global.openPopup(communityIntroDialogPopup, {
communityId: chatView.communityId,
isInvitationPending: root.rootStore.isMyCommunityRequestPending(chatView.communityId),
name: root.sectionItemModel.name,
communityName: root.sectionItemModel.name,
introMessage: root.sectionItemModel.introMessage,
imageSrc: root.sectionItemModel.image,
communityIcon: root.sectionItemModel.image,
accessType: root.sectionItemModel.access
})
}
@ -205,8 +205,8 @@ StackLayout {
Loader {
id: communitySettingsLoader
active: root.rootStore.chatCommunitySectionModule.isCommunity() &&
root.isPrivilegedUser &&
active: root.rootStore.chatCommunitySectionModule.isCommunity() &&
root.isPrivilegedUser &&
(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
sourceComponent: CommunitySettingsView {
@ -272,8 +272,9 @@ StackLayout {
property string communityId
loginType: root.rootStore.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
requirementsCheckPending: root.rootStore.requirementsCheckPending
permissionsModel: {
@ -293,8 +294,8 @@ StackLayout {
communityIntroDialog.keypairSigningModel = root.rootStore.communitiesModuleInst.keypairsSigningModel
}
onSignSharedAddressesForAllNonKeycardKeypairs: {
root.rootStore.signSharedAddressesForAllNonKeycardKeypairs()
onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.rootStore.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {
@ -321,9 +322,21 @@ StackLayout {
Connections {
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) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned()
communityIntroDialog.replaceLoader.item.allSigned()
}
}
}

View File

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

View File

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

View File

@ -24,13 +24,18 @@ import shared.panels 1.0
Control {
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
required property string communityName
required property string communityIcon
property int loginType: Constants.LoginType.Password
required property var walletAssetsModel
required property var walletAccountsModel // name, address, emoji, colorId, assets
@ -38,52 +43,16 @@ Control {
required property var assetsModel
required property var collectiblesModel
readonly property string title: isEditMode ? qsTr("Edit which addresses you share with %1").arg(communityName)
: qsTr("Select addresses to 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(root.communityName)
readonly property var buttons: ObjectModel {
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
readonly property var rightButtons: root.isEditMode? [d.cancelButton, d.saveButton] : [d.shareAddressesButton]
property var getCurrencyAmount: function (balance, symbol){}
signal sharedAddressesChanged(string airdropAddress, var sharedAddresses)
signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
signal prepareForSigning(string airdropAddress, var sharedAddresses)
signal toggleAddressSelection(string keyUid, string address)
signal airdropAddressSelected (string address)
signal shareSelectedAddressesClicked()
signal close()
padding: 0
@ -94,69 +63,108 @@ Control {
// internal logic
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)
property var initialSelectedSharedAddresses: []
property string initialSelectedAirdropAddress
// dirty state handling
readonly property bool selectedAddressesDirty: !SQInternal.ModelUtils.isSameArray(d.initialSelectedSharedAddresses, root.selectedSharedAddresses)
readonly property bool selectedAirdropAddressDirty: root.selectedAirdropAddress !== d.initialSelectedAirdropAddress
readonly property bool dirty: selectedAddressesDirty || selectedAirdropAddressDirty
readonly property bool dirty: {
if (root.currentSharedAddressesMap.size !== root.selectedSharedAddressesMap.size) {
return true
}
for (const [key, value] of root.currentSharedAddressesMap) {
const obj = root.selectedSharedAddressesMap.get(key)
if (!obj || value.selected !== obj.selected || value.isAirdrop !== obj.isAirdrop) {
return true
}
}
return false
}
// warning states
readonly property bool lostCommunityPermission: root.isEditMode && permissionsView.lostPermissionToJoin
readonly property bool lostChannelPermissions: root.isEditMode && permissionsView.lostChannelPermissions
}
Component.onCompleted: {
// initialize the state
d.initialSelectedSharedAddresses = root.selectedSharedAddresses.length ? root.selectedSharedAddresses
: filteredAccountsModel.count ? ModelUtils.modelToFlatArray(filteredAccountsModel, "address")
: []
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
readonly property var cancelButton: StatusFlatButton {
visible: root.isEditMode
borderColor: Theme.palette.baseColor2
text: qsTr("Cancel")
onClicked: root.close()
}
sorters: [
ExpressionSorter {
function isGenerated(modelData) {
return modelData.walletType === Constants.generatedWalletType
readonly property var saveButton: StatusButton {
enabled: d.dirty
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 isGenerated(modelLeft)
}
},
RoleSorter {
roleName: "position"
},
RoleSorter {
roleName: "name"
return ""
}
]
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 {
@ -184,14 +192,16 @@ Control {
Layout.fillHeight: !hasPermissions
model: root.walletAccountsModel
walletAssetsModel: root.walletAssetsModel
selectedSharedAddresses: d.initialSelectedSharedAddresses
selectedAirdropAddress: d.initialSelectedAirdropAddress
onAddressesChanged: accountSelector.applyChange()
function applyChange() {
root.selectedSharedAddresses = selectedSharedAddresses
root.selectedAirdropAddress = selectedAirdropAddress
root.sharedAddressesChanged(selectedAirdropAddress, selectedSharedAddresses)
selectedSharedAddressesMap: root.selectedSharedAddressesMap
onToggleAddressSelection: {
root.toggleAddressSelection(keyUid, address)
}
onAirdropAddressSelected: {
root.airdropAddressSelected(address)
}
getCurrencyAmount: function (balance, symbol){
return root.getCurrencyAmount(balance, symbol)
}

View File

@ -13,46 +13,67 @@ import SortFilterProxyModel 0.2
ColumnLayout {
id: root
required property string componentUid
required property bool isEditMode
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 bool allSigned: regularKeypairs.visible == d.sharedAddressesForAllNonKeycardKeypairsSigned &&
keycardKeypairs.visible == d.allKeycardKeypairsSigned
signal joinCommunity()
signal signSharedAddressesForAllNonKeycardKeypairs()
signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid)
function sharedAddressesForAllNonKeycardKeypairsSigned() {
d.sharedAddressesForAllNonKeycardKeypairsSigned = true
function allSigned() {
d.allSigned = true
}
QtObject {
id: d
property bool sharedAddressesForAllNonKeycardKeypairsSigned: false
property bool allKeycardKeypairsSigned: false
readonly property int selectedSharedAddressesCount: root.selectedSharedAddressesMap.size
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 {
enabled: root.allSigned
text: qsTr("Share your addresses to join")
enabled: d.allSigned
text: {
if (d.selectedSharedAddressesCount === root.totalNumOfAddressesForSharing) {
return qsTr("Share all addresses to join")
}
return qsTr("Share %n address(s) to join", "", d.selectedSharedAddressesCount)
}
onClicked: {
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 {
@ -61,43 +82,41 @@ ColumnLayout {
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 {
Layout.fillWidth: true
visible: regularKeypairs.visible
visible: nonKeycardProfileKeypair.visible
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Keypairs we need an authentication for")
text: qsTr("Stored on device")
font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1
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 {
id: regularKeypairs
id: nonKeycardProfileKeypair
Layout.fillWidth: true
Layout.preferredHeight: regularKeypairs.contentHeight
visible: regularKeypairs.model.count > 0
Layout.preferredHeight: nonKeycardProfileKeypair.contentHeight
visible: nonKeycardProfileKeypair.model.count > 0
spacing: Style.current.padding
model: SortFilterProxyModel {
sourceModel: root.keypairSigningModel
filters: ExpressionFilter {
expression: !model.keyPair.migratedToKeycard
expression: model.keyPair.keyUid === userProfile.keyUid && !userProfile.isKeycardUser
}
}
delegate: KeyPairItem {
id: kpOnDeviceDelegate
width: ListView.view.width
sensor.hoverEnabled: false
additionalInfoForProfileKeypair: ""
@ -109,22 +128,80 @@ ColumnLayout {
keyPairImage: model.keyPair.image
keyPairDerivedFrom: model.keyPair.derivedFrom
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 {
visible: regularKeypairs.visible && keycardKeypairs.visible
visible: nonKeycardProfileKeypair.visible
Layout.fillWidth: true
Layout.preferredHeight: Style.current.xlPadding
}
StatusBaseText {
RowLayout {
Layout.fillWidth: true
visible: keycardKeypairs.visible
text: qsTr("Keypairs that need to be singed using appropriate Keycard")
font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1
wrapMode: Text.WordWrap
StatusBaseText {
text: qsTr("Stored on keycard")
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 {
@ -140,6 +217,7 @@ ColumnLayout {
}
}
delegate: KeyPairItem {
id: kpOnKeycardDelegate
width: ListView.view.width
sensor.hoverEnabled: !model.keyPair.ownershipVerified
additionalInfoForProfileKeypair: ""
@ -153,28 +231,173 @@ ColumnLayout {
keyPairAccounts: model.keyPair.accounts
components: [
StatusBaseText {
font.weight: Font.Medium
font.underline: mouseArea.containsMouse
font.pixelSize: Theme.primaryTextFontSize
color: model.keyPair.ownershipVerified? Theme.palette.baseColor1 : Theme.palette.primaryColor1
text: model.keyPair.ownershipVerified? qsTr("Signed") : qsTr("Sign")
MouseArea {
id: mouseArea
anchors.fill: parent
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)
StatusButton {
text: qsTr("Authenticate")
visible: !model.keyPair.ownershipVerified
icon.name: "keycard"
onClicked: {
if (model.keyPair.keyUid === userProfile.keyUid) {
root.signProfileKeypairAndAllNonKeycardKeypairs()
return
}
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
/****************************************************
This file is not in use any more.
TODO: remove it
****************************************************/
StatusDialog {
id: root
@ -35,7 +41,7 @@ StatusDialog {
signal prepareForSigning(string airdropAddress, var sharedAddresses)
signal editRevealedAddresses()
signal signSharedAddressesForAllNonKeycardKeypairs()
signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid)
function setOldSharedAddresses(oldSharedAddresses) {
@ -71,6 +77,8 @@ StatusDialog {
property var oldSharedAddresses
property string oldAirdropAddress
property int selectedSharedAddressesCount
property var selectAddressesPanelButtons: ObjectModel {}
readonly property var signingPanelButtons: ObjectModel {
StatusFlatButton {
@ -136,6 +144,7 @@ StatusDialog {
d.displaySigningPanel = true
}
onSharedAddressesChanged: {
d.selectedSharedAddressesCount = sharedAddresses.length
root.sharedAddressesChanged(airdropAddress, sharedAddresses)
}
onClose: root.close()
@ -147,12 +156,16 @@ StatusDialog {
Component {
id: sharedAddressesSigningPanelComponent
SharedAddressesSigningPanel {
SharedAddressesSigningPanel {
totalNumOfAddressesForSharing: root.walletAccountsModel.count
numOfSelectedAddressesForSharing: d.selectedSharedAddressesCount
communityName: root.communityName
keypairSigningModel: root.keypairSigningModel
onSignSharedAddressesForAllNonKeycardKeypairs: {
root.signSharedAddressesForAllNonKeycardKeypairs()
onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {

View File

@ -305,7 +305,7 @@ Item {
chatListPopupMenu: 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
openHandler: function (id) {
@ -536,12 +536,14 @@ Item {
isInvitationPending: d.invitationPending
requirementsCheckPending: root.store.requirementsCheckPending
name: communityData.name
communityName: communityData.name
introMessage: communityData.introMessage
imageSrc: communityData.image
communityIcon: communityData.image
accessType: communityData.access
loginType: root.store.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
permissionsModel: {
root.store.prepareTokenModelForCommunity(communityData.id)
@ -560,8 +562,8 @@ Item {
communityIntroDialog.keypairSigningModel = root.store.communitiesModuleInst.keypairsSigningModel
}
onSignSharedAddressesForAllNonKeycardKeypairs: {
root.store.signSharedAddressesForAllNonKeycardKeypairs()
onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.store.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {
@ -589,9 +591,21 @@ Item {
Connections {
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) {
communityIntroDialog.replaceLoader.item.sharedAddressesForAllNonKeycardKeypairsSigned()
communityIntroDialog.replaceLoader.item.allSigned()
}
}
}
@ -628,7 +642,7 @@ Item {
property int channelPosition: -1
property var deleteChatConfirmationDialog
onCreateCommunityChannel: function (chName, chDescription, chEmoji, chColor,
chCategoryId, hideIfPermissionsNotMet) {
root.store.createCommunityChannel(chName, chDescription, chEmoji, chColor,
@ -649,7 +663,7 @@ Item {
onAddPermissions: function (permissions) {
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].isPrivate,
permissions[i].channelsListModel)

View File

@ -211,9 +211,9 @@ SettingsContentBase {
Global.openPopup(communityIntroDialogPopup, {
communityId: communityId,
isInvitationPending: root.rootStore.isMyCommunityRequestPending(communityId),
name: name,
communityName: name,
introMessage: introMessage,
imageSrc: imageSrc,
communityIcon: imageSrc,
accessType: accessType
})
}
@ -236,8 +236,9 @@ SettingsContentBase {
}
}
loginType: chatStore.loginType
walletAccountsModel: WalletStore.RootStore.nonWatchAccounts
canProfileProveOwnershipOfProvidedAddressesFn: WalletStore.RootStore.canProfileProveOwnershipOfProvidedAddresses
walletAssetsModel: walletAssetsStore.groupedAccountAssetsModel
requirementsCheckPending: root.rootStore.requirementsCheckPending
permissionsModel: {
@ -257,8 +258,8 @@ SettingsContentBase {
communityIntroDialog.keypairSigningModel = chatStore.communitiesModuleInst.keypairsSigningModel
}
onSignSharedAddressesForAllNonKeycardKeypairs: {
chatStore.signSharedAddressesForAllNonKeycardKeypairs()
onSignProfileKeypairAndAllNonKeycardKeypairs: {
chatStore.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {
@ -280,9 +281,21 @@ SettingsContentBase {
Connections {
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) {
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) {
localAccountSensitiveSettings.hideSignPhraseModal = value;
}

View File

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

View File

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

View File

@ -18,48 +18,98 @@ import SortFilterProxyModel 0.2
StatusStackModal {
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 int accessType
property url imageSrc
property bool isInvitationPending: false
property int loginType: Constants.LoginType.Password
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 assetsModel
required property var collectiblesModel
required property bool requirementsCheckPending
property var keypairSigningModel
property var currentSharedAddresses: []
onCurrentSharedAddressesChanged: d.reEvaluateModels()
property string currentAirdropAddress: ""
onCurrentAirdropAddressChanged: d.reEvaluateModels()
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 joinCommunity()
signal signSharedAddressesForAllNonKeycardKeypairs()
signal editRevealedAddresses()
signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid)
signal cancelMembershipRequest()
signal sharedAddressesUpdated(var sharedAddresses)
width: 640 // by design
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]
finishButton: StatusButton {
text: root.isInvitationPending ?
qsTr("Cancel Membership Request")
: root.accessType === Constants.communityChatOnRequestAccess?
qsTr("Prove ownership")
: qsTr("Join %1").arg(root.name)
text: {
if (root.isInvitationPending) {
return qsTr("Cancel Membership Request")
} else if (root.accessType === Constants.communityChatOnRequestAccess) {
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
: 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: {
if (root.isInvitationPending) {
root.cancelMembershipRequest()
@ -67,15 +117,48 @@ StatusStackModal {
return
}
root.prepareForSigning(d.selectedAirdropAddress, d.selectedSharedAddresses)
root.replace(sharedAddressesSigningPanelComponent)
d.proceedToSigningOrSubmitRequest(d.communityIntroUid)
}
}
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 {
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
sorters: [
ExpressionSorter {
@ -96,58 +179,188 @@ StatusStackModal {
]
}
// all non-watched addresses by default, unless selected otherwise below in SharedAddressesPanel
property var selectedSharedAddresses: tempAddressesModel.count ? ModelUtils.modelToFlatArray(tempAddressesModel, "address") : []
property string selectedAirdropAddress: selectedSharedAddresses.length ? selectedSharedAddresses[0] : ""
function proceedToSigningOrSubmitRequest(uidOfComponentThisFunctionIsCalledFrom) {
const selected = d.getSelectedAddresses()
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 {
height: finishButton.height
visible: !root.isInvitationPending && !root.replaceItem
borderColor: Theme.palette.baseColor2
borderColor: "transparent"
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 {
id: sharedAddressesPanelComponent
SharedAddressesPanel {
communityName: root.name
communityIcon: root.imageSrc
loginType: root.loginType
componentUid: d.shareAddressesUid
isEditMode: root.isEditMode
communityName: root.communityName
communityIcon: root.communityIcon
requirementsCheckPending: root.requirementsCheckPending
walletAccountsModel: SortFilterProxyModel {
sourceModel: root.walletAccountsModel
sorters: [
ExpressionSorter {
function isGenerated(modelData) {
return modelData.walletType === Constants.generatedWalletType
}
expression: {
return isGenerated(modelLeft)
}
},
RoleSorter {
roleName: "position"
},
RoleSorter {
roleName: "name"
}
]
}
walletAccountsModel: d.initialAddressesModel
selectedSharedAddressesMap: d.selectedSharedAddressesMap
currentSharedAddressesMap: d.currentSharedAddressesMap
totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
profileProvesOwnershipOfSelectedAddresses: root.profileProvesOwnershipOfSelectedAddresses
allAddressesToRevealBelongToSingleNonProfileKeypair: root.allAddressesToRevealBelongToSingleNonProfileKeypair
walletAssetsModel: root.walletAssetsModel
permissionsModel: root.permissionsModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
onClose: {
root.close()
}
onToggleAddressSelection: {
d.toggleAddressSelection(keyUid, address)
const obj = d.getSelectedAddresses()
root.sharedAddressesUpdated(obj.addresses)
}
onAirdropAddressSelected: {
d.selectAirdropAddress(address)
}
onShareSelectedAddressesClicked: {
d.selectedAirdropAddress = airdropAddress
d.selectedSharedAddresses = sharedAddresses
root.replaceItem = undefined // go back, unload us
}
onSharedAddressesChanged: {
root.sharedAddressesUpdated(sharedAddresses)
d.proceedToSigningOrSubmitRequest(d.shareAddressesUid)
}
getCurrencyAmount: function (balance, symbol){
return root.getCurrencyAmount(balance, symbol)
}
@ -158,10 +371,16 @@ StatusStackModal {
id: sharedAddressesSigningPanelComponent
SharedAddressesSigningPanel {
componentUid: d.signingPanelUid
isEditMode: root.isEditMode
totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
selectedSharedAddressesMap: d.selectedSharedAddressesMap
communityName: root.communityName
keypairSigningModel: root.keypairSigningModel
onSignSharedAddressesForAllNonKeycardKeypairs: {
root.signSharedAddressesForAllNonKeycardKeypairs()
onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {
@ -169,7 +388,11 @@ StatusStackModal {
}
onJoinCommunity: {
root.joinCommunity()
if (root.isEditMode) {
root.editRevealedAddresses()
} else {
root.joinCommunity()
}
root.close()
}
}
@ -191,12 +414,12 @@ StatusStackModal {
visible: ((image.status == Image.Loading) ||
(image.status == Image.Ready)) &&
!image.isError
image.source: root.imageSrc
image.source: root.communityIcon
}
StatusBaseText {
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
wrapMode: Text.WordWrap
}