Skip to content

Commit

Permalink
wallet-ext: dogfooding fixes (MystenLabs#2511)
Browse files Browse the repository at this point in the history
* wallet-ext: improve import mnemonic validation

* use formik & yup
* validate on change instead on blur to instantly enable/disable submit
* on blur transform the value of the textarea

* wallet-ext: css modules show original class name in dev mode

* also convert dashes to camelCase for exports

* wallet-ext: fix error message overflowing the container

* wallet-ext: link to explorer for account details

* from settings and active account label
* reuse components for explorer and external links

* wallet-ext: various ui fixes

* button color always black even for anchor elements
* wallet container stop expanding to full screen if not necessary
* coin type ellipsis text overflow (to avoid overflowing)
* coin type title

* wallet-ext: allow localhost and sui.io gateway host permissions

* to bypass cors when the api does not append the header for the extension origin

* wallet-ext: send coins format amount input
  • Loading branch information
pchrysochoidis authored Jun 10, 2022
1 parent dab176c commit a7ec54b
Show file tree
Hide file tree
Showing 30 changed files with 707 additions and 297 deletions.
16 changes: 14 additions & 2 deletions wallet/configs/webpack/webpack.config.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ const CONFIGS_ROOT = resolve(PROJECT_ROOT, 'configs');
const SRC_ROOT = resolve(PROJECT_ROOT, 'src');
const OUTPUT_ROOT = resolve(PROJECT_ROOT, 'dist');
const TS_CONFIGS_ROOT = resolve(CONFIGS_ROOT, 'ts');
const IS_DEV = process.env.NODE_ENV === 'development';
const TS_CONFIG_FILE = resolve(
TS_CONFIGS_ROOT,
`tsconfig.${process.env.NODE_ENV === 'development' ? 'dev' : 'prod'}.json`
`tsconfig.${IS_DEV ? 'dev' : 'prod'}.json`
);

function loadTsConfig(tsConfigFilePath: string) {
Expand Down Expand Up @@ -111,7 +112,18 @@ const commonConfig: () => Promise<Configuration> = async () => {
test: /\.(s)?css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: IS_DEV
? '[name]__[local]__[hash:base64:8]'
: '[hash:base64]',
exportLocalsConvention: 'dashes',
},
},
},
'postcss-loader',
'sass-loader',
],
Expand Down
27 changes: 22 additions & 5 deletions wallet/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-intl": "^6.0.2",
"react-number-format": "^4.9.3",
"react-redux": "^8.0.1",
"react-router-dom": "^6.3.0",
"rxjs": "^7.5.5",
Expand Down
6 changes: 5 additions & 1 deletion wallet/src/manifest/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"action": {
"default_popup": "ui.html?type=popup"
},
"host_permissions": [],
"host_permissions": [
"http://127.0.0.1:5001/",
"https://gateway.devnet.sui.io/",
"https://gateway.staging.sui.io/"
],
"icons": {
"16": "manifest/icons/sui-icon-16.png",
"32": "manifest/icons/sui-icon-32.png",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
.address-container {
display: inline-flex;
flex-direction: row;
align-items: center;
}

.address {
font-size: 15px;
font-weight: 700;
color: #404040;
}

.explorer-link {
color: inherit;
margin-left: 5px;
}
20 changes: 15 additions & 5 deletions wallet/src/ui/app/components/account-address/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import CopyToClipboard from '_components/copy-to-clipboard';
import ExplorerLink from '_components/explorer-link';
import { ExplorerLinkType } from '_components/explorer-link/ExplorerLinkType';
import { useAppSelector, useMiddleEllipsis } from '_hooks';

import st from './AccountAddress.module.scss';
Expand All @@ -12,11 +14,19 @@ function AccountAddress() {
);
const shortenAddress = useMiddleEllipsis(address || '');
return address ? (
<CopyToClipboard txt={address}>
<span className={st.address} title={address}>
{shortenAddress}
</span>
</CopyToClipboard>
<span className={st['address-container']}>
<CopyToClipboard txt={address}>
<span className={st.address} title={address}>
{shortenAddress}
</span>
</CopyToClipboard>
<ExplorerLink
type={ExplorerLinkType.address}
useActiveAddress={true}
title="View account on Sui Explorer"
className={st.explorerLink}
/>
</span>
) : null;
}

Expand Down
55 changes: 55 additions & 0 deletions wallet/src/ui/app/components/address-input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { memo, useCallback, useMemo } from 'react';

import { SUI_ADDRESS_VALIDATION } from './validation';

import type { SuiAddress } from '@mysten/sui.js';
import type { FieldProps } from 'formik';
import type { ChangeEventHandler } from 'react';

export interface AddressInputProps<Values>
extends FieldProps<SuiAddress, Values> {
disabled?: boolean;
placeholder?: string;
className?: string;
}

