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.",