Skip to content

Commit 7d5fd2d

Browse files
feat: support adding custom expiration time unlock condition in deeplinks (#7739)
* feat: add expiration date to deeplinks * fix: cleanup * fix: cleanup * fix: improve timestamp, use unix and allow 1h, 1d... * feat: update handbook * fix: update sendConfirmation flow and split constnats,enums.. in different files * feat: remove debris * fix: initial expiration component calculation --------- Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
1 parent 0130397 commit 7d5fd2d

16 files changed

+118
-9
lines changed

Diff for: docs/specifications/deep-links.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ This operation brings the user to the send confirmation popup:
103103
The deep link structure is as follows:
104104

105105
```
106-
firefly://wallet/sendConfirmation?address=<address>&amount=<amount>[&unit=<unit>][&assetId=<assetId>][&metadata=<metadata>][&tag=<tag>][&giftStorageDeposit=<true|false>][&disableToggleGift=<true|false>][&disableChangeExpiration=<true|false>][&surplus=<surplus>]
106+
firefly://wallet/sendConfirmation?address=<address>&amount=<amount>[&unit=<unit>][&assetId=<assetId>][&metadata=<metadata>][&tag=<tag>][&giftStorageDeposit=<true|false>][&disableToggleGift=<true|false>][&disableChangeExpiration=<true|false>][&surplus=<surplus>][&expiration=<expiration>]
107107
```
108108

109109
The following parameters are **required**:
@@ -123,15 +123,16 @@ The following parameters are **optional**:
123123
- `disableToggleGift` - prevents the user from being able to toggle the option to gift the storage deposit
124124
- `disableChangeExpiration` - prevents the user from being able to change the expiration time of the transaction
125125
- `surplus` - send additional amounts of the base token when transferring native tokens
126+
- `expiration` - the expiration time of the transaction, e.g. `1w`, `2d`, `5h` or `10m`. Also accepts a UNIX timestamp in milliseconds.
126127

127128
Example:
128129

129-
[!button Click me!](firefly://wallet/sendForm?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money)
130+
[!button Click me!](firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money&expiration=1h)
130131

131132
Source:
132133

133134
```
134-
firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money
135+
firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money&expiration=1h
135136
```
136137

137138
### Collectibles

Diff for: packages/desktop/components/popups/send/SendConfirmationPopup.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
onMount(async () => {
8686
await updateStorageDeposit()
8787
88-
if (isSendAndClosePopup) {
88+
if (isSendAndClosePopup || expirationDate) {
8989
// Needed after 'return from stronghold' to SHOW to correct expiration date before output is sent
9090
initialExpirationDate = getInitialExpirationDate(
9191
expirationDate,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { TimeUnit } from '../enums'
2+
3+
export const EXPIRATION_DATE_REGEX = new RegExp(`^(\\d+)(${Object.values(TimeUnit).join('|')})$`)
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export * from './expiration-date-regex.constant'
2+
export * from './time-unit-ms.constant'
13
export * from './url-cleanup-regex.constant'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {
2+
MILLISECONDS_PER_DAY,
3+
MILLISECONDS_PER_HOUR,
4+
MILLISECONDS_PER_MINUTE,
5+
MILLISECONDS_PER_WEEK,
6+
} from 'shared/lib/core/utils'
7+
import { TimeUnit } from '../enums'
8+
9+
export const TIME_UNIT_MS_MAP: Record<TimeUnit, number> = {
10+
[TimeUnit.Weeks]: MILLISECONDS_PER_WEEK,
11+
[TimeUnit.Days]: MILLISECONDS_PER_DAY,
12+
[TimeUnit.Hours]: MILLISECONDS_PER_HOUR,
13+
[TimeUnit.Minutes]: MILLISECONDS_PER_MINUTE,
14+
}
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
export * from './add-proposal-parameter.enum'
12
export * from './deep-link-context.enum'
23
export * from './governance-operation.enum'
3-
export * from './add-proposal-parameter.enum'
44
export * from './send-operation-parameter.enum'
5+
export * from './time-unit.enum'
56
export * from './wallet-operation.enum'

Diff for: packages/shared/lib/auxiliary/deep-link/enums/send-operation-parameter.enum.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export enum SendOperationParameter {
1212
Surplus = 'surplus',
1313
DisableToggleGift = 'disableToggleGift',
1414
DisableChangeExpiration = 'disableChangeExpiration',
15+
Expiration = 'expiration',
1516
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum TimeUnit {
2+
Weeks = 'w',
3+
Days = 'd',
4+
Hours = 'h',
5+
Minutes = 'm',
6+
}
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
export * from './amount-not-an-integer.error'
22
export * from './invalid-address.error'
33
export * from './invalid-asset-id.error'
4+
export * from './invalid-expiration-date.error'
45
export * from './metadata-length.error'
56
export * from './no-address-specified.error'
6-
export * from './tag-length.error'
7+
export * from './past-expiration-date.error'
78
export * from './surplus-not-a-number.error'
89
export * from './surplus-not-supported.error'
10+
export * from './tag-length.error'
911
export * from './unknown-asset.error'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { BaseError } from '@core/error'
2+
import { localize } from '@core/i18n'
3+
4+
export class InvalidExpirationDateError extends BaseError {
5+
constructor() {
6+
const message = localize('error.send.invalidExpirationDate')
7+
super({
8+
message,
9+
showNotification: true,
10+
saveToErrorLog: false,
11+
logToConsole: true,
12+
})
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { BaseError } from '@core/error'
2+
import { localize } from '@core/i18n'
3+
4+
export class PastExpirationDateError extends BaseError {
5+
constructor() {
6+
const message = localize('error.send.pastExpirationDate')
7+
super({
8+
message,
9+
showNotification: true,
10+
saveToErrorLog: false,
11+
logToConsole: true,
12+
})
13+
}
14+
}

Diff for: packages/shared/lib/auxiliary/deep-link/handlers/wallet/operations/handleDeepLinkSendConfirmationOperation.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { PopupId, openPopup } from '@auxiliary/popup'
2+
import { getActiveNetworkId } from '@core/network/utils/getNetworkId'
3+
import { getNetworkHrp } from '@core/profile/actions'
24
import { getByteLengthOfString, isStringTrue, isValidBech32AddressAndPrefix, validateAssetId } from '@core/utils'
35
import {
46
NewTransactionDetails,
@@ -21,9 +23,7 @@ import {
2123
TagLengthError,
2224
UnknownAssetError,
2325
} from '../../../errors'
24-
import { getRawAmountFromSearchParam } from '../../../utils'
25-
import { getNetworkHrp } from '@core/profile/actions'
26-
import { getActiveNetworkId } from '@core/network/utils/getNetworkId'
26+
import { getExpirationDateFromSearchParam, getRawAmountFromSearchParam } from '../../../utils'
2727

2828
export function handleDeepLinkSendConfirmationOperation(searchParams: URLSearchParams): void {
2929
const transactionDetails = parseSendConfirmationOperation(searchParams)
@@ -94,6 +94,7 @@ function parseSendConfirmationOperation(searchParams: URLSearchParams): NewTrans
9494
const giftStorageDeposit = isStringTrue(searchParams.get(SendOperationParameter.GiftStorageDeposit))
9595
const disableToggleGift = isStringTrue(searchParams.get(SendOperationParameter.DisableToggleGift))
9696
const disableChangeExpiration = isStringTrue(searchParams.get(SendOperationParameter.DisableChangeExpiration))
97+
const expirationDate = getExpirationDateFromSearchParam(searchParams.get(SendOperationParameter.Expiration))
9798

9899
return {
99100
type: NewTransactionType.TokenTransfer,
@@ -107,5 +108,6 @@ function parseSendConfirmationOperation(searchParams: URLSearchParams): NewTrans
107108
...(surplus && { surplus }),
108109
...(disableToggleGift && { disableToggleGift }),
109110
...(disableChangeExpiration && { disableChangeExpiration }),
111+
...(expirationDate && { expirationDate }),
110112
}
111113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { convertUnixTimestampToDate } from 'shared/lib/core/utils'
2+
import { EXPIRATION_DATE_REGEX, TIME_UNIT_MS_MAP } from '../constants'
3+
import { TimeUnit } from '../enums'
4+
import { InvalidExpirationDateError, PastExpirationDateError } from '../errors'
5+
6+
export function getExpirationDateFromSearchParam(expirationDate: string): Date | undefined {
7+
if (!expirationDate) {
8+
return undefined
9+
}
10+
11+
// Check if it's a Unix timestamp (numeric value)
12+
if (!isNaN(Number(expirationDate))) {
13+
const expirationTimestamp = parseInt(expirationDate)
14+
const expirationDateTime = convertUnixTimestampToDate(expirationTimestamp) // Convert seconds to milliseconds
15+
if (isNaN(expirationDateTime.getTime())) {
16+
throw new InvalidExpirationDateError()
17+
} else if (expirationDateTime.getTime() < Date.now()) {
18+
throw new PastExpirationDateError()
19+
} else {
20+
return expirationDateTime
21+
}
22+
}
23+
24+
// Validate expiration date format for relative time
25+
const regexMatch = EXPIRATION_DATE_REGEX.exec(expirationDate)
26+
27+
if (!regexMatch) {
28+
throw new InvalidExpirationDateError()
29+
}
30+
31+
const value = parseInt(regexMatch[1])
32+
const unit = regexMatch[2] as TimeUnit
33+
34+
const selectedTimeUnitValue = TIME_UNIT_MS_MAP[unit]
35+
36+
if (selectedTimeUnitValue === undefined) {
37+
throw new InvalidExpirationDateError()
38+
}
39+
40+
const expirationDateTime = new Date(Date.now() + value * selectedTimeUnitValue)
41+
42+
return expirationDateTime
43+
}
+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './getExpirationDateFromSearchParam'
12
export * from './getRawAmountFromSearchParam'

Diff for: packages/shared/lib/core/utils/constants/time.constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ export const DAYS_PER_YEAR = 365
99
// DERIVED
1010
export const SECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE
1111
export const MILLISECONDS_PER_DAY = SECONDS_PER_DAY * MILLISECONDS_PER_SECOND
12+
export const MILLISECONDS_PER_WEEK = SECONDS_PER_DAY * DAYS_PER_WEEK * MILLISECONDS_PER_SECOND
13+
export const MILLISECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * MILLISECONDS_PER_SECOND
14+
export const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND

Diff for: packages/shared/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,8 @@
19441944
"wrongAddressPrefix": "Addresses start with the prefix {prefix}.",
19451945
"wrongAddressFormat": "The address is not correctly formatted.",
19461946
"invalidAddress": "The address is not valid.",
1947+
"invalidExpirationDate": "The expiration date is not valid.",
1948+
"pastExpirationDate": "The expiration date is in the past.",
19471949
"invalidAssetId": "The asset id is not valid.",
19481950
"unknownAsset": "The asset is not known to this account.",
19491951
"insufficientFunds": "This wallet has insufficient funds.",

0 commit comments

Comments
 (0)