Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: register proposal via deep links #5500

Merged
merged 5 commits into from Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 6 additions & 10 deletions docs/specifications/deep-links.md
Expand Up @@ -152,7 +152,7 @@ This operation brings the user to the register proposal popup:
The deep link structure is as follows:

```
firefly://governance/registerProposal?eventId=<eventId>[&nodeUrl=<nodeUrl>]
firefly://governance/registerProposal?eventId=<eventId>&nodeUrl=<nodeUrl>
```

The following parameters are **required**:
Expand All @@ -161,27 +161,23 @@ The following parameters are **required**:

The following parameter(s) are **optional**:

- `nodeUrl` - the specific node that is tracking the proposal's correspoding participation event
- `nodeUrl` - the specific node that is tracking the proposal's corresponding participation event

:::info
If your node requires authentication (e.g. username / password, JWT), it will require the user
to manually enter the information.
If the node requires authentication (e.g. username and password, JWT), the user will be required
to manually enter the information.
:::

Example:

[!button Click me!](firefly://governance/registerProposal?eventId=TODO&nodeUrl=TODO)
[!button Click me!](firefly://governance/registerProposal?eventId=0x6d27606a773a3c87c151af09ad58ddc831864e2141ef598075dc24be5668ca7f7f&nodeUrl=https://api.testnet.shimmer.network)

Source:

```
firefly://governance/registerProposal?eventId=TODO&nodeUrl=TODO
firefly://governance/registerProposal?eventId=0x6d27606a773a3c87c151af09ad58ddc831864e2141ef598075dc24be5668ca7f7f&nodeUrl=https://api.testnet.shimmer.network
```

#### Vote

Coming :soon:

<style>
.image {
margin: auto;
Expand Down
Binary file modified docs/static/register-proposal-popup.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -8,8 +8,8 @@
import { registerParticipationEvent } from '@core/profile-manager/api'
import { isValidUrl } from '@core/utils/validation'

let eventId: string
let nodeUrl: string
export let eventId: string
export let nodeUrl: string

let eventIdError: string
let nodeUrlError: string
Expand Down
1 change: 1 addition & 0 deletions packages/shared/lib/auxiliary/deep-link/constants/index.ts
@@ -0,0 +1 @@
export * from './url-cleanup-regex.constant'
@@ -0,0 +1 @@
export const URL_CLEANUP_REGEX = /^\/+|\/+$/g
Expand Up @@ -2,5 +2,6 @@
* The contexts available in an IOTA deep link.
*/
export enum DeepLinkContext {
Governance = 'governance',
Wallet = 'wallet',
}
@@ -0,0 +1,6 @@
/**
* The operations available within the governance context.
*/
export enum GovernanceOperation {
RegisterProposal = 'registerProposal',
}
2 changes: 2 additions & 0 deletions packages/shared/lib/auxiliary/deep-link/enums/index.ts
@@ -1,3 +1,5 @@
export * from './deep-link-context.enum'
export * from './governance-operation.enum'
export * from './register-proposal-parameter.enum'
export * from './send-operation-parameter.enum'
export * from './wallet-operation.enum'
@@ -0,0 +1,7 @@
/**
* The query parameters available in a register proposal operation.
*/
export enum RegisterProposalOperationParameter {
EventId = 'eventId',
NodeUrl = 'nodeUrl',
}
@@ -1,7 +1,6 @@
/**
* The query parameters available in a send operation.
*/

export enum SendOperationParameter {
Address = 'address',
AssetId = 'assetId',
Expand Down
@@ -0,0 +1,34 @@
import { openPopup } from '@auxiliary/popup/actions'
import { addError } from '@core/error/stores'
import { localize } from '@core/i18n'

import { URL_CLEANUP_REGEX } from '../../constants'
import { GovernanceOperation } from '../../enums'
import { handleDeepLinkRegisterProposalOperation } from './operations'

export function handleDeepLinkGovernanceContext(url: URL): void {
const pathnameParts = url.pathname.replace(URL_CLEANUP_REGEX, '').split('/')
try {
if (pathnameParts.length === 0 || !pathnameParts[0]) {
throw new Error('No operation specified in the URL')
}
switch (pathnameParts[0]) {
case GovernanceOperation.RegisterProposal:
handleDeepLinkRegisterProposalOperation(url.searchParams)
break
default: {
throw new Error(
localize('notifications.deepLinkingRequest.governance.unrecognizedOperation', {
values: { operation: pathnameParts[0] },
})
)
}
}
} catch (err) {
openPopup({
type: 'deepLinkError',
props: { error: err, url },
})
addError({ time: Date.now(), type: 'deepLink', message: `Error handling deep link. ${err.message}` })
}
}
@@ -0,0 +1 @@
export * from './operations'
@@ -0,0 +1,35 @@
import { showAppNotification } from '@auxiliary/notification/actions'
import { closePopup, openPopup } from '@auxiliary/popup/actions'
import { isValidUrl } from '@core/utils/validation'
import { isProposalAlreadyRegistered, isValidProposalId } from '@contexts/governance/utils'

import { RegisterProposalOperationParameter } from '../../../enums'

export function handleDeepLinkRegisterProposalOperation(searchParams: URLSearchParams): void {
const eventId = searchParams.get(RegisterProposalOperationParameter.EventId)
if (!isValidProposalId(eventId)) {
throw new Error('Invalid proposal ID')
} else if (isProposalAlreadyRegistered(eventId)) {
/**
* NOTE: If we throw an error as normal, it will be handled and displayed in the "failed link"
* popup.
*/
showAppNotification({
type: 'warning',
alert: true,
message: 'This proposal has already been registered',
})
closePopup()
return
}

const nodeUrl = searchParams.get(RegisterProposalOperationParameter.NodeUrl)
if (!isValidUrl(nodeUrl)) {
throw new Error('Invalid node URL')
}

openPopup({
type: 'registerProposal',
props: { eventId, nodeUrl },
})
}
@@ -0,0 +1 @@
export * from './handleDeepLinkRegisterProposalOperation'
15 changes: 11 additions & 4 deletions packages/shared/lib/auxiliary/deep-link/handlers/handleDeepLink.ts
@@ -1,14 +1,17 @@
import { get } from 'svelte/store'

import { addError } from '@core/error'
import { DashboardRoute, dashboardRouter } from '@core/router'
import { closePopup, openPopup } from '@auxiliary/popup/actions'
import { addError } from '@core/error/stores'
import { visibleActiveAccounts } from '@core/profile/stores'
import { dashboardRouter } from '@core/router/routers'
import { DashboardRoute } from '@core/router/enums'

import { resetDeepLink } from '../actions'
import { DeepLinkContext } from '../enums'
import { isDeepLinkRequestActive } from '../stores'

import { handleDeepLinkGovernanceContext } from './governance/handleDeepLinkGovernanceContext'
import { handleDeepLinkWalletContext } from './wallet/handleDeepLinkWalletContext'
import { closePopup, openPopup } from '@auxiliary/popup'
import { visibleActiveAccounts } from '@core/profile/stores/active-accounts.store'

/**
* Parses an IOTA deep link, i.e. a URL that begins with the app protocol i.e "firefly://".
Expand Down Expand Up @@ -56,6 +59,10 @@ function handleDeepLinkForHostname(url: URL): void {
get(dashboardRouter).goTo(DashboardRoute.Wallet)
handleDeepLinkWalletContext(url)
break
case DeepLinkContext.Governance:
get(dashboardRouter).goTo(DashboardRoute.Governance)
handleDeepLinkGovernanceContext(url)
break
default:
throw new Error(`Unrecognized context '${url.host}'`)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/lib/auxiliary/deep-link/handlers/index.ts
@@ -1,2 +1,4 @@
export * from './governance'
export * from './wallet'

export * from './handleDeepLink'
@@ -1,7 +1,8 @@
import { addError } from '@core/error'
import { openPopup } from '@auxiliary/popup/actions'
import { addError } from '@core/error/stores'
import { localize } from '@core/i18n'
import { openPopup } from '@auxiliary/popup'

import { URL_CLEANUP_REGEX } from '../../constants'
import { WalletOperation } from '../../enums'
import { handleDeepLinkSendConfirmationOperation, handleDeepLinkSendFormOperation } from './operations'

Expand All @@ -16,10 +17,10 @@ import { handleDeepLinkSendConfirmationOperation, handleDeepLinkSendFormOperatio
*/
export function handleDeepLinkWalletContext(url: URL): void {
// Remove any leading and trailing slashes
const pathnameParts = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
const pathnameParts = url.pathname.replace(URL_CLEANUP_REGEX, '').split('/')
try {
if (pathnameParts.length === 0 || !pathnameParts[0]) {
throw new Error('No operation specified in the url')
throw new Error('No operation specified in the URL')
}
switch (pathnameParts[0]) {
case WalletOperation.SendForm:
Expand Down
1 change: 1 addition & 0 deletions packages/shared/lib/auxiliary/deep-link/index.ts
@@ -1,4 +1,5 @@
export * from './actions'
export * from './constants'
export * from './enums'
export * from './errors'
export * from './handlers'
Expand Down
1 change: 1 addition & 0 deletions packages/shared/lib/contexts/governance/constants/index.ts
@@ -1,2 +1,3 @@
export * from './participate-tag-hex.constant'
export * from './proposal-id-regex.constant'
export * from './proposal-status-poll-interval.constant'
@@ -0,0 +1 @@
export const PROPOSAL_ID_REGEX = /(0x)[0-9A-Fa-f]{64}/g
4 changes: 4 additions & 0 deletions packages/shared/lib/contexts/governance/index.ts
@@ -1,2 +1,6 @@
export * from './actions'
export * from './constants'
export * from './enums'
export * from './interfaces'
export * from './stores'
export * from './utils'
2 changes: 2 additions & 0 deletions packages/shared/lib/contexts/governance/utils/index.ts
Expand Up @@ -4,5 +4,7 @@ export * from './getNumberOfVotingProposals'
export * from './getTotalNumberOfProposals'
export * from './isParticipationOutput'
export * from './isProposalActive'
export * from './isProposalAlreadyRegistered'
export * from './isValidProposalId'
export * from './isVotingForProposal'
export * from './isVotingForSelectedProposal'
@@ -0,0 +1,7 @@
import { get } from 'svelte/store'

import { proposalsState } from '../stores'

export function isProposalAlreadyRegistered(proposalId: string): boolean {
return proposalId in get(proposalsState)
Tuditi marked this conversation as resolved.
Show resolved Hide resolved
}
@@ -0,0 +1,5 @@
import { PROPOSAL_ID_REGEX } from '../constants'

export function isValidProposalId(id: string): boolean {
return PROPOSAL_ID_REGEX.test(id)
}
3 changes: 3 additions & 0 deletions packages/shared/locales/en.json
Expand Up @@ -1588,6 +1588,9 @@
"invalidFormat": "The deep link you followed is invalid",
"invalidAmount": "The amount in deep link is not an integer number {amount}",
"invalidSurplus": "The surplus in deep link is not a number {surplus}",
"governance": {
"unrecognizedOperation": "Unrecognized Governance operation: {operation}"
},
"wallet": {
"send": {
"success": "Payment details added from deep link"
Expand Down