Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- Fullscreen region select on mobile
- Change portfolio chart default to year
- iOS: live price widget with 24-hour chart
- Receive screen revamp

## v4.50.1
- Fix a bug that would delay showing watch-only accounts.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.copyBtn {
display: inline-flex;
justify-content: flex-start;
margin-top: 0;
padding: 0;
text-align: left;
text-decoration: none;
}

.label {
align-items: center;
display: inline-flex;
font-size: var(--size-default);
gap: var(--space-quarter);
white-space: nowrap;
}

.icon {
height: 18px;
width: 18px;
}
48 changes: 48 additions & 0 deletions frontends/web/src/components/copy/copy-address-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/forms';
import { Checked, Copy } from '@/components/icon/icon';
import style from './copy-address-button.module.css';

type TProps =
| { mode: 'copy'; value: string; onClick?: never; className?: string }
| { mode: 'action'; onClick: () => void; value?: never; className?: string };

export const CopyAddressButton = (props: TProps) => {
const { mode, className } = props;
const { t } = useTranslation();
const [copied, setCopied] = useState(false);

useEffect(() => {
if (!copied) {
return;
}
const id = window.setTimeout(() => setCopied(false), 1500);
return () => window.clearTimeout(id);
}, [copied]);

const handleClick = () => {
if (mode === 'action') {
props.onClick();
return;
}
navigator.clipboard.writeText(props.value);
setCopied(true);
};

return (
<Button
transparent
inline
className={`${style.copyBtn || ''} ${className || ''}`.trim()}
onClick={handleClick}
>
<span className={style.label}>
{copied ? <Checked className={style.icon} /> : <Copy className={style.icon} />}
{t('button.copyAddress')}
</span>
</Button>
);
};

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion frontends/web/src/components/icon/combined.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
.caret {
display: block;
margin: 0 auto;
position: relative;
right: -8px;
}

.bitbox02 {
display: inline-block;
max-width: min(264px, 80%);
position: relative;
right: -16px;
}
}
6 changes: 4 additions & 2 deletions frontends/web/src/components/icon/combined.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { useDarkmode } from '@/hooks/darkmode';
import { BitBox02StylizedDark, BitBox02StylizedLight, CaretDown } from './icon';
import style from './combined.module.css';