function AddressInput<FormValues>({
disabled: forcedDisabled,
placeholder = '0x...',
className,
form: { isSubmitting, setFieldValue },
field: { onBlur, name, value },
}: AddressInputProps<FormValues>) {
const disabled =
forcedDisabled !== undefined ? forcedDisabled : isSubmitting;
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(e) => {
const address = e.currentTarget.value;
setFieldValue(name, SUI_ADDRESS_VALIDATION.cast(address));
},
[setFieldValue, name]
);
const formattedValue = useMemo(
() => SUI_ADDRESS_VALIDATION.cast(value),
[value]
);
return (
<input
type="text"
{...{
disabled,
placeholder,
className,
onBlur,
value: formattedValue,
name,
onChange: handleOnChange,
}}
/>
);
}

export default memo(AddressInput);
22 changes: 22 additions & 0 deletions wallet/src/ui/app/components/address-input/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { isValidSuiAddress } from '@mysten/sui.js';
import * as Yup from 'yup';

export const SUI_ADDRESS_VALIDATION = Yup.string()
.ensure()
.trim()
.required()
.transform((value: string) =>
value.startsWith('0x') || value === '' || value === '0'
? value
: `0x${value}`
)
.test(
'is-sui-address',
// eslint-disable-next-line no-template-curly-in-string
'${value} is not a valid Sui address',
(value) => isValidSuiAddress(value)
)
.label("Recipient's address");
1 change: 1 addition & 0 deletions wallet/src/ui/app/components/alert/Alert.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ $error-color: #8b1111;

.message {
margin-left: 12px;
overflow-wrap: anywhere;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
font-weight: 700;
letter-spacing: -0.6px;
color: #8bc3df;
max-width: 95%;
text-overflow: ellipsis;
overflow: hidden;
}

.value {
Expand Down
4 changes: 3 additions & 1 deletion wallet/src/ui/app/components/coin-balance/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ function CoinBalance({ type, balance }: CoinProps) {
);
return (
<div className={st.container}>
<span className={st.type}>{type}</span>
<span className={st.type} title={type}>
{type}
</span>
<span>
<span className={st.value}>{balanceFormatted}</span>
<span className={st.symbol}>{symbol}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { API_ENV, DEFAULT_API_ENV } from '_app/ApiProvider';

import type { ObjectId, TransactionDigest } from '@mysten/sui.js';
import type { ObjectId, SuiAddress, TransactionDigest } from '@mysten/sui.js';

const API_ENV_TO_EXPLORER_URL: Record<API_ENV, string | undefined> = {
[API_ENV.local]: process.env.EXPLORER_URL_LOCAL,
Expand Down Expand Up @@ -34,4 +34,8 @@ export class Explorer {
Explorer._url
).href;
}

public static getAddressUrl(address: SuiAddress) {
return new URL(`/addresses/${address}`, Explorer._url).href;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

export enum ExplorerLinkType {
address,
object,
transaction,
}
73 changes: 73 additions & 0 deletions wallet/src/ui/app/components/explorer-link/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { memo, useMemo } from 'react';

import { Explorer } from './Explorer';
import { ExplorerLinkType } from './ExplorerLinkType';
import BsIcon from '_components/bs-icon';
import ExternalLink from '_components/external-link';
import { useAppSelector } from '_hooks';
import { activeAccountSelector } from '_redux/slices/account';

import type { ObjectId, SuiAddress, TransactionDigest } from '@mysten/sui.js';
import type { ReactNode } from 'react';

export type ExplorerLinkProps = (
| {
type: ExplorerLinkType.address;
address: SuiAddress;
useActiveAddress?: false;
}
| {
type: ExplorerLinkType.address;
useActiveAddress: true;
}
| { type: ExplorerLinkType.object; objectID: ObjectId }
| { type: ExplorerLinkType.transaction; transactionID: TransactionDigest }
) & { children?: ReactNode; className?: string; title?: string };

function useAddress(props: ExplorerLinkProps) {
const { type } = props;
const isAddress = type === ExplorerLinkType.address;
const isProvidedAddress = isAddress && !props.useActiveAddress;
const activeAddress = useAppSelector(activeAccountSelector);
return isProvidedAddress ? props.address : activeAddress;
}

function ExplorerLink(props: ExplorerLinkProps) {
const { type, children, className, title } = props;
const address = useAddress(props);
const objectID = type === ExplorerLinkType.object ? props.objectID : null;
const transactionID =
type === ExplorerLinkType.transaction ? props.transactionID : null;
const explorerHref = useMemo(() => {
switch (type) {
case ExplorerLinkType.address:
return address && Explorer.getAddressUrl(address);
case ExplorerLinkType.object:
return objectID && Explorer.getObjectUrl(objectID);
case ExplorerLinkType.transaction:
return (
transactionID && Explorer.getTransactionUrl(transactionID)
);
}
}, [type, address, objectID, transactionID]);
if (!explorerHref) {
return null;
}
return (
<ExternalLink
href={explorerHref}
className={className}
title={title}
showIcon={false}
>
<>
{children} <BsIcon icon="box-arrow-up-right" />
</>
</ExternalLink>
);
}

export default memo(ExplorerLink);
Loading

0 comments on commit a7ec54b

Please sign in to comment.