Skip to content

Commit

Permalink
TW-1452: Redesign 'Receive' page (#1171)
Browse files Browse the repository at this point in the history
* TW-1452 Redesign the 'Receive' page

* TW-1452 Fix text color for a selected seed phrase verification pill

* TW-1452 Remove unused variables

* TW-1452 Remove unused variables

* TW-1452 Remove an unused export

* TW-1452 Update QR code style

* TW-1452 Update font size calculation for 'initials' identicon

* TW-1452 Fix navigation after an accounts modal is closed
  • Loading branch information
keshan3262 authored Jul 25, 2024
1 parent 26e49fc commit 9325498
Show file tree
Hide file tree
Showing 24 changed files with 446 additions and 206 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"postcss-preset-env": "^9",
"prettier": "^2.7.1",
"process": "^0.11.10",
"qr-code-styling-2": "^1.5.5",
"qs": "^6.11.1",
"react": "18.2.0",
"react-collapse": "5.1.1",
Expand Down
25 changes: 25 additions & 0 deletions public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,31 @@
"enterPasswordToRevealPrivateKey": {
"message": "Enter your password to reveal private key"
},
"networkToReceive": {
"message": "Network to receive"
},
"networkAddress": {
"message": "$network$ Address",
"placeholders": {
"network": {
"content": "$1"
}
}
},
"sendOnlySomeNetworkTokens": {
"message": "Send only $network$ network tokens to this address",
"placeholders": {
"network": {
"content": "$1"
}
}
},
"evmReceiveTooltip": {
"message": "Use this address to receive tokens\nand NFTs on Ethereum, BNB Smart Chain,\nArbitrum, Optimism and other EVM\ncompatible networks."
},
"tezosReceiveTooltip": {
"message": "Use this address to receive tokens\nand NFTs only on Tezos network."
},
"selectNetworkToReveal": {
"message": "Select Network to reveal"
},
Expand Down
2 changes: 1 addition & 1 deletion src/app/PageRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ImportAccount from 'app/pages/ImportAccount';
import ManageAssets from 'app/pages/ManageAssets';
import { ImportWallet } from 'app/pages/NewWallet/ImportWallet';
import AttentionPage from 'app/pages/Onboarding/pages/AttentionPage';
import Receive from 'app/pages/Receive/Receive';
import { Receive } from 'app/pages/Receive/Receive';
import Send from 'app/pages/Send';
import Settings from 'app/pages/Settings/Settings';
import { Swap } from 'app/pages/Swap/Swap';
Expand Down
47 changes: 4 additions & 43 deletions src/app/atoms/Identicon.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,24 @@
import React, { HTMLAttributes, memo, useMemo } from 'react';

import * as botttsNeutral from '@dicebear/bottts-neutral';
import { createAvatar } from '@dicebear/core';
import clsx from 'clsx';
import * as jdenticon from 'jdenticon';
import memoizee from 'memoizee';

import * as firstLetters from 'lib/first-letters';
import { IdenticonType, getIdenticonUri } from 'lib/temple/front';

type IdenticonProps = HTMLAttributes<HTMLDivElement> & {
type?: 'jdenticon' | 'botttsneutral' | 'initials';
type?: IdenticonType;
hash: string;
size?: number;
};

const MAX_INITIALS_LENGTH = 5;
const DEFAULT_FONT_SIZE = 50;

const getBackgroundImageUrl = memoizee(
(hash: string, size: number, type: NonNullable<IdenticonProps['type']>) => {
switch (type) {
case 'jdenticon':
return `data:image/svg+xml,${encodeURIComponent(jdenticon.toSvg(hash, size))}`;
case 'botttsneutral':
return createAvatar(botttsNeutral, { seed: hash, size }).toDataUriSync();
default:
return createAvatar(firstLetters, {
seed: hash,
size,
fontFamily: ['Menlo', 'Monaco', 'monospace'],
fontSize: estimateOptimalFontSize(hash.length),
chars: MAX_INITIALS_LENGTH
}).toDataUriSync();
}
},
{ max: 1024 }
);

export const Identicon = memo<IdenticonProps>(
({ type = 'jdenticon', hash, size = 100, className, style = {}, ...rest }) => {
const backgroundImage = useMemo(() => getBackgroundImageUrl(hash, size, type), [hash, size, type]);
const backgroundImage = useMemo(() => getIdenticonUri(hash, size, type), [hash, size, type]);

return (
<div
className={clsx(
'inline-block',
type === 'initials' ? 'bg-transparent' : 'bg-white',
'bg-no-repeat bg-center',
'overflow-hidden',
'bg-no-repeat bg-center inline-block overflow-hidden',
className
)}
style={style}
Expand All @@ -58,13 +29,3 @@ export const Identicon = memo<IdenticonProps>(
);
}
);

function estimateOptimalFontSize(length: number) {
const initialsLength = Math.min(length, MAX_INITIALS_LENGTH);
if (initialsLength > 2) {
const n = initialsLength;
const multiplier = Math.sqrt(10000 / ((32 * n + 4 * (n - 1)) ** 2 + 36 ** 2));
return Math.floor(DEFAULT_FONT_SIZE * multiplier);
}
return DEFAULT_FONT_SIZE;
}
2 changes: 1 addition & 1 deletion src/app/atoms/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export const PageTitle = memo<Props>(({ Icon, title }) => (
<>
{Icon && <IconBase Icon={Icon} />}

<span className="ml-1">{title}</span>
<span className="ml-1 text-font-regular-bold">{title}</span>
</>
));
62 changes: 62 additions & 0 deletions src/app/atoms/QRCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { memo, useEffect, useMemo, useRef } from 'react';

import QRCodeStyling, { Options } from 'qr-code-styling-2';

interface QRCodeProps {
size: number;
data: string;
imageUri?: string;
}

export const QRCode = memo<QRCodeProps>(({ size, data, imageUri }) => {
const internalSize = size * window.devicePixelRatio;

const fullProps = useMemo<Options>(
() => ({
width: internalSize,
height: internalSize,
data,
margin: 0,
qrOptions: { typeNumber: 0, mode: 'Byte', errorCorrectionLevel: 'Q' },
imageOptions: { hideBackgroundDots: true, imageSize: 0.6, margin: 12 },
dotsOptions: { type: 'dots', color: '#000000' },
backgroundOptions: { color: '#ffffff' },
image: imageUri,
dotsOptionsHelper: {
colorType: { single: true, gradient: false },
gradient: { linear: true, radial: false, color1: '#6a1a4c', color2: '#6a1a4c', rotation: '0' }
},
cornersSquareOptions: { type: 'extra-rounded', color: '#000000' },
cornersSquareOptionsHelper: {
colorType: { single: true, gradient: false },
gradient: { linear: true, radial: false, color1: '#000000', color2: '#000000', rotation: '0' }
},
cornersDotOptions: { type: 'square', color: '#000000' },
cornersDotOptionsHelper: {
colorType: { single: true, gradient: false },
gradient: { linear: true, radial: false, color1: '#000000', color2: '#000000', rotation: '0' }
},
backgroundOptionsHelper: {
colorType: { single: true, gradient: false },
gradient: { linear: true, radial: false, color1: '#ffffff', color2: '#ffffff', rotation: '0' }
}
}),
[data, imageUri, internalSize]
);
const styling = useMemo(() => new QRCodeStyling(fullProps), [fullProps]);
const canvasRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (canvasRef.current) {
styling.append(canvasRef.current);
}
}, [styling]);

useEffect(() => {
styling.update(fullProps);
}, [fullProps, styling]);

return (
<div ref={canvasRef} style={{ width: internalSize, height: internalSize, zoom: String(1 / devicePixelRatio) }} />
);
});
19 changes: 17 additions & 2 deletions src/app/atoms/SettingsCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React, { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';

import { createRoot } from 'react-dom/client';

import { useRichFormatTooltip } from 'app/hooks/use-rich-format-tooltip';
import { ReactComponent as InfoFillIcon } from 'app/icons/base/InfoFill.svg';
import { AnalyticsEventCategory, setTestID, useAnalytics } from 'lib/analytics';
import useTippy from 'lib/ui/useTippy';

import { CheckboxV2, CheckboxV2Props } from './CheckboxV2';
import { IconBase } from './IconBase';
Expand All @@ -14,6 +14,21 @@ interface SettingsCheckboxProps extends CheckboxV2Props {
tooltip?: ReactNode;
}

const basicTooltipProps = {
trigger: 'mouseenter',
hideOnClick: false,
interactive: true,
placement: 'bottom-end' as const,
animation: 'shift-away-subtle'
};

const tooltipWrapperFactory = () => {
const element = document.createElement('div');
element.className = 'max-w-48';

return element;
};

export const SettingsCheckbox = memo<SettingsCheckboxProps>(
({ label, tooltip, testID, testIDProperties, onChange, ...restProps }) => {
const { trackEvent } = useAnalytics();
Expand Down Expand Up @@ -43,7 +58,7 @@ export const SettingsCheckbox = memo<SettingsCheckboxProps>(
}
}, [tippyProps.content, tooltip]);

const infoIconWrapperRef = useTippy<HTMLDivElement>(tippyProps);
const infoIconWrapperRef = useRichFormatTooltip<HTMLDivElement>(basicTooltipProps, tooltipWrapperFactory, tooltip);

const handleChange = useCallback(
(toChecked: boolean, event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
2 changes: 2 additions & 0 deletions src/app/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ export { DataPlaceholder } from './DataPlaceholder';
export { PageTitle } from './PageTitle';

export { default as AccountTypeBadge } from './AccountTypeBadge';

export { QRCode } from './QRCode';
29 changes: 29 additions & 0 deletions src/app/hooks/use-modal-open-search-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useMemo } from 'react';

import { HistoryAction, navigate, useLocation } from 'lib/woozie';

export const useModalOpenSearchParams = (paramName: string) => {
const { search, pathname } = useLocation();
const isOpen = useMemo(() => {
const usp = new URLSearchParams(search);

return Boolean(usp.get(paramName));
}, [paramName, search]);
const setModalState = useCallback(
(newState: boolean) => {
const newUsp = new URLSearchParams(search);
if (newState) {
newUsp.set(paramName, 'true');
} else {
newUsp.delete(paramName);
}

navigate({ search: newUsp.toString(), pathname }, HistoryAction.Replace);
},
[search, pathname, paramName]
);
const openModal = useCallback(() => setModalState(true), [setModalState]);
const closeModal = useCallback(() => setModalState(false), [setModalState]);

return { isOpen, openModal, closeModal };
};
26 changes: 26 additions & 0 deletions src/app/hooks/use-rich-format-tooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReactNode, useEffect, useMemo } from 'react';

import { createRoot } from 'react-dom/client';

import useTippy, { UseTippyOptions } from 'lib/ui/useTippy';

export const useRichFormatTooltip = <T extends HTMLElement>(
props: Omit<UseTippyOptions, 'content'>,
wrapperFactory: () => HTMLElement,
content: ReactNode
) => {
const tippyProps = useMemo(
() => ({
...props,
content: wrapperFactory()
}),
[props, wrapperFactory]
);

useEffect(() => {
const root = createRoot(tippyProps.content);
root.render(content);
}, [tippyProps.content, content]);

return useTippy<T>(tippyProps);
};
2 changes: 1 addition & 1 deletion src/app/layouts/PageLayout/DefaultHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const DefaultHeader = memo<PropsWithChildren<DefaultHeaderProps>>(
>
<div className="flex-1 flex items-center">
<Button className="block" onClick={onBackClick} testID={PageLayoutSelectors.backButton}>
<IconBase Icon={ChevronLeftIcon} className="text-grey-1" />
<IconBase Icon={ChevronLeftIcon} className="text-grey-2" />
</Button>
</div>

Expand Down
29 changes: 29 additions & 0 deletions src/app/pages/Receive/AccountDropdownHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { memo } from 'react';

import clsx from 'clsx';

import { IconBase } from 'app/atoms';
import { AccountAvatar } from 'app/atoms/AccountAvatar';
import { ReactComponent as CompactDownIcon } from 'app/icons/base/compact_down.svg';
import { StoredAccount } from 'lib/temple/types';

interface AccountDropdownHeaderProps {
account: StoredAccount;
onClick: EmptyFn;
className?: string;
}

export const AccountDropdownHeader = memo<AccountDropdownHeaderProps>(({ account, className, onClick }) => (
<div
className={clsx(
className,
'flex items-center gap-x-2 p-3 rounded-lg shadow-center border-0.5 border-transparent hover:border-lines',
'cursor-pointer'
)}
onClick={onClick}
>
<AccountAvatar seed={account.id} size={24} borderColor="secondary" />
<span className="flex-1 text-font-medium-bold">{account.name}</span>
<IconBase Icon={CompactDownIcon} size={16} className="text-primary" />
</div>
));
Loading

0 comments on commit 9325498

Please sign in to comment.