diff --git a/cashtab/extension/public/manifest.json b/cashtab/extension/public/manifest.json --- a/cashtab/extension/public/manifest.json +++ b/cashtab/extension/public/manifest.json @@ -3,7 +3,7 @@ "name": "Cashtab", "description": "A browser-integrated eCash wallet from Bitcoin ABC", - "version": "3.18.2", + "version": "3.20.0", "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "2.19.1", + "version": "2.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.19.1", + "version": "2.20.0", "dependencies": { "@ant-design/icons": "^5.3.0", "@bitgo/utxo-lib": "^9.33.0", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "2.19.1", + "version": "2.20.0", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/components/Common/Atoms.js b/cashtab/src/components/Common/Atoms.js --- a/cashtab/src/components/Common/Atoms.js +++ b/cashtab/src/components/Common/Atoms.js @@ -28,15 +28,6 @@ color: ${props => props.theme.primary}; `; -export const FormLabel = styled.label` - font-size: 16px; - margin-bottom: 5px; - text-align: left; - width: 100%; - display: inline-block; - color: ${props => props.theme.contrast}; -`; - export const WalletInfoCtn = styled.div` background: ${props => props.theme.walletInfoContainer}; padding: 12px 20px; diff --git a/cashtab/src/components/Common/Inputs.js b/cashtab/src/components/Common/Inputs.js --- a/cashtab/src/components/Common/Inputs.js +++ b/cashtab/src/components/Common/Inputs.js @@ -37,6 +37,7 @@ `; const CashtabInput = styled.input` + ${props => props.disabled && `cursor: not-allowed`}; background-color: ${props => props.theme.forms.selectionBackground}; font-size: 18px; padding: 16px 12px; @@ -66,7 +67,23 @@ :focus-visible { outline: none; } - height: 142px; + height: ${props => props.height}px; + resize: none; + ${props => props.disabled && `cursor: not-allowed`}; + &::-webkit-scrollbar { + width: 12px; + } + &::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + background-color: ${props => props.theme.eCashBlue}; + border-radius: 10px; + height: 80%; + } + &::-webkit-scrollbar-thumb { + border-radius: 10px; + color: ${props => props.theme.eCashBlue}; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); + } `; const LeftInput = styled(CashtabInput)` @@ -193,7 +210,7 @@ `; const CountAndErrorFlex = styled.div` display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; `; const TextAreaErrorMsg = styled.div` @@ -207,6 +224,8 @@ name = '', value = '', handleInput, + disabled = false, + height = 142, error = false, showCount = false, customCount = false, @@ -218,6 +237,8 @@ placeholder={placeholder} name={name} value={value} + height={height} + disabled={disabled} onChange={e => handleInput(e)} /> @@ -242,6 +263,8 @@ name: PropTypes.string, value: PropTypes.string, handleInput: PropTypes.func, + disabled: PropTypes.bool, + height: PropTypes.number, error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), showCount: PropTypes.bool, customCount: PropTypes.number, @@ -335,7 +358,7 @@ max diff --git a/cashtab/src/components/Common/Switch.js b/cashtab/src/components/Common/Switch.js --- a/cashtab/src/components/Common/Switch.js +++ b/cashtab/src/components/Common/Switch.js @@ -33,12 +33,8 @@ content: attr(data-on); ${props => props.bgImageOn - ? `background: ${ - props.disabled ? '#ccc' : props.theme.eCashBlue - } url(${props.bgImageOn}) 20%/contain no-repeat` - : `background-color: ${ - props.disabled ? '#ccc' : props.theme.eCashBlue - }`}; + ? `background: ${props.theme.eCashBlue} url(${props.bgImageOn}) 20%/contain no-repeat` + : `background-color: ${props.theme.eCashBlue}`}; text-transform: uppercase; padding-left: 10px; color: #fff; @@ -132,7 +128,6 @@ bgImageOn={bgImageOn} bgImageOff={bgImageOff} bgColorOff={bgColorOff} - disabled={disabled} small={small} > diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js --- a/cashtab/src/components/Send/SendXec.js +++ b/cashtab/src/components/Send/SendXec.js @@ -17,13 +17,9 @@ shouldSendXecBeDisabled, parseAddressInput, isValidXecSendAmount, + getOpReturnRawError, } from 'validation'; -import { - ConvertAmount, - AlertMsg, - FormLabel, - TxLink, -} from 'components/Common/Atoms'; +import { ConvertAmount, AlertMsg, TxLink } from 'components/Common/Atoms'; import { getWalletState } from 'utils/cashMethods'; import { sendXec, @@ -35,6 +31,7 @@ getAirdropTargetOutput, getCashtabMsgByteCount, getOpreturnParamTargetOutput, + parseOpReturnRaw, } from 'opreturn'; import ApiError from 'components/Common/ApiError'; import { formatFiatBalance, formatBalance } from 'utils/formatting'; @@ -54,8 +51,10 @@ TextArea, } from 'components/Common/Inputs'; import Switch from 'components/Common/Switch'; +import { opReturn } from 'config/opreturn'; const SendXecForm = styled.div` + margin: 12px 0; display: flex; flex-direction: column; gap: 12px; @@ -104,6 +103,23 @@ flex-direction: column; justify-content: center; `; +const ParsedOpReturnRawRow = styled.div` + display: flex; + flex-direction: column; + word-break: break-word; +`; +const ParsedOpReturnRawLabel = styled.div` + color: ${props => props.theme.contrast}; + text-align: left; + width: 100%; +`; +const ParsedOpReturnRaw = styled.div` + background-color: #fff2f0; + border-radius: 12px; + color: ${props => props.theme.eCashBlue}; + padding: 12px; + text-align: left; +`; const LocaleFormattedValue = styled.div` color: ${props => props.theme.contrast}; @@ -114,6 +130,15 @@ const SendToOneHolder = styled.div``; const SendToManyHolder = styled.div``; +const SendToOneInputForm = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; +const InputAndAliasPreviewHolder = styled.div` + displaly: flex; + flex-direction: column; +`; const InputModesHolder = styled.div` min-height: 9rem; @@ -139,15 +164,6 @@ } `; -const AmountSetByBip21Alert = styled.span` - color: ${props => props.theme.eCashPurple}; -`; -const OpReturnRawSetByBip21Alert = styled.div` - color: ${props => props.theme.eCashPurple}; - padding: 12px; - overflow-wrap: break-word; -`; - const SendXec = () => { const ContextValue = React.useContext(WalletContext); const location = useLocation(); @@ -164,6 +180,12 @@ ); const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); const [sendWithCashtabMsg, setSendWithCashtabMsg] = useState(false); + const [sendWithOpReturnRaw, setSendWithOpReturnRaw] = useState(false); + const [opReturnRawError, setOpReturnRawError] = useState(false); + const [parsedOpReturnRaw, setParsedOpReturnRaw] = useState({ + protocol: '', + data: '', + }); // Load with QR code open if device is mobile const openWithScanner = @@ -175,6 +197,7 @@ multiAddressInput: '', airdropTokenId: '', cashtabMsg: '', + opReturnRaw: '', }; const [formData, setFormData] = useState(emptyFormData); @@ -427,14 +450,9 @@ } else if (sendWithCashtabMsg && formData.cashtabMsg !== '') { // Send this tx with a Cashtab msg if the user has the switch enabled and the input field is not empty targetOutputs.push(getCashtabMsgTargetOutput(formData.cashtabMsg)); - } else if ( - 'op_return_raw' in parsedAddressInput && - parsedAddressInput.op_return_raw.error === false - ) { + } else if (formData.opReturnRaw !== '' && opReturnRawError === false) { targetOutputs.push( - getOpreturnParamTargetOutput( - parsedAddressInput.op_return_raw.value, - ), + getOpreturnParamTargetOutput(formData.opReturnRaw), ); } @@ -583,6 +601,19 @@ handleAmountChange(amountObj); } + // Set op_return_raw if it's in the query string + if ('op_return_raw' in parsedAddressInput) { + // Turn on sendWithOpReturnRaw + setSendWithOpReturnRaw(true); + // Update the op_return_raw field and trigger its validation + handleOpReturnRawInput({ + target: { + name: 'opReturnRaw', + value: parsedAddressInput.op_return_raw.value, + }, + }); + } + // Set address field to user input setFormData(p => ({ ...p, @@ -641,6 +672,23 @@ })); }; + const handleOpReturnRawInput = e => { + const { name, value } = e.target; + // Validate input + const error = getOpReturnRawError(value); + setOpReturnRawError(error); + // Update formdata + setFormData(p => ({ + ...p, + [name]: value, + })); + // Update parsedOpReturn if we have no opReturnRawError + if (error === false) { + // Need to gate this for no error as parseOpReturnRaw expects a validated op_return_raw + setParsedOpReturnRaw(parseOpReturnRaw(value)); + } + }; + const handleCashtabMsgChange = e => { const { name, value } = e.target; let cashtabMsgError = false; @@ -773,6 +821,8 @@ multiSendAddressError, sendWithCashtabMsg, cashtabMsgError, + sendWithOpReturnRaw, + opReturnRawError, priceApiError, isOneToManyXECSend, ); @@ -821,64 +871,59 @@ - - - - {aliasInputAddress && - `${aliasInputAddress.slice( - 0, - 10, - )}...${aliasInputAddress.slice(-5)}`} - - - - {'amount' in parsedAddressInput && - parsedAddressInput.amount.value !== null && ( - - {' '} - (set by BIP21 query string) - - )} - - + + + + + + {aliasInputAddress && + `${aliasInputAddress.slice( + 0, + 10, + )}...${aliasInputAddress.slice(-5)}`} + + + + + {priceApiError && ( @@ -909,12 +954,18 @@ checked={sendWithCashtabMsg} disabled={ txInfoFromUrl || - 'queryString' in parsedAddressInput || - 'op_return_raw' in parsedAddressInput - } - handleToggle={() => - setSendWithCashtabMsg(!sendWithCashtabMsg) + 'queryString' in parsedAddressInput } + handleToggle={() => { + // If we are sending a Cashtab msg, toggle off op_return_raw + if ( + !sendWithCashtabMsg && + sendWithOpReturnRaw + ) { + setSendWithOpReturnRaw(false); + } + setSendWithCashtabMsg(!sendWithCashtabMsg); + }} /> Cashtab Msg @@ -923,6 +974,7 @@