diff --git a/apps/wallet-mobile/src/features/Scan/common/mocks.ts b/apps/wallet-mobile/src/features/Scan/common/mocks.ts index e7f3ee6692..d890cd7634 100644 --- a/apps/wallet-mobile/src/features/Scan/common/mocks.ts +++ b/apps/wallet-mobile/src/features/Scan/common/mocks.ts @@ -28,6 +28,8 @@ export const codeContent = { 'web+cardano://claim/v1?code=42&faucet_url=https%3A%2F%2Ffaucet.com&memo=memo-text&message=message1&message=message2&message=message3&extra=extra', legacyCip13Transfer: 'web+cardano:addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km?extra=extra&amount=1.23&memo=memo&message=message', + yoroiPaymentRequestWithLink: + 'yoroi://yoroi-wallet.com/w1/transfer/request/ada-with-link?link=web%252Bcardano%253Aaddr1qxvn5nehgdjadqpztxqckh4yz37h0vc7rnl57x9jfaraxxe627hhjyls27xwmke4e4ewn27rv3qcntakvp7wd53dqahqxuhfua%253Famount%253D2', }, }, } as const diff --git a/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts b/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts index 728937d77f..e9fd3f86a1 100644 --- a/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts +++ b/apps/wallet-mobile/src/features/Scan/common/parsers.test.ts @@ -40,4 +40,12 @@ describe('parseScanAction', () => { params: expect.any(Object), }) }) + + it('should correctly parse a Yoroi link', () => { + const result = parseScanAction(codeContent.links.success.yoroiPaymentRequestWithLink) + expect(result).toEqual({ + action: 'launch-url', + url: codeContent.links.success.yoroiPaymentRequestWithLink, + }) + }) }) diff --git a/apps/wallet-mobile/src/features/Scan/common/parsers.ts b/apps/wallet-mobile/src/features/Scan/common/parsers.ts index 0af18bdf37..51a56ea5ef 100644 --- a/apps/wallet-mobile/src/features/Scan/common/parsers.ts +++ b/apps/wallet-mobile/src/features/Scan/common/parsers.ts @@ -1,16 +1,24 @@ import {linksCardanoModuleMaker} from '@yoroi/links' import {Links, Scan} from '@yoroi/types' +import {freeze} from 'immer' export const parseScanAction = (codeContent: string): Scan.Action => { - const isLink = codeContent.includes(':') + const isPossibleLink = codeContent.includes(':') // NOTE: if it is a string < 256 with valid characters, it'd be consider a Yoroi Receiver (wallet address | domain name) - if (!isLink) { + if (!isPossibleLink) { if (codeContent.length > 255 || !nonProtocolRegex.test(codeContent)) throw new Scan.Errors.UnknownContent() - return { + return freeze({ action: 'send-only-receiver', receiver: codeContent, - } + } as const) + } + + if (isOpenableLink(codeContent)) { + return freeze({ + action: 'launch-url', + url: codeContent, + } as const) } const cardanoLinks = linksCardanoModuleMaker() @@ -20,24 +28,33 @@ export const parseScanAction = (codeContent: string): Scan.Action => { if (parsedCardanoLink.config.authority === 'claim') { const {faucet_url: url, code, ...params} = parsedCardanoLink.params - return { - action: 'claim', - url, - code, - params, - } as const + return freeze( + { + action: 'claim', + url, + code, + params, + } as const, + true, + ) } const {address: receiver, amount, memo, message} = parsedCardanoLink.params - return { - action: 'send-single-pt', - receiver, - params: { - amount, - memo, - message, - }, - } as const + return freeze( + { + action: 'send-single-pt', + receiver, + params: { + amount, + memo, + message, + }, + } as const, + true, + ) } const nonProtocolRegex = /^[a-zA-Z0-9_\-.$]+$/ +const isOpenableLink = (content: string) => { + return content.startsWith('yoroi') || content.startsWith('https') +} diff --git a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx index 324a494f3b..02250c8596 100644 --- a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx +++ b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx @@ -3,7 +3,7 @@ import {toBigInt} from '@yoroi/common' import {useTransfer} from '@yoroi/transfer' import {Scan} from '@yoroi/types' import * as React from 'react' -import {Alert} from 'react-native' +import {Alert, Linking} from 'react-native' import {useModal} from '../../../components/Modal/ModalContext' import {useClaimErrorResolver} from '../../../features/Claim/common/useClaimErrorResolver' @@ -46,6 +46,11 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: Scan.Featu const trigger = (scanAction: Scan.Action) => { switch (scanAction.action) { + case 'launch-url': { + Linking.openURL(scanAction.url) + break + } + case 'send-single-pt': { if (insideFeature !== 'send') resetTransferState() diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 704109ea71..e7d72fcf35 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -237,6 +237,7 @@ import {ScanErrorUnknown, ScanErrorUnknownContent} from './scan/errors' import { ScanAction, ScanActionClaim, + ScanActionLaunchUrl, ScanActionSendOnlyReceiver, ScanActionSendSinglePt, ScanFeature, @@ -657,6 +658,7 @@ export namespace Scan { export type ActionClaim = ScanActionClaim export type ActionSendOnlyReceiver = ScanActionSendOnlyReceiver export type ActionSendSinglePt = ScanActionSendSinglePt + export type ActionScanLaunchUrl = ScanActionLaunchUrl } export namespace Claim { diff --git a/packages/types/src/scan/actions.ts b/packages/types/src/scan/actions.ts index a05a66da28..704265bdfa 100644 --- a/packages/types/src/scan/actions.ts +++ b/packages/types/src/scan/actions.ts @@ -22,9 +22,15 @@ export type ScanActionClaim = Readonly<{ params: Record | undefined }> +export type ScanActionLaunchUrl = Readonly<{ + action: 'launch-url' + url: string +}> + export type ScanAction = | ScanActionSendOnlyReceiver | ScanActionSendSinglePt | ScanActionClaim + | ScanActionLaunchUrl export type ScanFeature = 'send' | 'scan'