diff --git a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.module.scss b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.module.scss new file mode 100644 index 0000000000..515c2532fe --- /dev/null +++ b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.module.scss @@ -0,0 +1,63 @@ +@import '../../../../../../../../packages/common/src/ui/styles/theme.scss'; +@import '../../../../../../../../packages/common/src/ui/styles/abstracts/typography'; + +.opReturnMessageInput { + margin-top: size_unit(2); + padding-bottom: size_unit(3); + width: 100%; + + :global(.ant-input-affix-wrapper) { + transition: none !important; + } + + &.visibleCounter { + padding-bottom: size_unit(2.5); + + :global(.ant-input-suffix) { + position: absolute; + right: size_unit(0.5); + top: size_unit(0.5); + width: size_unit(6.5); + } + } + + input { + @include text-bodyLarge-medium; + color: var(--text-color-primary) !important; + transition: none; + ::placeholder { + color: var(--text-color-secondary) !important; + } + } + + @media (max-width: 400px) { + margin-top: size_unit(1); + padding-bottom: size_unit(5); + + &.visibleCounter { + padding-bottom: size_unit(4); + } + } + + .suffixContent { + margin-right: -7px; + &.focus { + margin-bottom: -#{size_unit(1.5)}; + } + } +} + +.characterCounter { + margin: size_unit(0.5) 0 0; + padding-left: size_unit(3); + @include text-form-label($weight: 500); + color: var(--text-color-secondary); + + &.error { + color: var(--data-pink); + } +} + +.iconSize { + font-size: size_unit(3) !important; +} diff --git a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.tsx b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.tsx new file mode 100644 index 0000000000..17bdfdafec --- /dev/null +++ b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/OpReturnMessageInput.tsx @@ -0,0 +1,78 @@ +/* eslint-disable unicorn/no-useless-undefined */ +import React, { useState } from 'react'; +import { Input, TextBoxItem } from '@lace/common'; +import classnames from 'classnames'; +import styles from './OpReturnMessageInput.module.scss'; +import { useTranslation, Trans } from 'react-i18next'; + +const MAX_LENGTH = 80; + +interface OpReturnInputProps { + onOpReturnMessageChange: (value: string) => void; + opReturnMessage: string; + disabled?: boolean; +} + +export const OpReturnMessageInput: React.FC = ({ + onOpReturnMessageChange, + opReturnMessage, + disabled = false +}) => { + const { t } = useTranslation(); + const [value, setOpReturnMessageMsg] = useState(opReturnMessage); + const [focused, setFocused] = useState(false); + const handleChange = ({ target }: React.ChangeEvent) => { + setOpReturnMessageMsg(target.value); + onOpReturnMessageChange(target.value); + }; + + const handleClick = (event: React.MouseEvent) => { + event.stopPropagation(); + setOpReturnMessageMsg(undefined); + onOpReturnMessageChange(''); + }; + + const handleFocusedState = () => setFocused((prev) => !prev); + + const hasReachedCharLimit = value?.length > MAX_LENGTH; + const displayCounter = !!value || focused; + return ( +
+ 0 })} + > + +
+ } + label={t('browserView.transaction.send.metadata.addAOpReturnNote')} + focus={focused} + disabled={disabled} + /> + + {displayCounter && ( +

+ +

+ )} + + ); +}; diff --git a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/ReviewTransaction.tsx b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/ReviewTransaction.tsx index 0829d89a95..f6dd4de42b 100644 --- a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/ReviewTransaction.tsx +++ b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/ReviewTransaction.tsx @@ -1,4 +1,4 @@ -/* eslint-disable complexity */ +/* eslint-disable complexity, sonarjs/cognitive-complexity */ /* eslint-disable no-magic-numbers */ import React from 'react'; import { renderLabel, RowContainer } from '@lace/core'; @@ -16,6 +16,7 @@ interface ReviewTransactionProps { unsignedTransaction: Bitcoin.UnsignedTransaction & { isHandle: boolean; handle: string }; btcToUsdRate: number; feeRate: number; + opReturnMessage: string; estimatedTime: string; onConfirm: () => void; onClose: () => void; @@ -29,12 +30,14 @@ export const ReviewTransaction: React.FC = ({ estimatedTime, onConfirm, isPopupView, - onClose + onClose, + opReturnMessage }) => { const { t } = useTranslation(); const amount = Number(unsignedTransaction.amount); const usdValue = (amount / SATS_IN_BTC) * btcToUsdRate; const feeInBtc = unsignedTransaction.fee; + const hasOpReturn = opReturnMessage && opReturnMessage.length > 0; return ( @@ -94,6 +97,24 @@ export const ReviewTransaction: React.FC = ({ + {hasOpReturn && ( + + + {renderLabel({ + label: t('core.outputSummaryList.note'), + dataTestId: 'output-summary-note' + })} + + + + {opReturnMessage} + + + + + + )} + diff --git a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendFlow.tsx b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendFlow.tsx index 745dfa3c84..12d20e8529 100644 --- a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendFlow.tsx +++ b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendFlow.tsx @@ -80,6 +80,7 @@ type BuildTxProps = { knownAddresses: Bitcoin.DerivedAddress[]; changeAddress: string; recipientAddress: string; + opReturnMessage: string; feeRate: number; amount: bigint; utxos: Bitcoin.UTxO[]; @@ -93,12 +94,14 @@ const buildTransaction = ({ feeRate, amount, utxos, + opReturnMessage, network }: BuildTxProps): Bitcoin.UnsignedTransaction => new Bitcoin.TransactionBuilder(network, feeRate, knownAddresses) .setChangeAddress(changeAddress) .setUtxoSet(utxos) .addOutput(recipientAddress, amount) + .addOpReturnOutput(opReturnMessage) .build(); const btcStringToSatoshisBigint = (btcString: string): bigint => { @@ -132,7 +135,7 @@ export const SendFlow: React.FC = () => { const [confirmationHash, setConfirmationHash] = useState(''); const [txError, setTxError] = useState(); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); - + const [opReturnMessage, setOpReturnMessage] = useState(''); const { priceResult } = useFetchCoinPrice(); const { bitcoinWallet } = useWalletManager(); @@ -268,7 +271,8 @@ export const SendFlow: React.FC = () => { feeRate: newFeeRate, amount: btcStringToSatoshisBigint(amount), utxos, - network + network, + opReturnMessage }), isHandle: address.isHandle, handle: address.isHandle ? address.address : '' @@ -333,6 +337,8 @@ export const SendFlow: React.FC = () => { onContinue={goToReview} network={network} hasUtxosInMempool={hasUtxosInMempool} + onOpReturnMessageChange={setOpReturnMessage} + opReturnMessage={opReturnMessage} /> ); } @@ -348,6 +354,7 @@ export const SendFlow: React.FC = () => { feeRate={feeRate} estimatedTime={estimatedTime} onConfirm={goToPassword} + opReturnMessage={opReturnMessage} /> ); diff --git a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendStepOne.tsx b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendStepOne.tsx index c6d9c24398..8ac7f49ab5 100644 --- a/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendStepOne.tsx +++ b/apps/browser-extension-wallet/src/views/bitcoin-mode/features/send/components/SendStepOne.tsx @@ -21,6 +21,7 @@ import debounce from 'lodash/debounce'; import { CustomConflictError, CustomError, ensureHandleOwnerHasntChanged, verifyHandle } from '@utils/validators'; import { AddressValue, HandleVerificationState } from './types'; import { CheckCircleOutlined, ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'; +import { OpReturnMessageInput } from './OpReturnMessageInput'; const SATS_IN_BTC = 100_000_000; @@ -51,6 +52,8 @@ interface SendStepOneProps { onClose: () => void; network: Bitcoin.Network | null; hasUtxosInMempool: boolean; + onOpReturnMessageChange: (value: string) => void; + opReturnMessage: string; } const InputError = ({ @@ -87,7 +90,9 @@ export const SendStepOne: React.FC = ({ isPopupView, onClose, network, - hasUtxosInMempool + hasUtxosInMempool, + onOpReturnMessageChange, + opReturnMessage }) => { const { t } = useTranslation(); const numericAmount = Number.parseFloat(amount) || 0; @@ -369,6 +374,12 @@ export const SendStepOne: React.FC = ({ /> + + {isPopupView ? ( {t('browserView.transaction.btc.send.feeRate')} @@ -445,7 +456,14 @@ export const SendStepOne: React.FC = ({ className={mainStyles.buttons} >