export const PointToBitBox02 = () => {
type TProps = { className?: string };

export const PointToBitBox02 = ({ className }: TProps) => {
const { isDarkMode } = useDarkmode();
return (
<div className={style.point2bitbox02}>
<div className={`${style.point2bitbox02 || ''} ${className || ''}`}>
<CaretDown className={style.caret} />
{ isDarkMode
? (<BitBox02StylizedLight className={style.bitbox02} />)
Expand Down
10 changes: 2 additions & 8 deletions frontends/web/src/components/icon/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import arrowFloorUpRedSVG from './assets/icons/arrow-floor-up-red.svg';
import arrowFloorDownGreenSVG from './assets/icons/arrow-floor-down-green.svg';
import arrowFloorUpWhiteSVG from './assets/icons/arrow-floor-up-white.svg';
import arrowFloorDownWhiteSVG from './assets/icons/arrow-floor-down-white.svg';
import arrowCircleLeftSVG from './assets/icons/arrow-circle-left.svg';
import arrowCircleLeftActiveSVG from './assets/icons/arrow-circle-left-active.svg';
import arrowCircleRightSVG from './assets/icons/arrow-circle-right.svg';
import arrowCircleRightActiveSVG from './assets/icons/arrow-circle-right-active.svg';
import arrowSwapSVG from './assets/icons/arrow-swap.svg';
import bankDarkSVG from './assets/icons/bank.svg';
import bankLightSVG from './assets/icons/bank-light.svg';
Expand All @@ -31,6 +27,7 @@ import checkSVG from './assets/icons/check.svg';
import chevronRightDark from './assets/icons/chevron-right-dark.svg';
import chevronLeftDark from './assets/icons/chevron-left-dark.svg';
import chevronDownDark from './assets/icons/chevron-down-dark.svg';
import chevronUpBlue from './assets/icons/chevron-up-blue.svg';
import cancelSVG from './assets/icons/cancel.svg';
import cogLightSVG from './assets/icons/cog-light.svg';
import cogDarkSVG from './assets/icons/cog-dark.svg';
Expand Down Expand Up @@ -134,10 +131,6 @@ export const ArrowFloorUpRed = (props: ImgProps) => (<img src={arrowFloorUpRedSV
export const ArrowFloorDownGreen = (props: ImgProps) => (<img src={arrowFloorDownGreenSVG} draggable={false} {...props} />);
export const ArrowFloorUpWhite = (props: ImgProps) => (<img src={arrowFloorUpWhiteSVG} draggable={false} {...props} />);
export const ArrowFloorDownWhite = (props: ImgProps) => (<img src={arrowFloorDownWhiteSVG} draggable={false} {...props} />);
export const ArrowCirlceLeft = (props: ImgProps) => (<img src={arrowCircleLeftSVG} draggable={false} {...props} />);
export const ArrowCirlceLeftActive = (props: ImgProps) => (<img src={arrowCircleLeftActiveSVG} draggable={false} {...props} />);
export const ArrowCirlceRight = (props: ImgProps) => (<img src={arrowCircleRightSVG} draggable={false} {...props} />);
export const ArrowCirlceRightActive = (props: ImgProps) => (<img src={arrowCircleRightActiveSVG} draggable={false} {...props} />);
export const ArrowSwap = (props: ImgProps) => (<img src={arrowSwapSVG} draggable={false} {...props} />);
export const BankDark = (props: ImgProps) => (<img src={bankDarkSVG} draggable={false} {...props} />);
export const Bank = (props: ImgProps) => (<img src={bankLightSVG} draggable={false} {...props} />);
Expand All @@ -152,6 +145,7 @@ export const Check = (props: ImgProps) => (<img src={checkSVG} draggable={false}
export const ChevronLeftDark = (props: ImgProps) => (<img width={16} height={16} src={chevronLeftDark} draggable={false} {...props} />);
export const ChevronRightDark = (props: ImgProps) => (<img width={16} height={16} src={chevronRightDark} draggable={false} {...props} />);
export const ChevronDownDark = (props: ImgProps) => (<img width={16} height={16} src={chevronDownDark} draggable={false} {...props} />);
export const ChevronUpBlue = (props: ImgProps) => (<img width={14} height={8} src={chevronUpBlue} draggable={false} {...props} />);
export const Cancel = (props: ImgProps) => (<img src={cancelSVG} draggable={false} {...props} />);
export const CreditCardDark = (props: ImgProps) => (<img src={creditCardDarkSVG} draggable={false} {...props} />);
export const CreditCard = (props: ImgProps) => (<img src={creditCardLightSVG} draggable={false} {...props} />);
Expand Down
1 change: 0 additions & 1 deletion frontends/web/src/components/message/message.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
}

.content {
padding-top: 2px;
width: 100%;
}

Expand Down
54 changes: 14 additions & 40 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -1335,44 +1335,9 @@
"insurance": "Insurance guide",
"manageAccount": "Manage accounts guide",
"manageDevice": "Manage device guide",
"receive": "Receive guide",
"send": "Send guide",
"walletConnect": "WalletConnect guide"
},
"receive": {
"address": {
"text": "You can give the address to others to send you some coins. Just make sure they are sending to the correct address.",
"title": "What do I do with an address?"
},
"addressChange": {
"text": "As soon as you transact, a new address is automatically added to the list so there are always 20 addresses available which have never received any coins.",
"title": "When do the addresses change?"
},
"addressFormats": {
"text": "By default, the address type is Native Segwit. This address type is widely adopted by other wallets/exchanges and gives you the best fee rates for everyday transactions. However, you may also choose to send to Taproot (Bitcoin only), which is the newest address type, but may not be widely supported yet.",
"title": "When do I use “Change address type”?"
},
"howVerify": {
"text": "You can verify addresses directly on the BitBox during the send/receive process.",
"title": "How can I verify an address securely?"
},
"plugout": {
"text": "No, once you sent coins to your BitBox address, you do not need to leave your BitBox plugged in. You are free to disconnect your BitBox.",
"title": "Do I need to leave my BitBox plugged in while receiving?"
},
"why20": {
"text": "During start-up the app generates addresses derived from your seed to see if they have received funds. As the app can generate an almost infinite number of addresses, it could spend years determining the balance. To limit this search it stops after it sees 20 addresses that have never received funds. This is the \"gap limit\" and 20 is a de-facto standard though the number is arbitrary. These are the 20 addresses you can choose from.",
"title": "Why only 20 addresses?"
},
"whyMany": {
"text": "To maintain privacy and security, never hand out the same address twice. If you have used an address, click on on the right arrow for a new address. You can generate up to 20 addresses at a time. Think of addresses like invoice numbers. All addresses are derived from your single backup seed.",
"title": "Why so many addresses?"
},
"whyVerify": {
"text": "You shouldn't trust your computer to generate and display authentic addresses. It's big attack surface makes it significantly more vulnerable than a hardware wallet. The address can be verified directly on the BitBox display.",
"title": "Why should I verify the address securely?"
}
},
"send": {
"change": {
"text": "The change will be returned to a Taproot address if you have at least one Taproot UTXO. If you use coin control, the change will be returned to a Taproot address if there is at least one Taproot UTXO among the selected UTXOs. In all other cases, the change is returned to a Native Segwit address.",
Expand Down Expand Up @@ -1729,17 +1694,26 @@
}
},
"receive": {
"bitsuranceWarning": "This is an insured account, meaning it can only receive to Native Segwit. This is so you don't accidently receive to Taproot, which is not insured.",
"alwaysVerifyBody": "Be sure the sender has the same address displayed on your BitBox.",
"alwaysVerifyTitle": "Always verify address on BitBox",
"changeScriptType": "Change address type",
"label": "Your address",
"continueOnBitBox": "Continue on BitBox",
"getNewAddress": "Get new address",
"moreOptions": "More options",
"onlyThisCoin": {
"description": "To receive other tokens, enable them in the settings. If you deposit other tokens, they might not be accessible.",
"warning": "Make sure to only receive {{coinName}} on this address."
},
"qrCodeCopiedMessage": "Copied!",
"scriptType": {
"p2tr": "Taproot (newest format)",
"p2wpkh": "Native Segwit (default)"
"scriptTypeHint": {
"p2tr": "Newest format",
"p2wpkh": "Default",
"p2wpkh-p2sh": "Compatibility"
},
"scriptTypeName": {
"p2tr": "Taproot",
"p2wpkh": "Native segwit",
"p2wpkh-p2sh": "Wrapped segwit"
},
"selectAccount": "Select account",
"taprootWarning": "Note: Taproot is a new Bitcoin feature and is not yet widely adopted. Funds received on Taproot addresses may not be visible in third party watch-only wallets. Many wallets and exchanges are not yet able to send to Taproot addresses.",
Expand Down
10 changes: 3 additions & 7 deletions frontends/web/src/routes/account/addresses/address-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { AccountCode, TUsedAddress } from '@/api/account';
import { Button } from '@/components/forms';
import { Copy, OutlinedFileProtectPrimary } from '@/components/icon';
import { CopyAddressButton } from '@/components/copy/copy-address-button';
import { OutlinedFileProtectPrimary } from '@/components/icon';
import style from './addresses.module.css';

type TProps = {
Expand All @@ -18,12 +19,7 @@ export const AddressActions = ({ code, address, onCopy }: TProps) => {
const navigate = useNavigate();
return (
<div className={style.inlineActions}>
<Button transparent inline className={style.linkAction} onClick={() => onCopy(address)}>
<span className={style.linkActionLabel}>
<Copy className={style.linkActionIcon} />
{t('button.copyAddress')}
</span>
</Button>
<CopyAddressButton mode="action" onClick={() => onCopy(address)} />
{address.canSignMsg && (
<Button transparent inline className={style.linkAction} onClick={() => navigate(`/account/${code}/addresses/${address.addressID}/sign-message`)}>
<span className={style.linkActionLabel}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.card {
display: flex;
align-items: stretch;
gap: var(--space-default);
padding: var(--space-half);
border-radius: var(--radius-xl);
background-color: var(--background-secondary);
}

.qrWrap {
flex: 0 0 auto;
width: 240px;
height: 240px;
display: flex;
align-items: center;
justify-content: center;
filter: blur(0);
transition: filter 280ms ease;
}

.qrWrap.blurred {
filter: blur(4px);
pointer-events: none;
}

.right {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
min-width: 0;
}

.address {
margin: 0;
font-family: var(--monospace, monospace);
font-size: 14px;
overflow-wrap: break-word;
text-align: left;
}

@media (max-width: 768px) {
.qrWrap {
width: 128px;
height: 128px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0

import { type TReceiveAddress } from '@/api/account';
import { CopyAddressButton } from '@/components/copy/copy-address-button';
import { QRCode } from '@/components/qrcode/qrcode';
import { truncateDisplayAddress } from '@/utils/address';
import { useMediaQuery } from '@/hooks/mediaquery';
import style from './address-card.module.css';

type TProps = {
currentAddress: TReceiveAddress;
uriPrefix: string;
isVerifying: boolean;
};

export const AddressCard = ({
currentAddress,
uriPrefix,
isVerifying,
}: TProps) => {
const isMobile = useMediaQuery('(max-width: 768px)');

return (
<div className={style.card}>
<div className={`${style.qrWrap || ''} ${!isVerifying ? (style.blurred || '') : ''}`}>
<QRCode data={uriPrefix + currentAddress.address} size={isMobile ? 128 : 240} tapToCopy={false} />
</div>
<div className={style.right}>
<p className={style.address} data-testid="receive-address">
{isVerifying ? currentAddress.displayAddress : truncateDisplayAddress(currentAddress.displayAddress)}
</p>
{isVerifying && <CopyAddressButton mode="copy" value={currentAddress.address} />}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.navLabel {
margin: 0 0 var(--space-quarter);
font-size: 14px;
color: var(--color-secondary);
}

.navRow {
display: flex;
align-items: center;
gap: var(--space-three-quarter);
margin-bottom: var(--space-half);
}

.chevronBtn {
background: none;
border: 0;
padding: var(--space-eight);
cursor: pointer;
}

.chevronBtn:disabled {
opacity: 0.3;
cursor: default;
}

.navAddress {
font-family: var(--monospace, monospace);
font-size: 14px;
}
Loading
Loading