diff --git a/docs/specifications/deep-links.md b/docs/specifications/deep-links.md
index 96a7fe4d4a0..bb5c8f75966 100644
--- a/docs/specifications/deep-links.md
+++ b/docs/specifications/deep-links.md
@@ -103,7 +103,7 @@ This operation brings the user to the send confirmation popup:
The deep link structure is as follows:
```
-firefly://wallet/sendConfirmation?address=
&amount=[&unit=][&assetId=][&metadata=][&tag=][&giftStorageDeposit=][&disableToggleGift=][&disableChangeExpiration=][&surplus=]
+firefly://wallet/sendConfirmation?address=&amount=[&unit=][&assetId=][&metadata=][&tag=][&giftStorageDeposit=][&disableToggleGift=][&disableChangeExpiration=][&surplus=][&expiration=]
```
The following parameters are **required**:
@@ -123,15 +123,16 @@ The following parameters are **optional**:
- `disableToggleGift` - prevents the user from being able to toggle the option to gift the storage deposit
- `disableChangeExpiration` - prevents the user from being able to change the expiration time of the transaction
- `surplus` - send additional amounts of the base token when transferring native tokens
+- `expiration` - the expiration time of the transaction, e.g. `1w`, `2d`, `5h` or `10m`. Also accepts a UNIX timestamp in milliseconds.
Example:
-[!button Click me!](firefly://wallet/sendForm?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money)
+[!button Click me!](firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&surplus=1&metadata=Take%20my%20money&expiration=1h)
Source:
```
-firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money
+firefly://wallet/sendConfirmation?address=iota1qrhacyfwlcnzkvzteumekfkrrwks98mpdm37cj4xx3drvmjvnep6xqgyzyx&amount=10&unit=Gi&giftStorageDeposit=true&disableToggleGift=true&surplus=1&metadata=Take%20my%20money&expiration=1h
```
### Collectibles
diff --git a/packages/desktop/components/popups/send/SendConfirmationPopup.svelte b/packages/desktop/components/popups/send/SendConfirmationPopup.svelte
index da41753901c..8b6a5f39382 100644
--- a/packages/desktop/components/popups/send/SendConfirmationPopup.svelte
+++ b/packages/desktop/components/popups/send/SendConfirmationPopup.svelte
@@ -85,7 +85,7 @@
onMount(async () => {
await updateStorageDeposit()
- if (isSendAndClosePopup) {
+ if (isSendAndClosePopup || expirationDate) {
// Needed after 'return from stronghold' to SHOW to correct expiration date before output is sent
initialExpirationDate = getInitialExpirationDate(
expirationDate,
diff --git a/packages/shared/lib/auxiliary/deep-link/constants/expiration-date-regex.constant.ts b/packages/shared/lib/auxiliary/deep-link/constants/expiration-date-regex.constant.ts
new file mode 100644
index 00000000000..e5f947a0ee8
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/constants/expiration-date-regex.constant.ts
@@ -0,0 +1,3 @@
+import { TimeUnit } from '../enums'
+
+export const EXPIRATION_DATE_REGEX = new RegExp(`^(\\d+)(${Object.values(TimeUnit).join('|')})$`)
diff --git a/packages/shared/lib/auxiliary/deep-link/constants/index.ts b/packages/shared/lib/auxiliary/deep-link/constants/index.ts
index fb65a20e027..b76e09a4e36 100644
--- a/packages/shared/lib/auxiliary/deep-link/constants/index.ts
+++ b/packages/shared/lib/auxiliary/deep-link/constants/index.ts
@@ -1 +1,3 @@
+export * from './expiration-date-regex.constant'
+export * from './time-unit-ms.constant'
export * from './url-cleanup-regex.constant'
diff --git a/packages/shared/lib/auxiliary/deep-link/constants/time-unit-ms.constant.ts b/packages/shared/lib/auxiliary/deep-link/constants/time-unit-ms.constant.ts
new file mode 100644
index 00000000000..97561be9ae7
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/constants/time-unit-ms.constant.ts
@@ -0,0 +1,14 @@
+import {
+ MILLISECONDS_PER_DAY,
+ MILLISECONDS_PER_HOUR,
+ MILLISECONDS_PER_MINUTE,
+ MILLISECONDS_PER_WEEK,
+} from 'shared/lib/core/utils'
+import { TimeUnit } from '../enums'
+
+export const TIME_UNIT_MS_MAP: Record = {
+ [TimeUnit.Weeks]: MILLISECONDS_PER_WEEK,
+ [TimeUnit.Days]: MILLISECONDS_PER_DAY,
+ [TimeUnit.Hours]: MILLISECONDS_PER_HOUR,
+ [TimeUnit.Minutes]: MILLISECONDS_PER_MINUTE,
+}
diff --git a/packages/shared/lib/auxiliary/deep-link/enums/index.ts b/packages/shared/lib/auxiliary/deep-link/enums/index.ts
index c91ee775505..944421bbf18 100644
--- a/packages/shared/lib/auxiliary/deep-link/enums/index.ts
+++ b/packages/shared/lib/auxiliary/deep-link/enums/index.ts
@@ -1,5 +1,6 @@
+export * from './add-proposal-parameter.enum'
export * from './deep-link-context.enum'
export * from './governance-operation.enum'
-export * from './add-proposal-parameter.enum'
export * from './send-operation-parameter.enum'
+export * from './time-unit.enum'
export * from './wallet-operation.enum'
diff --git a/packages/shared/lib/auxiliary/deep-link/enums/send-operation-parameter.enum.ts b/packages/shared/lib/auxiliary/deep-link/enums/send-operation-parameter.enum.ts
index 915e569e414..cfc20f777b6 100644
--- a/packages/shared/lib/auxiliary/deep-link/enums/send-operation-parameter.enum.ts
+++ b/packages/shared/lib/auxiliary/deep-link/enums/send-operation-parameter.enum.ts
@@ -12,4 +12,5 @@ export enum SendOperationParameter {
Surplus = 'surplus',
DisableToggleGift = 'disableToggleGift',
DisableChangeExpiration = 'disableChangeExpiration',
+ Expiration = 'expiration',
}
diff --git a/packages/shared/lib/auxiliary/deep-link/enums/time-unit.enum.ts b/packages/shared/lib/auxiliary/deep-link/enums/time-unit.enum.ts
new file mode 100644
index 00000000000..62fe4f044ec
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/enums/time-unit.enum.ts
@@ -0,0 +1,6 @@
+export enum TimeUnit {
+ Weeks = 'w',
+ Days = 'd',
+ Hours = 'h',
+ Minutes = 'm',
+}
diff --git a/packages/shared/lib/auxiliary/deep-link/errors/index.ts b/packages/shared/lib/auxiliary/deep-link/errors/index.ts
index 066181b1df4..30b8105bdb9 100644
--- a/packages/shared/lib/auxiliary/deep-link/errors/index.ts
+++ b/packages/shared/lib/auxiliary/deep-link/errors/index.ts
@@ -1,9 +1,11 @@
export * from './amount-not-an-integer.error'
export * from './invalid-address.error'
export * from './invalid-asset-id.error'
+export * from './invalid-expiration-date.error'
export * from './metadata-length.error'
export * from './no-address-specified.error'
-export * from './tag-length.error'
+export * from './past-expiration-date.error'
export * from './surplus-not-a-number.error'
export * from './surplus-not-supported.error'
+export * from './tag-length.error'
export * from './unknown-asset.error'
diff --git a/packages/shared/lib/auxiliary/deep-link/errors/invalid-expiration-date.error.ts b/packages/shared/lib/auxiliary/deep-link/errors/invalid-expiration-date.error.ts
new file mode 100644
index 00000000000..36f1516dbbb
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/errors/invalid-expiration-date.error.ts
@@ -0,0 +1,14 @@
+import { BaseError } from '@core/error'
+import { localize } from '@core/i18n'
+
+export class InvalidExpirationDateError extends BaseError {
+ constructor() {
+ const message = localize('error.send.invalidExpirationDate')
+ super({
+ message,
+ showNotification: true,
+ saveToErrorLog: false,
+ logToConsole: true,
+ })
+ }
+}
diff --git a/packages/shared/lib/auxiliary/deep-link/errors/past-expiration-date.error.ts b/packages/shared/lib/auxiliary/deep-link/errors/past-expiration-date.error.ts
new file mode 100644
index 00000000000..70f10edfe4a
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/errors/past-expiration-date.error.ts
@@ -0,0 +1,14 @@
+import { BaseError } from '@core/error'
+import { localize } from '@core/i18n'
+
+export class PastExpirationDateError extends BaseError {
+ constructor() {
+ const message = localize('error.send.pastExpirationDate')
+ super({
+ message,
+ showNotification: true,
+ saveToErrorLog: false,
+ logToConsole: true,
+ })
+ }
+}
diff --git a/packages/shared/lib/auxiliary/deep-link/handlers/wallet/operations/handleDeepLinkSendConfirmationOperation.ts b/packages/shared/lib/auxiliary/deep-link/handlers/wallet/operations/handleDeepLinkSendConfirmationOperation.ts
index c31412f621c..3911c1d5c92 100644
--- a/packages/shared/lib/auxiliary/deep-link/handlers/wallet/operations/handleDeepLinkSendConfirmationOperation.ts
+++ b/packages/shared/lib/auxiliary/deep-link/handlers/wallet/operations/handleDeepLinkSendConfirmationOperation.ts
@@ -1,4 +1,6 @@
import { PopupId, openPopup } from '@auxiliary/popup'
+import { getActiveNetworkId } from '@core/network/utils/getNetworkId'
+import { getNetworkHrp } from '@core/profile/actions'
import { getByteLengthOfString, isStringTrue, isValidBech32AddressAndPrefix, validateAssetId } from '@core/utils'
import {
NewTransactionDetails,
@@ -21,9 +23,7 @@ import {
TagLengthError,
UnknownAssetError,
} from '../../../errors'
-import { getRawAmountFromSearchParam } from '../../../utils'
-import { getNetworkHrp } from '@core/profile/actions'
-import { getActiveNetworkId } from '@core/network/utils/getNetworkId'
+import { getExpirationDateFromSearchParam, getRawAmountFromSearchParam } from '../../../utils'
export function handleDeepLinkSendConfirmationOperation(searchParams: URLSearchParams): void {
const transactionDetails = parseSendConfirmationOperation(searchParams)
@@ -94,6 +94,7 @@ function parseSendConfirmationOperation(searchParams: URLSearchParams): NewTrans
const giftStorageDeposit = isStringTrue(searchParams.get(SendOperationParameter.GiftStorageDeposit))
const disableToggleGift = isStringTrue(searchParams.get(SendOperationParameter.DisableToggleGift))
const disableChangeExpiration = isStringTrue(searchParams.get(SendOperationParameter.DisableChangeExpiration))
+ const expirationDate = getExpirationDateFromSearchParam(searchParams.get(SendOperationParameter.Expiration))
return {
type: NewTransactionType.TokenTransfer,
@@ -107,5 +108,6 @@ function parseSendConfirmationOperation(searchParams: URLSearchParams): NewTrans
...(surplus && { surplus }),
...(disableToggleGift && { disableToggleGift }),
...(disableChangeExpiration && { disableChangeExpiration }),
+ ...(expirationDate && { expirationDate }),
}
}
diff --git a/packages/shared/lib/auxiliary/deep-link/utils/getExpirationDateFromSearchParam.ts b/packages/shared/lib/auxiliary/deep-link/utils/getExpirationDateFromSearchParam.ts
new file mode 100644
index 00000000000..88320a2338c
--- /dev/null
+++ b/packages/shared/lib/auxiliary/deep-link/utils/getExpirationDateFromSearchParam.ts
@@ -0,0 +1,43 @@
+import { convertUnixTimestampToDate } from 'shared/lib/core/utils'
+import { EXPIRATION_DATE_REGEX, TIME_UNIT_MS_MAP } from '../constants'
+import { TimeUnit } from '../enums'
+import { InvalidExpirationDateError, PastExpirationDateError } from '../errors'
+
+export function getExpirationDateFromSearchParam(expirationDate: string): Date | undefined {
+ if (!expirationDate) {
+ return undefined
+ }
+
+ // Check if it's a Unix timestamp (numeric value)
+ if (!isNaN(Number(expirationDate))) {
+ const expirationTimestamp = parseInt(expirationDate)
+ const expirationDateTime = convertUnixTimestampToDate(expirationTimestamp) // Convert seconds to milliseconds
+ if (isNaN(expirationDateTime.getTime())) {
+ throw new InvalidExpirationDateError()
+ } else if (expirationDateTime.getTime() < Date.now()) {
+ throw new PastExpirationDateError()
+ } else {
+ return expirationDateTime
+ }
+ }
+
+ // Validate expiration date format for relative time
+ const regexMatch = EXPIRATION_DATE_REGEX.exec(expirationDate)
+
+ if (!regexMatch) {
+ throw new InvalidExpirationDateError()
+ }
+
+ const value = parseInt(regexMatch[1])
+ const unit = regexMatch[2] as TimeUnit
+
+ const selectedTimeUnitValue = TIME_UNIT_MS_MAP[unit]
+
+ if (selectedTimeUnitValue === undefined) {
+ throw new InvalidExpirationDateError()
+ }
+
+ const expirationDateTime = new Date(Date.now() + value * selectedTimeUnitValue)
+
+ return expirationDateTime
+}
diff --git a/packages/shared/lib/auxiliary/deep-link/utils/index.ts b/packages/shared/lib/auxiliary/deep-link/utils/index.ts
index 1174377121b..37761bf0334 100644
--- a/packages/shared/lib/auxiliary/deep-link/utils/index.ts
+++ b/packages/shared/lib/auxiliary/deep-link/utils/index.ts
@@ -1 +1,2 @@
+export * from './getExpirationDateFromSearchParam'
export * from './getRawAmountFromSearchParam'
diff --git a/packages/shared/lib/core/utils/constants/time.constants.ts b/packages/shared/lib/core/utils/constants/time.constants.ts
index d792a17f7d3..f1c2c74c93b 100644
--- a/packages/shared/lib/core/utils/constants/time.constants.ts
+++ b/packages/shared/lib/core/utils/constants/time.constants.ts
@@ -9,3 +9,6 @@ export const DAYS_PER_YEAR = 365
// DERIVED
export const SECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE
export const MILLISECONDS_PER_DAY = SECONDS_PER_DAY * MILLISECONDS_PER_SECOND
+export const MILLISECONDS_PER_WEEK = SECONDS_PER_DAY * DAYS_PER_WEEK * MILLISECONDS_PER_SECOND
+export const MILLISECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * MILLISECONDS_PER_SECOND
+export const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND
diff --git a/packages/shared/locales/en.json b/packages/shared/locales/en.json
index b76c37a5b1a..a38ed23ba77 100644
--- a/packages/shared/locales/en.json
+++ b/packages/shared/locales/en.json
@@ -1944,6 +1944,8 @@
"wrongAddressPrefix": "Addresses start with the prefix {prefix}.",
"wrongAddressFormat": "The address is not correctly formatted.",
"invalidAddress": "The address is not valid.",
+ "invalidExpirationDate": "The expiration date is not valid.",
+ "pastExpirationDate": "The expiration date is in the past.",
"invalidAssetId": "The asset id is not valid.",
"unknownAsset": "The asset is not known to this account.",
"insufficientFunds": "This wallet has insufficient funds.",