From cdb0b9f134395a767956ff9e9b03376af0fc2b1e Mon Sep 17 00:00:00 2001 From: banklesss <105349292+banklesss@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:07:59 +0100 Subject: [PATCH] feature(wallet-mobile): new tx review mint + reference inputs + infrastructure issue screen (#3734) --- .../src/features/Discover/common/helpers.ts | 12 +- ...Manager.ts => useDappConnectorManager.tsx} | 136 +++-- .../features/ReviewTx/ReviewTxNavigator.tsx | 9 +- .../ReviewTx/common/hooks/useFormattedTx.tsx | 20 +- .../ReviewTx/common/hooks/useStrings.tsx | 35 ++ .../src/features/ReviewTx/common/mocks.ts | 391 ++++++++++++ .../src/features/ReviewTx/common/types.ts | 2 + .../InfraestructureIssueIcon.tsx | 76 +++ .../ReviewTxScreen/ReviewTx/Mint/MintTab.tsx | 77 +++ .../ReviewTx/Overview/OverviewTab.tsx | 54 +- .../ReferenceInputs/ReferenceInputs.tsx | 60 ++ .../ReviewTx/ReviewTx.stories.tsx | 17 +- .../ReviewTxScreen/ReviewTx/ReviewTx.tsx | 61 +- .../ReviewTxScreen/ReviewTxScreen.tsx | 14 +- .../InfraestructureIssueScreen.tsx | 81 +++ .../src/kernel/i18n/locales/en-US.json | 9 +- apps/wallet-mobile/src/kernel/navigation.tsx | 1 + .../ReviewTx/common/hooks/useStrings.json | 577 +++++++++++------- 18 files changed, 1284 insertions(+), 348 deletions(-) rename apps/wallet-mobile/src/features/Discover/{useDappConnectorManager.ts => useDappConnectorManager.tsx} (69%) create mode 100644 apps/wallet-mobile/src/features/ReviewTx/illustrations/InfraestructureIssueIcon.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Mint/MintTab.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReferenceInputs/ReferenceInputs.tsx create mode 100644 apps/wallet-mobile/src/features/ReviewTx/useCases/ShowInfraestructureIssueScreen/InfraestructureIssueScreen.tsx diff --git a/apps/wallet-mobile/src/features/Discover/common/helpers.ts b/apps/wallet-mobile/src/features/Discover/common/helpers.ts index f4dc10a159..c16db27802 100644 --- a/apps/wallet-mobile/src/features/Discover/common/helpers.ts +++ b/apps/wallet-mobile/src/features/Discover/common/helpers.ts @@ -64,9 +64,9 @@ type CreateDappConnectorOptions = { wallet: YoroiWallet meta: Wallet.Meta confirmConnection: (origin: string, manager: DappConnector) => Promise - signTx: (cbor: string) => Promise + signTx: (options: {cbor: string; manager: DappConnector}) => Promise signData: (address: string, payload: string) => Promise - signTxWithHW: (cbor: string, partial?: boolean) => Promise + signTxWithHW: (options: {cbor: string; partial?: boolean; manager: DappConnector}) => Promise signDataWithHW: (address: string, payload: string) => Promise<{signature: string; key: string}> } @@ -115,17 +115,17 @@ export const createDappConnector = (options: CreateDappConnectorOptions) => { }, signTx: async (cbor: string, partial?: boolean) => { if (meta.isHW) { - const tx = await options.signTxWithHW(cbor, partial) + const tx = await options.signTxWithHW({cbor, partial, manager}) return tx.witnessSet() } - const rootKey = await signTx(cbor) + const rootKey = await signTx({cbor, manager}) return cip30.signTx(rootKey, cbor, partial) }, sendReorganisationTx: async (value?: string) => { const cbor = await cip30.buildReorganisationTx(value) if (meta.isHW) { - const signedTx = await options.signTxWithHW(cbor, false) + const signedTx = await options.signTxWithHW({cbor, partial: false, manager}) const base64 = Buffer.from(await signedTx.toBytes()).toString('base64') await wallet.submitTransaction(base64) return getTransactionUnspentOutput({ @@ -135,7 +135,7 @@ export const createDappConnector = (options: CreateDappConnectorOptions) => { }) } - const rootKey = await signTx(cbor) + const rootKey = await signTx({cbor, manager}) const witnesses = await cip30.signTx(rootKey, cbor, false) return cip30.sendReorganisationTx(cbor, witnesses) }, diff --git a/apps/wallet-mobile/src/features/Discover/useDappConnectorManager.ts b/apps/wallet-mobile/src/features/Discover/useDappConnectorManager.tsx similarity index 69% rename from apps/wallet-mobile/src/features/Discover/useDappConnectorManager.ts rename to apps/wallet-mobile/src/features/Discover/useDappConnectorManager.tsx index e6ef3a412c..daa12d3788 100644 --- a/apps/wallet-mobile/src/features/Discover/useDappConnectorManager.ts +++ b/apps/wallet-mobile/src/features/Discover/useDappConnectorManager.tsx @@ -9,7 +9,9 @@ import {logger} from '../../kernel/logger/logger' import {useWalletNavigation} from '../../kernel/navigation' import {cip30LedgerExtensionMaker} from '../../yoroi-wallets/cardano/cip30/cip30-ledger' import {useReviewTx} from '../ReviewTx/common/ReviewTxProvider' +import {CreatedByInfoItem} from '../ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab' import {useSelectedWallet} from '../WalletManager/common/hooks/useSelectedWallet' +import {useBrowser} from './common/BrowserProvider' import {useOpenConfirmConnectionModal} from './common/ConfirmConnectionModal' import {useConfirmHWConnectionModal} from './common/ConfirmHWConnectionModal' import {isUserRejectedError, userRejectedError} from './common/errors' @@ -26,6 +28,10 @@ export const useDappConnectorManager = () => { const {wallet, meta} = useSelectedWallet() const {navigateToTxReview} = useWalletNavigation() const {cborChanged} = useReviewTx() + const {tabs, tabActiveIndex} = useBrowser() + const activeTab = tabs[tabActiveIndex] + const activeTabUrl = activeTab?.url + const activeTabOrigin = activeTabUrl === undefined ? null : new URL(activeTabUrl).origin const confirmConnection = useConfirmConnection() @@ -35,77 +41,85 @@ export const useDappConnectorManager = () => { const promptRootKey = useConnectorPromptRootKey() const {sign: signTxWithHW} = useSignTxWithHW() + const handleSignTx = React.useCallback( + ({cbor, manager}: {cbor: string; manager: DappConnector}) => { + return new Promise((resolve, reject) => { + let shouldResolve = true + cborChanged(cbor) + return manager.getDAppList().then(({dapps}) => { + const matchingDapp = + activeTabOrigin != null ? dapps.find((dapp) => dapp.origins.includes(activeTabOrigin)) : null + navigateToTxReview({ + createdBy: matchingDapp != null && , + onConfirm: async () => { + if (!shouldResolve) return + shouldResolve = false + const rootKey = await promptRootKey() + resolve(rootKey) + navigateTo.browseDapp() + }, + onCancel: () => { + if (!shouldResolve) return + shouldResolve = false + reject(userRejectedError()) + }, + }) + }) + }) + }, + [activeTabOrigin, cborChanged, navigateToTxReview, promptRootKey, navigateTo], + ) + + const handleSignTxWithHW = React.useCallback( + ({cbor, partial, manager}: {cbor: string; partial?: boolean; manager: DappConnector}) => { + return new Promise((resolve, reject) => { + let shouldResolve = true + cborChanged(cbor) + return manager.getDAppList().then(({dapps}) => { + const matchingDapp = + activeTabOrigin != null ? dapps.find((dapp) => dapp.origins.includes(activeTabOrigin)) : null + navigateToTxReview({ + createdBy: matchingDapp != null && , + onConfirm: () => { + if (!shouldResolve) return + shouldResolve = false + signTxWithHW( + {cbor, partial}, + { + onSuccess: (signature) => resolve(signature), + onError: (error) => { + logger.error('ReviewTransaction::handleOnConfirm', {error}) + reject(error) + }, + }, + ) + navigateTo.browseDapp() + }, + onCancel: () => { + if (!shouldResolve) return + shouldResolve = false + reject(userRejectedError()) + }, + }) + }) + }) + }, + [activeTabOrigin, cborChanged, navigateToTxReview, navigateTo, signTxWithHW], + ) + return React.useMemo( () => createDappConnector({ appStorage, wallet, confirmConnection, - signTx: (cbor) => { - return new Promise((resolve, reject) => { - let shouldResolve = true - cborChanged(cbor) - navigateToTxReview({ - onConfirm: async () => { - if (!shouldResolve) return - shouldResolve = false - const rootKey = await promptRootKey() - resolve(rootKey) - navigateTo.browseDapp() - }, - onCancel: () => { - if (!shouldResolve) return - shouldResolve = false - reject(userRejectedError()) - }, - }) - }) - }, + signTx: handleSignTx, signData, meta, - signTxWithHW: (cbor, partial) => { - return new Promise((resolve, reject) => { - let shouldResolve = true - cborChanged(cbor) - navigateToTxReview({ - onConfirm: () => { - if (!shouldResolve) return - shouldResolve = false - signTxWithHW( - {cbor, partial}, - { - onSuccess: (signature) => resolve(signature), - onError: (error) => { - logger.error('ReviewTransaction::handleOnConfirm', {error}) - reject(error) - }, - }, - ) - navigateTo.browseDapp() - }, - onCancel: () => { - if (!shouldResolve) return - shouldResolve = false - reject(userRejectedError()) - }, - }) - }) - }, + signTxWithHW: handleSignTxWithHW, signDataWithHW, }), - [ - appStorage, - wallet, - confirmConnection, - signData, - meta, - signDataWithHW, - cborChanged, - navigateToTxReview, - promptRootKey, - navigateTo, - signTxWithHW, - ], + [appStorage, wallet, confirmConnection, handleSignTx, signData, meta, handleSignTxWithHW, signDataWithHW], ) } diff --git a/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx index 1ae41aecb4..115f2f57a1 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/ReviewTxNavigator.tsx @@ -7,6 +7,7 @@ import {defaultStackNavigationOptions, ReviewTxRoutes} from '../../kernel/naviga import {useStrings} from './common/hooks/useStrings' import {ReviewTxScreen} from './useCases/ReviewTxScreen/ReviewTxScreen' import {FailedTxScreen} from './useCases/ShowFailedTxScreen/FailedTxScreen' +import {InfraestructureIssueScreen} from './useCases/ShowInfraestructureIssueScreen/InfraestructureIssueScreen' import {SubmittedTxScreen} from './useCases/ShowSubmittedTxScreen/SubmittedTxScreen' export const Stack = createStackNavigator() @@ -15,6 +16,8 @@ export const ReviewTxNavigator = () => { const {atoms, color} = useTheme() const strings = useStrings() + const fallback = React.useCallback(() => , []) + return ( { > {() => ( - + )} diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx index 2ca1bdf71f..491c223f6e 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useFormattedTx.tsx @@ -44,19 +44,25 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => { return tokenIds }) - const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds]) + const mintTokenIds = + data.mint?.map(([policyId, asset]) => `${policyId}.${Object.keys(asset)[0] ?? ''}` as Portfolio.Token.Id) ?? [] + + const tokenIds = _.uniq([...inputTokenIds, ...outputTokenIds, ...mintTokenIds]) const portfolioTokenInfos = usePortfolioTokenInfos({wallet, tokenIds}, {suspense: true}) const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos) const formattedOutputs = useFormattedOutputs(wallet, outputs, portfolioTokenInfos) const formattedFee = formatFee(wallet, data) const formattedCertificates = formatCertificates(data.certs) + const formattedMintData = formatMintData(data.mint, portfolioTokenInfos) return { inputs: formattedInputs, outputs: formattedOutputs, fee: formattedFee, certificates: formattedCertificates, + mint: formattedMintData, + referenceInputs: data.reference_inputs, } } @@ -230,6 +236,18 @@ const formatCertificates = (certificates: TransactionBody['certs']) => { ) } +const formatMintData = ( + mintData: TransactionBody['mint'] | null, + portfolioTokenInfos: ReturnType, +) => { + if (mintData == null) return null + return (mintData?.flatMap(([policyId, tokens]) => + Object.entries(tokens) + .map(([assetNameHex, count]) => [portfolioTokenInfos.tokenInfos?.get(`${policyId}.${assetNameHex}`), count]) + .filter(([tokenInfo]) => tokenInfo != null), + ) ?? []) as Array<[Portfolio.Token.Info, string]> +} + const deriveAddress = async (address: string, chainId: number) => { try { return await deriveRewardAddressFromAddress(address, chainId) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx index f6563a0b68..ef43a82cf4 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/common/hooks/useStrings.tsx @@ -12,6 +12,8 @@ export const useStrings = () => { utxosTab: intl.formatMessage(messages.utxosTab), overviewTab: intl.formatMessage(messages.overviewTab), metadataTab: intl.formatMessage(messages.metadataTab), + mintTab: intl.formatMessage(messages.mintTab), + referenceInputsTab: intl.formatMessage(messages.referenceInputsTab), metadataHash: intl.formatMessage(messages.metadataHash), metadataJsonLabel: intl.formatMessage(messages.metadataJsonLabel), walletLabel: intl.formatMessage(messages.walletLabel), @@ -60,12 +62,17 @@ export const useStrings = () => { submittedTxText: intl.formatMessage(messages.submittedTxText), submittedTxButton: intl.formatMessage(messages.submittedTxButton), failedTxTitle: intl.formatMessage(messages.failedTxTitle), + infraestructureIssueTitle: intl.formatMessage(messages.infraestructureIssueTitle), + infraestructureIssueText: intl.formatMessage(messages.infraestructureIssueText), + infraestructureIssueButton: intl.formatMessage(messages.infraestructureIssueButton), failedTxText: intl.formatMessage(messages.failedTxText), failedTxButton: intl.formatMessage(messages.failedTxButton), multiExternalPartiesSectionLabel: intl.formatMessage(messages.multiExternalPartiesSectionLabel), multiExternalPartiesSectionNotice: intl.formatMessage(messages.multiExternalPartiesSectionNotice), receiveLabel: intl.formatMessage(messages.receiveLabel), operationsLabel: intl.formatMessage(messages.operationsLabel), + policyIdLabel: intl.formatMessage(messages.policyIdLabel), + createdBy: intl.formatMessage(messages.createdBy), } } @@ -86,6 +93,14 @@ const messages = defineMessages({ id: 'txReview.tabLabel.overview', defaultMessage: '!!!Overview', }, + mintTab: { + id: 'txReview.tabLabel.mint', + defaultMessage: '!!!Mint', + }, + referenceInputsTab: { + id: 'txReview.tabLabel.referenceInputs', + defaultMessage: '!!!Reference inputs', + }, metadataTab: { id: 'txReview.tabLabel.metadataTab', defaultMessage: '!!!Metadata', @@ -290,6 +305,18 @@ const messages = defineMessages({ id: 'txReview.failedTxButton', defaultMessage: '!!!Go to transactions', }, + infraestructureIssueTitle: { + id: 'txReview.infraestructureIssueTitle', + defaultMessage: '!!!Something unexpected happened', + }, + infraestructureIssueText: { + id: 'txReview.infraestructureIssueText', + defaultMessage: '!!!Please go back and try again. If this keep happening, contact our support team.', + }, + infraestructureIssueButton: { + id: 'txReview.infraestructureIssueButton', + defaultMessage: '!!!Go to transactions', + }, multiExternalPartiesSectionLabel: { id: 'txReview.overview.multiExternalPartiesSectionLabel', defaultMessage: '!!!Other parties', @@ -307,4 +334,12 @@ const messages = defineMessages({ id: 'txReview.operationsLabel', defaultMessage: '!!!Operations', }, + policyIdLabel: { + id: 'txReview.policyIdLabel', + defaultMessage: '!!!Policy ID', + }, + createdBy: { + id: 'txReview.createdBy', + defaultMessage: '!!!Created by', + }, }) diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts index 1cd3d8479a..5fa5bc4c1c 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/mocks.ts @@ -163,6 +163,388 @@ export const onlyAdaOneReceiver: FormattedTx = { isPrimary: true, }, certificates: null, + mint: null, + referenceInputs: null, +} + +export const onlyAdaOneReceiverReferenceInputs: FormattedTx = { + inputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '10.618074 ADA', + quantity: '10618074', + isPrimary: true, + }, + ], + address: + 'addr1qykrmfm7qmhpvmt6xkapegwun67wf75pcghm7p3a78gmm470ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqrqg5yh', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 0, + txHash: '968c8b93fa086cb09fca400d2fe11b52e3b551a0527840c2dbb02796379467ca', + }, + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '2.000000 ADA', + quantity: '2000000', + isPrimary: true, + }, + ], + address: + 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 0, + txHash: 'ee2a6b1ca4887e5d0827225ab1351418ce17c551ee4f59f68e901a9f6a2a51a8', + }, + ], + outputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '10.000000 ADA', + quantity: '10000000', + isPrimary: true, + }, + ], + address: + 'addr1q8a3kt40xel75qeknwcsa75jevg9nljf64vjxfdmz24p00d84fs97ylhclpxmu3ej5dyy8wjjl54tk8tjynnwag83a2q90y4sx', + addressKind: 0, + rewardAddress: 'stake1uxn65czlz0mu0snd7gue2xjzrhff0624mr4ezfehw5rc74qrjwn0v', + ownAddress: false, + }, + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '2.443465 ADA', + quantity: '2443465', + isPrimary: true, + }, + ], + address: + 'addr1q9eggas5e4l0jzhhhxfds0q3rr3243zrydgyvm4myh6mu770ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwq0nyf7t', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + }, + ], + fee: { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '0.174609 ADA', + quantity: '174609', + isPrimary: true, + }, + certificates: null, + mint: null, + referenceInputs: [ + { + transaction_id: 'f38eb82a583b61d914d6b838509ab38c4bd398c410c9218682fb69fd702df1c4', + index: 1, + }, + { + transaction_id: 'f38eb82a583b61d914d6b838509ab38c4bd398c410c9218682fb69fd702df1c4', + index: 0, + }, + ], +} + +export const onlyAdaOneReceiverMint: FormattedTx = { + inputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '10.618074 ADA', + quantity: '10618074', + isPrimary: true, + }, + ], + address: + 'addr1qykrmfm7qmhpvmt6xkapegwun67wf75pcghm7p3a78gmm470ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwqrqg5yh', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 0, + txHash: '968c8b93fa086cb09fca400d2fe11b52e3b551a0527840c2dbb02796379467ca', + }, + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '2.000000 ADA', + quantity: '2000000', + isPrimary: true, + }, + ], + address: + 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + txIndex: 0, + txHash: 'ee2a6b1ca4887e5d0827225ab1351418ce17c551ee4f59f68e901a9f6a2a51a8', + }, + ], + outputs: [ + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '10.000000 ADA', + quantity: '10000000', + isPrimary: true, + }, + ], + address: + 'addr1q8a3kt40xel75qeknwcsa75jevg9nljf64vjxfdmz24p00d84fs97ylhclpxmu3ej5dyy8wjjl54tk8tjynnwag83a2q90y4sx', + addressKind: 0, + rewardAddress: 'stake1uxn65czlz0mu0snd7gue2xjzrhff0624mr4ezfehw5rc74qrjwn0v', + ownAddress: false, + }, + { + assets: [ + { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '2.443465 ADA', + quantity: '2443465', + isPrimary: true, + }, + ], + address: + 'addr1q9eggas5e4l0jzhhhxfds0q3rr3243zrydgyvm4myh6mu770ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwq0nyf7t', + addressKind: 0, + rewardAddress: 'stake1u88sshxrn2j075k7r6nqdmjcr2kpm2upvmtgxrn6ndkwehqyy4w9s', + ownAddress: true, + }, + ], + fee: { + tokenInfo: { + id: '.', + nature: Portfolio.Token.Nature.Primary, + type: Portfolio.Token.Type.FT, + application: Portfolio.Token.Application.Coin, + status: Portfolio.Token.Status.Valid, + fingerprint: '', + decimals: 6, + name: 'ADA', + ticker: 'ADA', + symbol: '₳', + reference: '', + tag: '', + website: 'https://www.cardano.org/', + originalImage: '', + description: 'Cardano', + }, + name: 'ADA', + label: '0.174609 ADA', + quantity: '174609', + isPrimary: true, + }, + certificates: null, + mint: [ + [ + { + id: '9204963f0066a9d53480566825449c9e422c4f085f95f4ca0b489ff6.5465737441626364', + nature: Portfolio.Token.Nature.Secondary, + type: Portfolio.Token.Type.NFT, + application: Portfolio.Token.Application.General, + status: Portfolio.Token.Status.Valid, + tag: '', + reference: '', + fingerprint: 'asset1azhn55w9907qpnrh6hf04h9ztx589z2m94d5sy', + name: 'TestAbcd', + decimals: 0, + website: '', + ticker: '', + symbol: '', + description: '', + originalImage: 'https://ipfs.io/ipfs/QmNzUxafVyqXkSW4vTzeh2JfjcwCvsA3KjpdGT635k33Ap', + }, + '1', + ], + [ + { + id: 'af4a3e1a8b2557d181f3f611c9769a4a38477cd5acef4024fbc751bf.43617264616e6f42726f202331363231', + nature: Portfolio.Token.Nature.Secondary, + type: Portfolio.Token.Type.NFT, + application: Portfolio.Token.Application.General, + status: Portfolio.Token.Status.Valid, + tag: '', + reference: '', + fingerprint: 'asset1wheh5wk0tk83kxjts79e4fk0z3f3lpqdwkrgr3', + name: 'CardanoBro #1621', + decimals: 0, + website: '', + ticker: '', + symbol: '', + description: '', + originalImage: 'https://ipfs.io/ipfs/QmYNof7Xj6ydsebhFF7qYnLXrU3kt68D1u2yb8JrQMuFkM/0', + }, + '1', + ], + ], + referenceInputs: null, } export const multiAssetOneReceiver: FormattedTx = { @@ -479,6 +861,8 @@ export const multiAssetOneReceiver: FormattedTx = { isPrimary: true, }, certificates: null, + mint: null, + referenceInputs: null, } const onlyAdaMultiReceiver: FormattedTx = { @@ -697,6 +1081,8 @@ const onlyAdaMultiReceiver: FormattedTx = { isPrimary: true, }, certificates: null, + mint: null, + referenceInputs: null, } const multiAssetMultiReceiver: FormattedTx = { @@ -938,12 +1324,17 @@ const multiAssetMultiReceiver: FormattedTx = { isPrimary: true, }, certificates: null, + mint: null, + referenceInputs: null, } + export const mocks = { formattedTxs: { onlyAdaOneReceiver, multiAssetOneReceiver, onlyAdaMultiReceiver, multiAssetMultiReceiver, + onlyAdaOneReceiverMint, + onlyAdaOneReceiverReferenceInputs, }, } diff --git a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts index 2e19bfaa0b..6753585834 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/common/types.ts +++ b/apps/wallet-mobile/src/features/ReviewTx/common/types.ts @@ -58,6 +58,8 @@ export type FormattedTx = { outputs: FormattedOutputs fee: FormattedFee certificates: FormattedCertificate[] | null + mint: Array<[Portfolio.Token.Info, string]> | null + referenceInputs: TransactionBodyJSON['reference_inputs'] } export type FormattedMetadata = { diff --git a/apps/wallet-mobile/src/features/ReviewTx/illustrations/InfraestructureIssueIcon.tsx b/apps/wallet-mobile/src/features/ReviewTx/illustrations/InfraestructureIssueIcon.tsx new file mode 100644 index 0000000000..c7a02529e6 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/illustrations/InfraestructureIssueIcon.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import Svg, {Defs, LinearGradient, Path, Stop} from 'react-native-svg' + +export const InfraestructureIssueIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Mint/MintTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Mint/MintTab.tsx new file mode 100644 index 0000000000..a9aa4fa973 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Mint/MintTab.tsx @@ -0,0 +1,77 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Space} from '../../../../../../components/Space/Space' +import {CopiableText} from '../../../../common/CopiableText' +import {useStrings} from '../../../../common/hooks/useStrings' +import {TokenItem} from '../../../../common/TokenItem' +import {FormattedTx} from '../../../../common/types' + +export const MintTab = ({mintData}: {mintData: FormattedTx['mint']}) => { + const {styles} = useStyles() + const strings = useStrings() + + return ( + + {mintData?.map(([info, count], index) => { + const [policyId] = info.id.split('.') + + return ( + + + + + {`${strings.policyIdLabel}:`} + + + + + {policyId} + + + + + + + + ) + })} + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.flex_1, + ...atoms.px_lg, + backgroundColor: color.bg_color_max, + }, + policyId: { + ...atoms.flex_1, + ...atoms.flex_row, + ...atoms.justify_between, + }, + policyIdLabel: { + ...atoms.body_2_md_medium, + color: color.text_gray_medium, + }, + policyIdText: { + ...atoms.flex_1, + ...atoms.body_2_md_regular, + color: color.text_gray_medium, + }, + token: { + ...atoms.flex_1, + ...atoms.flex_row, + ...atoms.justify_end, + }, + policyIdTextContainer: { + ...atoms.flex_1, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx index 97c877de3f..e7386fe94f 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab.tsx @@ -2,8 +2,9 @@ import {CredKind} from '@emurgo/cross-csl-core' import {Blockies} from '@yoroi/identicon' import {useTheme} from '@yoroi/theme' import {Balance} from '@yoroi/types' +import {Image} from 'expo-image' import * as React from 'react' -import {StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native' +import {Linking, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native' import {Divider} from '../../../../../../components/Divider/Divider' import {Icon} from '../../../../../../components/Icon' @@ -27,11 +28,13 @@ export const OverviewTab = ({ extraOperations, receiverCustomTitle, details, + createdBy, }: { tx: FormattedTx extraOperations?: Array receiverCustomTitle?: React.ReactNode details?: {title: string; component: React.ReactNode} + createdBy?: React.ReactNode }) => { const {styles} = useStyles() const operations = useOperations(tx.certificates) @@ -43,7 +46,7 @@ export const OverviewTab = ({ - + @@ -68,7 +71,7 @@ export const OverviewTab = ({ ) } -const WalletInfoSection = ({tx}: {tx: FormattedTx}) => { +const WalletInfoSection = ({tx, createdBy}: {tx: FormattedTx; createdBy?: React.ReactNode}) => { const {styles} = useStyles() const strings = useStrings() const {wallet, meta} = useSelectedWallet() @@ -104,6 +107,14 @@ const WalletInfoSection = ({tx}: {tx: FormattedTx}) => { + {createdBy != null && ( + <> + {createdBy} + + + + )} + ) @@ -244,7 +255,11 @@ const OneExternalPartySection = ({ {receiverCustomTitle ?? ( - + {address} @@ -390,6 +405,27 @@ const Details = ({details}: {details?: {title: string; component: React.ReactNod ) } +export const CreatedByInfoItem = ({logo, url}: {logo?: string; url: string}) => { + const {styles} = useStyles() + const strings = useStrings() + + return ( + + {strings.createdBy} + + + {logo != null && } + + + + Linking.openURL(url)}> + {url.replace(/^https?:\/\//, '').replace(/\/+$/, '')} + + + + ) +} + const useStyles = () => { const {atoms, color} = useTheme() const styles = StyleSheet.create({ @@ -451,7 +487,7 @@ const useStyles = () => { width: 24, height: 24, }, - receiverSectionAddress: { + externalPartiesSectionAddress: { maxWidth: 260, }, addressText: { @@ -471,6 +507,14 @@ const useStyles = () => { ...atoms.body_2_md_medium, color: color.text_primary_medium, }, + link: { + color: color.text_primary_medium, + ...atoms.body_2_md_medium, + }, + logo: { + width: 24, + height: 24, + }, }) const colors = { diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReferenceInputs/ReferenceInputs.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReferenceInputs/ReferenceInputs.tsx new file mode 100644 index 0000000000..4da093eadb --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReferenceInputs/ReferenceInputs.tsx @@ -0,0 +1,60 @@ +import {useTheme} from '@yoroi/theme' +import * as React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Space} from '../../../../../../components/Space/Space' +import {Accordion} from '../../../../common/Accordion' +import {CopiableText} from '../../../../common/CopiableText' +import {useStrings} from '../../../../common/hooks/useStrings' +import {FormattedTx} from '../../../../common/types' + +export const ReferenceInputsTab = ({referenceInputs}: {referenceInputs: FormattedTx['referenceInputs']}) => { + const {styles} = useStyles() + const strings = useStrings() + + return ( + + + + + {referenceInputs?.map((input, index) => ( + + + + + {input.transaction_id} + + + + {`#${input.index}`} + + + + + ))} + + + ) +} + +const useStyles = () => { + const {atoms, color} = useTheme() + const styles = StyleSheet.create({ + root: { + ...atoms.flex_1, + ...atoms.px_lg, + backgroundColor: color.bg_color_max, + }, + input: { + ...atoms.flex_1, + ...atoms.body_2_md_regular, + color: color.text_gray_medium, + }, + index: { + ...atoms.body_2_md_medium, + color: color.text_gray_medium, + }, + }) + + return {styles} as const +} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx index 344b49db96..5c4e1185f1 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.stories.tsx @@ -10,19 +10,16 @@ import {ReviewTx} from './ReviewTx' storiesOf('Review Tx Screen', module) .addDecorator((story) => {story()}) .add('Only Ada Tx / One Receiver', () => ) + .add('Only Ada Tx / MINT (mint data and utxos dont match. Fake data)', () => ( + + )) + .add('Only Ada Tx / Reference Inputs', () => ( + + )) .add('Only Ada Tx / Multi Receiver', () => ) .add('Multi Asset Tx / One Receiver', () => ) .add('Multi Asset Tx / Multi Receiver', () => ) const Component = ({formattedTx}: {formattedTx: FormattedTx}) => { - return ( - - ) + return } diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.tsx index 11600997a1..e956cd8886 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTx/ReviewTx.tsx @@ -15,11 +15,14 @@ import { import {Button} from '../../../../../components/Button/Button' import {SafeArea} from '../../../../../components/SafeArea' import {ScrollView} from '../../../../../components/ScrollView/ScrollView' +import {isEmptyString} from '../../../../../kernel/utils' import {useStrings} from '../../../common/hooks/useStrings' import {FormattedMetadata, FormattedTx} from '../../../common/types' import {MetadataTab} from '../ReviewTx/Metadata/MetadataTab' import {OverviewTab} from '../ReviewTx/Overview/OverviewTab' import {UTxOsTab} from '../ReviewTx/UTxOs/UTxOsTab' +import {MintTab} from './Mint/MintTab' +import {ReferenceInputsTab} from './ReferenceInputs/ReferenceInputs' const MaterialTab = createMaterialTopTabNavigator() @@ -29,13 +32,15 @@ export const ReviewTx = ({ operations, details, receiverCustomTitle, + createdBy, onConfirm, }: { formattedTx: FormattedTx - formattedMetadata: FormattedMetadata | undefined - operations: Array | undefined - details: {title: string; component: React.ReactNode} | undefined - receiverCustomTitle: React.ReactNode | undefined + formattedMetadata?: FormattedMetadata + operations?: Array + details?: {title: string; component: React.ReactNode} + receiverCustomTitle?: React.ReactNode + createdBy?: React.ReactNode onConfirm: () => void }) => { const {styles} = useStyles() @@ -46,17 +51,25 @@ export const ReviewTx = ({ [strings.utxosTab, 'utxos'], ] + const showMetadataTab = !isEmptyString(formattedMetadata?.hash) && formattedMetadata?.metadata != null + const showMintTab = !!formattedTx.mint + const showReferenceInoutsTab = !!formattedTx.referenceInputs + + if (showMetadataTab) tabsData.push([strings.metadataTab, 'metadata']) + if (showMintTab) tabsData.push([strings.mintTab, 'mint']) + if (showReferenceInoutsTab) tabsData.push([strings.referenceInputsTab, 'reference_inputs']) + return ( }> {() => ( - /* TODO: make scrollview general to use button border */ @@ -65,21 +78,41 @@ export const ReviewTx = ({ {() => ( - /* TODO: make scrollview general to use button border */ )} - - {() => ( - /* TODO: make scrollview general to use button border */ - - - - )} - + {showMetadataTab && ( + + {() => ( + + + + )} + + )} + + {showMintTab && ( + + {() => ( + + + + )} + + )} + + {showReferenceInoutsTab && ( + + {() => ( + + + + )} + + )} diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx index d53525da31..e0a903040f 100644 --- a/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ReviewTxScreen/ReviewTxScreen.tsx @@ -1,17 +1,14 @@ import * as React from 'react' import {ReviewTxRoutes, useUnsafeParams} from '../../../../kernel/navigation' -import {isEmptyString} from '../../../../kernel/utils' import {useFormattedMetadata} from '../../common/hooks/useFormattedMetadata' import {useFormattedTx} from '../../common/hooks/useFormattedTx' import {useOnConfirm} from '../../common/hooks/useOnConfirm' -import {useStrings} from '../../common/hooks/useStrings' import {useTxBody} from '../../common/hooks/useTxBody' import {useReviewTx} from '../../common/ReviewTxProvider' import {ReviewTx} from './ReviewTx/ReviewTx' export const ReviewTxScreen = () => { - const strings = useStrings() const {unsignedTx, cbor} = useReviewTx() const params = useUnsafeParams() @@ -29,14 +26,6 @@ export const ReviewTxScreen = () => { const formattedTx = useFormattedTx(txBody) const formattedMetadata = useFormattedMetadata({txBody, unsignedTx, cbor}) - const tabsData = [ - [strings.overviewTab, 'overview'], - [strings.utxosTab, 'utxos'], - ] - - if (!isEmptyString(formattedMetadata?.hash) && formattedMetadata?.metadata != null) - tabsData.push([strings.metadataTab, 'metadata']) - React.useEffect(() => { return () => { params?.onCancel?.() @@ -53,8 +42,6 @@ export const ReviewTxScreen = () => { onConfirm() } - console.log('formattedTx', JSON.stringify(formattedTx, null, 2)) - return ( { operations={params?.operations} details={params?.details} receiverCustomTitle={params?.receiverCustomTitle} + createdBy={params?.createdBy} onConfirm={handleOnConfirm} /> ) diff --git a/apps/wallet-mobile/src/features/ReviewTx/useCases/ShowInfraestructureIssueScreen/InfraestructureIssueScreen.tsx b/apps/wallet-mobile/src/features/ReviewTx/useCases/ShowInfraestructureIssueScreen/InfraestructureIssueScreen.tsx new file mode 100644 index 0000000000..f38aabd196 --- /dev/null +++ b/apps/wallet-mobile/src/features/ReviewTx/useCases/ShowInfraestructureIssueScreen/InfraestructureIssueScreen.tsx @@ -0,0 +1,81 @@ +import {useNavigation} from '@react-navigation/native' +import {useTheme} from '@yoroi/theme' +import React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Button} from '../../../../components/Button/Button' +import {SafeArea} from '../../../../components/SafeArea' +import {Space} from '../../../../components/Space/Space' +import {Spacer} from '../../../../components/Spacer/Spacer' +import {useBlockGoBack, useWalletNavigation} from '../../../../kernel/navigation' +import {useStrings} from '../../common/hooks/useStrings' +import {InfraestructureIssueIcon} from '../../illustrations/InfraestructureIssueIcon' + +export const InfraestructureIssueScreen = () => { + useBlockGoBack() + const strings = useStrings() + const {styles} = useStyles() + const {resetToTxHistory} = useWalletNavigation() + const navigation = useNavigation() + + React.useLayoutEffect(() => { + navigation.setOptions({headerLeft: () => null}) + }) + + return ( + + + + + + + + {strings.infraestructureIssueTitle} + + {strings.infraestructureIssueText} + + + + +