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: support adding custom expiration time unlock condition in deeplinks #7739

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/specifications/deep-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<address>&amount=<amount>[&unit=<unit>][&assetId=<assetId>][&metadata=<metadata>][&tag=<tag>][&giftStorageDeposit=<true|false>][&disableToggleGift=<true|false>][&disableChangeExpiration=<true|false>][&surplus=<surplus>]
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>]
```

The following parameters are **required**:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TimeUnit } from '../enums'

export const EXPIRATION_DATE_REGEX = new RegExp(`^(\\d+)(${Object.values(TimeUnit).join('|')})$`)
2 changes: 2 additions & 0 deletions packages/shared/lib/auxiliary/deep-link/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './expiration-date-regex.constant'
export * from './time-unit-ms.constant'
export * from './url-cleanup-regex.constant'
Original file line number Diff line number Diff line change
@@ -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, number> = {
[TimeUnit.Weeks]: MILLISECONDS_PER_WEEK,
[TimeUnit.Days]: MILLISECONDS_PER_DAY,
[TimeUnit.Hours]: MILLISECONDS_PER_HOUR,
[TimeUnit.Minutes]: MILLISECONDS_PER_MINUTE,
}
3 changes: 2 additions & 1 deletion packages/shared/lib/auxiliary/deep-link/enums/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export enum SendOperationParameter {
Surplus = 'surplus',
DisableToggleGift = 'disableToggleGift',
DisableChangeExpiration = 'disableChangeExpiration',
Expiration = 'expiration',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum TimeUnit {
Weeks = 'w',
Days = 'd',
Hours = 'h',
Minutes = 'm',
}
4 changes: 3 additions & 1 deletion packages/shared/lib/auxiliary/deep-link/errors/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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,
})
}
}
Original file line number Diff line number Diff line change
@@ -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,
})
}
}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -107,5 +108,6 @@ function parseSendConfirmationOperation(searchParams: URLSearchParams): NewTrans
...(surplus && { surplus }),
...(disableToggleGift && { disableToggleGift }),
...(disableChangeExpiration && { disableChangeExpiration }),
...(expirationDate && { expirationDate }),
begonaalvarezd marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/shared/lib/auxiliary/deep-link/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './getExpirationDateFromSearchParam'
export * from './getRawAmountFromSearchParam'
3 changes: 3 additions & 0 deletions packages/shared/lib/core/utils/constants/time.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions packages/shared/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading