Skip to content

Commit

Permalink
feat: support adding custom expiration time unlock condition in deepl…
Browse files Browse the repository at this point in the history
…inks (#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]>
  • Loading branch information
evavirseda and begonaalvarezd authored Jan 25, 2024
1 parent 0130397 commit 7d5fd2d
Show file tree
Hide file tree
Showing 16 changed files with 118 additions and 9 deletions.
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 }),
}
}
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

0 comments on commit 7d5fd2d

Please sign in to comment.