Commit 15539ccb authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1826

parents b9508d73 abf8a445
changelog:
categories:
- title: 🚀 New Features
labels:
- client feature
- feature
- title: 🐛 Bug Fixes
labels:
- bug
- title: ⚡ Performance Improvements
labels:
- performance
- title: 📦 Dependencies Updates
labels:
- dependencies
- title: 🚨 Changes in ENV variables
labels:
- ENVs
- title: ✨ Other Changes
labels:
- "*"
\ No newline at end of file
name: Copy issues labels to pull request
on:
workflow_dispatch:
inputs:
pr_number:
description: Pull request number
required: true
type: string
issues:
description: JSON encoded list of issue ids
required: true
type: string
workflow_call:
inputs:
pr_number:
description: Pull request number
required: true
type: string
issues:
description: JSON encoded list of issue ids
required: true
type: string
jobs:
run:
name: Run
runs-on: ubuntu-latest
steps:
- name: Find unique labels
id: find_unique_labels
uses: actions/github-script@v7
env:
ISSUES: ${{ inputs.issues }}
with:
script: |
const issues = JSON.parse(process.env.ISSUES);
const WHITE_LISTED_LABELS = [
'client feature',
'feature',
'bug',
'dependencies',
'performance',
'chore',
'enhancement',
'refactoring',
'tech',
'ENVs',
]
const labels = await Promise.all(issues.map(getIssueLabels));
const uniqueLabels = uniqueStringArray(labels.flat().filter((label) => WHITE_LISTED_LABELS.includes(label)));
if (uniqueLabels.length === 0) {
core.info('No labels found.\n');
return [];
}
core.info(`Found following labels: ${ uniqueLabels.join(', ') }.\n`);
return uniqueLabels;
async function getIssueLabels(issue) {
core.info(`Obtaining labels list for the issue #${ issue }...`);
try {
const response = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', {
owner: 'blockscout',
repo: 'frontend',
issue_number: issue,
});
return response.data.map(({ name }) => name);
} catch (error) {
core.error(`Failed to obtain labels for the issue #${ issue }: ${ error.message }`);
return [];
}
}
function uniqueStringArray(array) {
return Array.from(new Set(array));
}
- name: Update pull request labels
id: update_pr_labels
uses: actions/github-script@v7
env:
LABELS: ${{ steps.find_unique_labels.outputs.result }}
PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const labels = JSON.parse(process.env.LABELS);
const prNumber = Number(process.env.PR_NUMBER);
if (labels.length === 0) {
core.info('Nothing to update.\n');
return;
}
for (const label of labels) {
await addLabelToPr(prNumber, label);
}
core.info('Done.\n');
async function addLabelToPr(prNumber, label) {
console.log(`Adding label to the pull request #${ prNumber }...`);
return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', {
owner: 'blockscout',
repo: 'frontend',
issue_number: prNumber,
labels: [ label ],
});
}
\ No newline at end of file
......@@ -55,7 +55,10 @@ jobs:
label_description: Tasks in pre-release right now
secrets: inherit
# Temporary disable this step because it is broken
# There is an issue with building web3modal deps
upload_source_maps:
name: Upload source maps to Sentry
if: false
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
......@@ -28,7 +28,7 @@ jobs:
issues: "[${{ github.event.issue.number }}]"
secrets: inherit
review_requested_issues:
pr_linked_issues:
name: Get issues linked to PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request && github.event.action == 'review_requested' }}
......@@ -76,14 +76,24 @@ jobs:
return issues;
review_requested_tasks:
issues_in_review:
name: Update status for issues in review
needs: [ review_requested_issues ]
if: ${{ needs.review_requested_issues.outputs.issues }}
needs: [ pr_linked_issues ]
if: ${{ needs.pr_linked_issues.outputs.issues }}
uses: './.github/workflows/update-project-cards.yml'
secrets: inherit
with:
project_name: ${{ vars.PROJECT_NAME }}
field_name: Status
field_value: Review
issues: ${{ needs.review_requested_issues.outputs.issues }}
issues: ${{ needs.pr_linked_issues.outputs.issues }}
copy_labels:
name: Copy issues labels to pull request
needs: [ pr_linked_issues ]
if: ${{ needs.pr_linked_issues.outputs.issues }}
uses: './.github/workflows/copy-issues-labels.yml'
secrets: inherit
with:
pr_number: ${{ github.event.pull_request.number }}
issues: ${{ needs.pr_linked_issues.outputs.issues }}
......@@ -81,8 +81,11 @@ jobs:
with:
platforms: linux/amd64,linux/arm64/v8
# Temporary disable this step because it is broken
# There is an issue with building web3modal deps
upload_source_maps:
name: Upload source maps to Sentry
if: false
needs: publish_image
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
......@@ -2,6 +2,9 @@ import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items';
import type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
......@@ -21,6 +24,11 @@ const hiddenLinks = (() => {
return result;
})();
const defaultColorTheme = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
return COLOR_THEMES.find((theme) => theme.id === envValue);
})();
// eslint-disable-next-line max-len
const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
......@@ -70,6 +78,9 @@ const UI = Object.freeze({
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
},
hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false,
colorTheme: {
'default': defaultColorTheme,
},
});
export default UI;
......@@ -46,6 +46,7 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
......
......@@ -24,8 +24,8 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(51, 53, 67, 1)'
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(165, 252, 122, 1)'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51,53,67,1)
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165,252,122,1)
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
......
......@@ -51,5 +51,6 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
......@@ -28,6 +28,7 @@ import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage';
import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
......@@ -567,6 +568,7 @@ const schema = yup
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
// 5. Features configuration
NEXT_PUBLIC_API_SPEC_URL: yup.string().test(urlTest),
......
......@@ -21,6 +21,7 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}]
NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}]
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dim
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
......
......@@ -275,6 +275,7 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` |
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` |
| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` |
#### Network explorer configuration properties
......
......@@ -16,7 +16,7 @@ const PAGE_PROPS = {
cookies: '',
referrer: '',
query: {},
adBannerProvider: undefined,
adBannerProvider: null,
apiData: null,
};
......
......@@ -21,6 +21,10 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
'tooltipTitle',
'tooltipDescription',
'tooltipUrl',
'appID',
'appMarketplaceURL',
'appLogoURL',
'appActionButtonText',
];
for (const stringField of stringFields) {
......
......@@ -12,7 +12,7 @@ const AppContext = createContext<PageProps>({
cookies: '',
referrer: '',
query: {},
adBannerProvider: undefined,
adBannerProvider: null,
apiData: null,
});
......
......@@ -6,11 +6,13 @@ import {
import type { ChakraProviderProps } from '@chakra-ui/react';
import React from 'react';
import theme from 'theme';
interface Props extends ChakraProviderProps {
cookies?: string;
}
export function ChakraProvider({ cookies, theme, children }: Props) {
export function ChakraProvider({ cookies, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
......
......@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures {
test_value: string;
action_button_exp: boolean;
}
export const growthBook = (() => {
......
......@@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? (
} | {
'Type': 'Security score';
'Source': 'Analyzed contracts popup';
} | {
'Type': 'Action button';
'Info': string;
'Source': 'Txn' | 'NFT collection' | 'NFT item';
} | {
'Type': 'Address tag';
'Info': string;
......
import type { ColorMode } from '@chakra-ui/react';
import type { ColorThemeId } from 'types/settings';
interface ColorTheme {
id: ColorThemeId;
label: string;
colorMode: ColorMode;
hex: string;
sampleBg: string;
}
export const COLOR_THEMES: Array<ColorTheme> = [
{
id: 'light',
label: 'Light',
colorMode: 'light',
hex: '#FFFFFF',
sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)',
},
{
id: 'dim',
label: 'Dim',
colorMode: 'dark',
hex: '#232B37',
sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)',
},
{
id: 'midnight',
label: 'Midnight',
colorMode: 'dark',
hex: '#1B2E48',
sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)',
},
{
id: 'dark',
label: 'Dark',
colorMode: 'dark',
hex: '#101112',
sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)',
},
];
import type { IdenticonType } from 'types/views/address';
export const COLOR_THEMES = [
{
label: 'Light',
colorMode: 'light',
hex: '#FFFFFF',
sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)',
},
{
label: 'Dim',
colorMode: 'dark',
hex: '#232B37',
sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)',
},
{
label: 'Midnight',
colorMode: 'dark',
hex: '#1B2E48',
sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)',
},
{
label: 'Dark',
colorMode: 'dark',
hex: '#101112',
sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)',
},
];
export type ColorTheme = typeof COLOR_THEMES[number];
export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [
{
label: 'GitHub',
......
......@@ -19,8 +19,12 @@ export function middleware(req: NextRequest) {
return accountResponse;
}
const end = Date.now();
const res = NextResponse.next();
middlewares.colorTheme(req, res);
const end = Date.now();
res.headers.append('Content-Security-Policy', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
res.headers.append('Docker-ID', process.env.HOSTNAME || '');
......
......@@ -61,3 +61,18 @@ export const protocolTag: AddressMetadataTagApi = {
ordinal: 0,
meta: null,
};
export const protocolTagWithMeta: AddressMetadataTagApi = {
slug: 'uniswap',
name: 'Uniswap',
tagType: 'protocol',
ordinal: 0,
meta: {
appID: 'uniswap',
appMarketplaceURL: 'https://example.com',
appLogoURL: 'https://localhost:3100/icon.svg',
appActionButtonText: 'Swap',
textColor: '#FFFFFF',
bgColor: '#FF007A',
},
};
import type { AddressMetadataTagApi } from 'types/api/addressMetadata';
const appID = 'uniswap';
const appMarketplaceURL = 'https://example.com';
export const appLogoURL = 'https://localhost:3100/icon.svg';
const appActionButtonText = 'Swap';
const textColor = '#FFFFFF';
const bgColor = '#FF007A';
export const buttonWithoutStyles: AddressMetadataTagApi['meta'] = {
appID,
appMarketplaceURL,
appLogoURL,
appActionButtonText,
};
export const linkWithoutStyles: AddressMetadataTagApi['meta'] = {
appMarketplaceURL,
appLogoURL,
appActionButtonText,
};
export const buttonWithStyles: AddressMetadataTagApi['meta'] = {
appID,
appMarketplaceURL,
appLogoURL,
appActionButtonText,
textColor,
bgColor,
};
export const linkWithStyles: AddressMetadataTagApi['meta'] = {
appMarketplaceURL,
appLogoURL,
appActionButtonText,
textColor,
bgColor,
};
......@@ -56,6 +56,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
getFeaturePayload(config.features.addressVerification)?.api.endpoint,
getFeaturePayload(config.features.nameService)?.api.endpoint,
getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
marketplaceFeaturePayload && 'api' in marketplaceFeaturePayload ? marketplaceFeaturePayload.api.endpoint : '',
// chain RPC server
......
......@@ -14,7 +14,7 @@ export interface Props<Pathname extends Route['pathname'] = never> {
query: Route['query'];
cookies: string;
referrer: string;
adBannerProvider: AdBannerProviders | undefined;
adBannerProvider: AdBannerProviders | null;
// if apiData is undefined, Next.js will complain that it is not serializable
// so we force it to be always present in the props but it can be null
apiData: metadata.ApiData<Pathname> | null;
......@@ -32,7 +32,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
return adBannerFeature.provider;
}
}
return;
return null;
})();
return {
......@@ -40,7 +40,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
query,
cookies: req.headers.cookie || '',
referrer: req.headers.referer || '',
adBannerProvider,
adBannerProvider: adBannerProvider,
apiData: null,
},
};
......
import type { NextRequest, NextResponse } from 'next/server';
import appConfig from 'configs/app';
import * as cookiesLib from 'lib/cookies';
export default function colorThemeMiddleware(req: NextRequest, res: NextResponse) {
const colorModeCookie = req.cookies.get(cookiesLib.NAMES.COLOR_MODE);
if (!colorModeCookie) {
if (appConfig.UI.colorTheme.default) {
res.cookies.set(cookiesLib.NAMES.COLOR_MODE, appConfig.UI.colorTheme.default.colorMode, { path: '/' });
res.cookies.set(cookiesLib.NAMES.COLOR_MODE_HEX, appConfig.UI.colorTheme.default.hex, { path: '/' });
}
}
}
export { account } from './account';
export { default as colorTheme } from './colorTheme';
......@@ -18,7 +18,6 @@ import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout';
......@@ -57,7 +56,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
return (
<ChakraProvider theme={ theme } cookies={ pageProps.cookies }>
<ChakraProvider cookies={ pageProps.cookies }>
<AppErrorBoundary
{ ...ERROR_SCREEN_STYLES }
onError={ handleError }
......
......@@ -60,4 +60,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
noWalletClient: [
[ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ],
],
noNftMarketplaces: [
[ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ],
],
};
import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import getFormStyles from '../utils/getFormStyles';
import FancySelect from './FancySelect';
import FormLabel from './FormLabel';
import Input from './Input';
......@@ -13,8 +12,7 @@ const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const { theme } = props;
const { focusPlaceholderColor, errorColor } = getDefaultFormColors(props);
const formStyles = getFormStyles(props);
const activeLabelStyles = {
...FormLabel.variants?.floating?.(props)._focusWithin,
......@@ -63,12 +61,29 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
// label styles
label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles,
'textarea:not(:placeholder-shown) + label': {
bgColor: formStyles.input.filled.bgColor,
},
[`
input[readonly] + label,
textarea[readonly] + label,
&[aria-readonly=true] label
`]: {
bgColor: formStyles.input.readOnly.bgColor,
},
[`
input[aria-invalid=true] + label,
textarea[aria-invalid=true] + label,
&[aria-invalid=true] label
`]: {
color: getColor(theme, errorColor),
color: formStyles.placeholder.error.color,
},
[`
input[disabled] + label,
textarea[disabled] + label,
&[aria-disabled=true] label
`]: {
color: formStyles.placeholder.disabled.color,
},
// input styles
......@@ -79,31 +94,24 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
padding: inputPx,
},
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
[`
input[disabled] + label,
&[aria-disabled=true] label
`]: {
backgroundColor: 'transparent',
},
// in textarea bg of label could not be transparent; it should match the background color of input but without alpha
// so we have to use non-standard colors here
'textarea[disabled] + label': {
backgroundColor: mode('#ececec', '#232425')(props),
},
'textarea[disabled] + label[data-in-modal=true]': {
backgroundColor: mode('#ececec', '#292b34')(props),
},
// indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, focusPlaceholderColor),
color: formStyles.placeholder.default.color,
},
[`
input[aria-invalid=true] + label .chakra-form__required-indicator,
textarea[aria-invalid=true] + label .chakra-form__required-indicator,
&[aria-invalid=true] .chakra-form__required-indicator
`]: {
color: getColor(theme, errorColor),
color: formStyles.placeholder.error.color,
},
[`
input[disabled] + label .chakra-form__required-indicator,
textarea[disabled] + label .chakra-form__required-indicator,
&[aria-disabled=true] .chakra-form__required-indicator
`]: {
color: formStyles.placeholder.disabled.color,
},
},
};
......
......@@ -65,6 +65,19 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot();
});
test('filled read-only', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isReadOnly/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('filled error', async({ mount }) => {
const component = await mount(
<TestApp>
......
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { getColor } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import getFormStyles from '../utils/getFormStyles';
const baseStyle = defineStyle({
display: 'flex',
......@@ -13,14 +12,12 @@ const baseStyle = defineStyle({
transitionDuration: 'normal',
opacity: 1,
_disabled: {
opacity: 0.4,
opacity: 0.2,
},
});
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusPlaceholderColor } = getDefaultFormColors(props);
const bc = backgroundColor || 'transparent';
const formStyles = getFormStyles(props);
return {
left: '2px',
......@@ -29,8 +26,8 @@ const variantFloating = defineStyle((props) => {
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
color: formStyles.placeholder.default.color,
backgroundColor: props.bgColor || props.backgroundColor || 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
......@@ -39,8 +36,8 @@ const variantFloating = defineStyle((props) => {
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
_focusWithin: {
backgroundColor: bc,
color: getColor(theme, focusPlaceholderColor),
backgroundColor: props.bgColor || props.backgroundColor || 'transparent',
color: formStyles.placeholder.default.color,
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
......@@ -70,7 +67,7 @@ const sizes = {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 4px 28px 24px',
padding: '26px 4px 26px 24px',
right: '22px',
_focusWithin: {
padding: '16px 0 2px 24px',
......
......@@ -10,11 +10,11 @@ import { runIfFn } from '@chakra-ui/utils';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleDialog = defineStyle((props) => {
const baseStyleDialog = defineStyle(() => {
return {
padding: 8,
borderRadius: 'lg',
bg: mode('white', 'gray.900')(props),
bg: 'dialog_bg',
margin: 'auto',
};
});
......@@ -61,7 +61,7 @@ const baseStyleOverlay = defineStyle({
});
const baseStyle = definePartsStyle((props) => ({
dialog: runIfFn(baseStyleDialog, props),
dialog: runIfFn(baseStyleDialog),
dialogContainer: baseStyleDialogContainer,
header: runIfFn(baseStyleHeader, props),
......
import { Textarea as TextareaComponent } from '@chakra-ui/react';
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const variantFilledInactive = defineStyle((props) => {
return {
// https://bugs.chromium.org/p/chromium/issues/detail?id=1362573
// there is a problem with scrollbar color in chromium
// so blackAlpha.50 here is replaced with #f5f5f6
// and whiteAlpha.50 is replaced with #1a1b1b
// bgColor: mode('blackAlpha.50', 'whiteAlpha.50')(props),
bgColor: mode('#f5f5f6', '#1a1b1b')(props),
};
});
const sizes = {
md: defineStyle({
fontSize: 'md',
......@@ -38,7 +24,6 @@ const Textarea = defineStyleConfig({
sizes,
variants: {
outline: defineStyle(getOutlinedFieldStyles),
filledInactive: variantFilledInactive,
},
defaultProps: {
variant: 'outline',
......
import { type ThemeConfig } from '@chakra-ui/react';
import appConfig from 'configs/app';
const config: ThemeConfig = {
initialColorMode: 'system',
initialColorMode: appConfig.UI.colorTheme.default?.colorMode ?? 'system',
useSystemColorMode: false,
disableTransitionOnChange: false,
};
......
......@@ -23,6 +23,10 @@ const semanticTokens = {
'default': 'red.400',
_dark: 'red.300',
},
dialog_bg: {
'default': 'white',
_dark: 'gray.900',
},
},
shadows: {
action_bar: '0 4px 4px -4px rgb(0 0 0 / 10%), 0 2px 4px -4px rgb(0 0 0 / 6%)',
......
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { mode } from '@chakra-ui/theme-tools';
export default function getDefaultFormColors(props: StyleFunctionProps) {
const { focusBorderColor: fc, errorBorderColor: ec, filledBorderColor: flc } = props;
return {
focusBorderColor: fc || mode('blue.500', 'blue.300')(props),
focusPlaceholderColor: fc || 'gray.500',
errorColor: ec || mode('red.400', 'red.300')(props),
filledColor: flc || mode('gray.300', 'gray.600')(props),
};
}
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { mode, transparentize } from '@chakra-ui/theme-tools';
export default function getFormStyles(props: StyleFunctionProps) {
return {
input: {
empty: {
// there is no text in the empty input
// color: ???,
bgColor: props.bgColor || mode('white', 'black')(props),
borderColor: mode('gray.100', 'gray.700')(props),
},
hover: {
color: mode('gray.800', 'gray.50')(props),
bgColor: props.bgColor || mode('white', 'black')(props),
borderColor: mode('gray.200', 'gray.500')(props),
},
focus: {
color: mode('gray.800', 'gray.50')(props),
bgColor: props.bgColor || mode('white', 'black')(props),
borderColor: mode('blue.400', 'blue.400')(props),
},
filled: {
color: mode('gray.800', 'gray.50')(props),
bgColor: props.bgColor || mode('white', 'black')(props),
borderColor: mode('gray.300', 'gray.600')(props),
},
readOnly: {
color: mode('gray.800', 'gray.50')(props),
bgColor: mode('gray.200', 'gray.800')(props),
borderColor: mode('gray.200', 'gray.800')(props),
},
// we use opacity to show the disabled state
disabled: {
opacity: 0.2,
},
error: {
color: mode('gray.800', 'gray.50')(props),
bgColor: props.bgColor || mode('white', 'black')(props),
borderColor: mode('red.500', 'red.500')(props),
},
},
placeholder: {
'default': {
color: mode('gray.500', 'gray.500')(props),
},
disabled: {
color: transparentize('gray.500', 0.2)(props.theme),
},
error: {
color: mode('red.500', 'red.500')(props),
},
},
};
}
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { mode, getColor } from '@chakra-ui/theme-tools';
import { mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from './getDefaultFormColors';
import getDefaultTransitionProps from './getDefaultTransitionProps';
import getFormStyles from './getFormStyles';
export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
const { theme, borderColor } = props;
const { focusBorderColor, errorColor } = getDefaultFormColors(props);
const formStyles = getFormStyles(props);
const transitionProps = getDefaultTransitionProps();
return {
border: '2px solid',
// filled input
backgroundColor: 'transparent',
borderColor: mode('gray.300', 'gray.600')(props),
...formStyles.input.filled,
...transitionProps,
_hover: {
borderColor: mode('gray.200', 'gray.500')(props),
...formStyles.input.hover,
},
_readOnly: {
boxShadow: 'none !important',
userSelect: 'all',
pointerEvents: 'none',
...formStyles.input.readOnly,
_hover: {
...formStyles.input.readOnly,
},
_focus: {
...formStyles.input.readOnly,
},
},
_disabled: {
opacity: 1,
backgroundColor: mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderColor: 'transparent',
...formStyles.input.disabled,
cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
':-webkit-autofill': {
// background color for disabled input which value was selected from browser autocomplete popup
'-webkit-box-shadow': `0 0 0px 1000px ${ mode('rgba(16, 17, 18, 0.08)', 'rgba(255, 255, 255, 0.08)')(props) } inset`,
},
},
_invalid: {
borderColor: getColor(theme, errorColor),
...formStyles.input.error,
boxShadow: `none`,
_placeholder: {
color: formStyles.placeholder.error.color,
},
},
_focusVisible: {
...formStyles.input.focus,
zIndex: 1,
borderColor: getColor(theme, focusBorderColor),
boxShadow: 'md',
},
_placeholder: {
color: mode('blackAlpha.600', 'whiteAlpha.600')(props),
color: formStyles.placeholder.default.color,
},
// not filled input
':placeholder-shown:not(:focus-visible):not(:hover):not([aria-invalid=true])': { borderColor: borderColor || mode('gray.100', 'gray.700')(props) },
':placeholder-shown:not(:focus-visible):not(:hover):not([aria-invalid=true]):not([aria-readonly=true])': {
...formStyles.input.empty,
},
// not filled input with type="date"
':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true])': {
borderColor: borderColor || mode('gray.100', 'gray.700')(props),
color: 'gray.500',
':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true]):not([aria-readonly=true])': {
...formStyles.input.empty,
},
':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
......
......@@ -26,6 +26,10 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
tooltipTitle?: string;
tooltipDescription?: string;
tooltipUrl?: string;
appID?: string;
appMarketplaceURL?: string;
appLogoURL?: string;
appActionButtonText?: string;
} | null;
}
......
export const COLOR_THEME_IDS = [ 'light', 'dim', 'midnight', 'dark' ] as const;
export type ColorThemeId = typeof COLOR_THEME_IDS[number];
......@@ -128,7 +128,6 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
size="xs"
value={ sourceType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="auto"
fontWeight={ 600 }
borderRadius="base"
......
......@@ -24,7 +24,6 @@ interface Props {
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
......@@ -39,7 +38,7 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onI
placeholder="Search by token name"
ml="1px"
onChange={ onInputChange }
borderColor={ inputBorderColor }
bgColor="dialog_bg"
/>
</InputGroup>
<Flex flexDir="column" rowGap={ 6 }>
......
......@@ -31,6 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
animationUrl={ tokenInstance?.animation_url ?? null }
imageUrl={ tokenInstance?.image_url ?? null }
isLoading={ isLoading }
autoplayVideo={ false }
/>
</Link>
<Flex justifyContent="space-between" w="100%" flexWrap="wrap">
......
import { FormControl, Input, useColorModeValue } from '@chakra-ui/react';
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
......@@ -15,13 +15,11 @@ interface Props {
}
const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor } mt={ 8 }>
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg" mt={ 8 }>
<Input
{ ...field }
required
......@@ -29,11 +27,12 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, backgroundColor ]);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
......
import { FormControl, Textarea, useColorModeValue } from '@chakra-ui/react';
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
......@@ -15,25 +15,24 @@ interface Props {
}
const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor }>
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Textarea
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled
isReadOnly
autoComplete="off"
maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/>
<InputPlaceholder text="Message to sign" error={ error } isInModal/>
<InputPlaceholder text="Message to sign" error={ error }/>
</FormControl>
);
}, [ formState.errors, backgroundColor ]);
}, [ formState.errors ]);
return (
<Controller
......
import { FormControl, Input, useColorModeValue } from '@chakra-ui/react';
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
......@@ -16,24 +16,23 @@ interface Props {
}
const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor }>
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Signature hash" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, backgroundColor ]);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
......
......@@ -105,7 +105,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting } flexShrink={ 0 }>
<Button size="lg" type="submit" isLoading={ formState.isSubmitting } loadingText="Continue" flexShrink={ 0 }>
Continue
</Button>
<AdminSupportText/>
......
......@@ -4,7 +4,6 @@ import {
FormControl,
FormLabel,
Input,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
......@@ -42,7 +41,6 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
const apiFetch = useApiFetch();
const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const updateApiKey = (data: Inputs) => {
const body = { name: data.name };
......@@ -102,25 +100,27 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<FormControl variant="floating" id="address">
<Input
{ ...field }
isDisabled={ true }
bgColor="dialog_bg"
isReadOnly
/>
<FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
<FormLabel>Auto-generated API key token</FormLabel>
</FormControl>
);
}, []);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }>
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
......@@ -108,7 +108,6 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
borderRadius="base"
value={ format }
onChange={ handleSelectChange }
focusBorderColor="none"
w="auto"
>
{ formats.map((format) => (
......
......@@ -4,7 +4,6 @@ import {
FormControl,
Input,
Textarea,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
......@@ -61,8 +60,6 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
};
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const mutation = useMutation({
mutationFn: customAbiKey,
onSuccess: (data) => {
......@@ -109,38 +106,40 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<AddressInput<Inputs, 'contract_address_hash'>
field={ field }
error={ errors.contract_address_hash }
backgroundColor={ formBackgroundColor }
bgColor="dialog_bg"
placeholder="Smart contract address (0x...)"
/>
);
}, [ errors, formBackgroundColor ]);
}, [ errors ]);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }>
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Project name" error={ errors.name }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
}, [ errors ]);
const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => {
return (
<FormControl variant="floating" id="abi" isRequired backgroundColor={ formBackgroundColor }>
<FormControl variant="floating" id="abi" isRequired bgColor="dialog_bg">
<Textarea
{ ...field }
size="lg"
minH="300px"
isInvalid={ Boolean(errors.abi) }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
......@@ -2,7 +2,7 @@ import { Box, Center, useColorMode, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
......@@ -16,6 +16,7 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
import ContentLoader from 'ui/shared/ContentLoader';
import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar';
......@@ -36,9 +37,10 @@ type Props = {
address: string | undefined;
data: MarketplaceAppOverview | undefined;
isPending: boolean;
appUrl?: string;
};
const MarketplaceAppContent = ({ address, data, isPending }: Props) => {
const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
const { iframeRef, isReady } = useDappscoutIframe();
const [ iframeKey, setIframeKey ] = useState(0);
......@@ -89,7 +91,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => {
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ data.url }
src={ appUrl }
title={ data.title }
onLoad={ handleIframeLoad }
/>
......@@ -132,6 +134,26 @@ const MarketplaceApp = () => {
const { data, isPending } = query;
const { setIsAutoConnectDisabled } = useMarketplaceContext();
const appUrl = useMemo(() => {
if (!data?.url) {
return;
}
try {
const customUrl = getQueryParamString(router.query.url);
const customOrigin = new URL(customUrl).origin;
const appOrigin = new URL(data.url).origin;
if (customOrigin === appOrigin) {
return customUrl;
} else {
removeQueryParam(router, 'url');
}
} catch (err) {}
return data.url;
}, [ data?.url, router ]);
useEffect(() => {
if (data) {
metadata.update(
......@@ -153,13 +175,13 @@ const MarketplaceApp = () => {
/>
<DappscoutIframeProvider
address={ address }
appUrl={ data?.url }
appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl }
sendTransaction={ sendTransaction }
signMessage={ signMessage }
signTypedData={ signTypedData }
>
<MarketplaceAppContent address={ address } data={ data } isPending={ isPending }/>
<MarketplaceAppContent address={ address } data={ data } isPending={ isPending } appUrl={ appUrl }/>
</DappscoutIframeProvider>
</Flex>
);
......
......@@ -27,7 +27,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
required
disabled
readOnly
value={ data.name || '' }
/>
<FormLabel>Name</FormLabel>
......@@ -35,7 +35,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="nickname" isRequired size="lg">
<Input
required
disabled
readOnly
value={ data.nickname || '' }
/>
<FormLabel>Nickname</FormLabel>
......@@ -43,7 +43,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="email" isRequired size="lg">
<Input
required
disabled
readOnly
value={ data.email || '' }
/>
<FormLabel>Email</FormLabel>
......
import {
Box,
Button,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
......@@ -42,8 +41,6 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
},
});
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { mutate } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
......@@ -87,12 +84,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
};
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]);
return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } bgColor="dialog_bg"/>;
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]);
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
import {
Box,
Button,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
......@@ -34,7 +33,6 @@ type Inputs = {
const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched',
......@@ -90,12 +88,12 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
};
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } error={ errors.transaction } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]);
return <TransactionInput field={ field } error={ errors.transaction } bgColor="dialog_bg"/>;
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]);
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
......@@ -13,7 +13,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
size?: InputProps['size'];
placeholder?: string;
backgroundColor?: string;
bgColor?: string;
error?: FieldError;
}
......@@ -23,14 +23,15 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
field,
size,
placeholder = 'Address (0x...)',
backgroundColor,
bgColor,
}: Props<Inputs, Name>) {
return (
<FormControl variant="floating" id="address" isRequired backgroundColor={ backgroundColor } size={ size }>
<FormControl variant="floating" id="address" isRequired size={ size } bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text={ placeholder } error={ error }/>
</FormControl>
......
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import * as actionButtonMetadataMock from 'mocks/metadata/appActionButton';
import { test, expect } from 'playwright/lib';
import AppActionButton from './AppActionButton';
test.beforeEach(async({ mockAssetResponse }) => {
await mockAssetResponse(actionButtonMetadataMock.appLogoURL as string, './playwright/mocks/image_s.jpg');
});
test('button without styles +@dark-mode', async({ render }) => {
const component = await render(
<Flex w="200px">
<AppActionButton
data={ actionButtonMetadataMock.buttonWithoutStyles as NonNullable<AddressMetadataTagFormatted['meta']> }
source="Txn"
/>
</Flex>,
);
await expect(component).toHaveScreenshot();
});
test('link without styles +@dark-mode', async({ render }) => {
const component = await render(
<Flex w="200px">
<AppActionButton
data={ actionButtonMetadataMock.linkWithoutStyles as NonNullable<AddressMetadataTagFormatted['meta']> }
source="Txn"
/>
</Flex>,
);
await expect(component).toHaveScreenshot();
});
test('button with styles', async({ render }) => {
const component = await render(
<Flex w="200px">
<AppActionButton
data={ actionButtonMetadataMock.buttonWithStyles as NonNullable<AddressMetadataTagFormatted['meta']> }
source="Txn"
/>
</Flex>,
);
await expect(component).toHaveScreenshot();
});
test('link with styles', async({ render }) => {
const component = await render(
<Flex w="200px">
<AppActionButton
data={ actionButtonMetadataMock.linkWithStyles as NonNullable<AddressMetadataTagFormatted['meta']> }
source="Txn"
/>
</Flex>,
);
await expect(component).toHaveScreenshot();
});
import { Button, Image, Text, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from '../LinkExternal';
type Props = {
data: NonNullable<AddressMetadataTagFormatted['meta']>;
className?: string;
txHash?: string;
source: 'Txn' | 'NFT collection' | 'NFT item';
}
const AppActionButton = ({ data, className, txHash, source }: Props) => {
const defaultTextColor = useColorModeValue('blue.600', 'blue.300');
const defaultBg = useColorModeValue('gray.100', 'gray.700');
const { appID, textColor, bgColor, appActionButtonText, appLogoURL, appMarketplaceURL } = data;
const actionURL = appMarketplaceURL?.replace('{chainId}', config.chain.id || '').replace('{txHash}', txHash || '');
const handleClick = React.useCallback(() => {
const info = appID || actionURL;
if (info) {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Action button', Info: info, Source: source });
}
}, [ source, appID, actionURL ]);
if ((!appID && !appMarketplaceURL) || (!appActionButtonText && !appLogoURL)) {
return null;
}
const content = (
<>
{ appLogoURL && (
<Image
src={ appLogoURL }
alt={ `${ appActionButtonText } button` }
boxSize={ 5 }
borderRadius="sm"
mr={ 2 }
/>
) }
<Text fontSize="sm" fontWeight="500" color="currentColor">
{ appActionButtonText }
</Text>
</>
);
return appID ? (
<Button
className={ className }
as="a"
href={ route({ pathname: '/apps/[id]', query: { id: appID, action: 'connect', ...(actionURL ? { url: actionURL } : {}) } }) }
onClick={ handleClick }
display="flex"
size="sm"
px={ 2 }
color={ textColor || defaultTextColor }
bg={ bgColor || defaultBg }
_hover={{ bg: bgColor, opacity: 0.9 }}
_active={{ bg: bgColor, opacity: 0.9 }}
>
{ content }
</Button>
) : (
<LinkExternal
className={ className }
href={ actionURL }
onClick={ handleClick }
variant="subtle"
display="flex"
px={ 2 }
iconColor={ textColor }
color={ textColor }
bg={ bgColor }
_hover={{ color: textColor }}
_active={{ color: textColor }}
>
{ content }
</LinkExternal>
);
};
export default chakra(AppActionButton);
import { useMemo } from 'react';
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
export default function useAppActionData(address: string | undefined = '', isEnabled = false) {
const memoizedArray = useMemo(() => (address && isEnabled) ? [ address ] : [], [ address, isEnabled ]);
const { data } = useAddressMetadataInfoQuery(memoizedArray);
const metadata = data?.addresses[address?.toLowerCase()];
const tag = metadata?.tags?.find(({ tagType }) => tagType === 'protocol');
if (tag?.meta?.appMarketplaceURL || tag?.meta?.appID) {
return tag.meta;
}
return null;
}
......@@ -92,5 +92,20 @@ const defaultProps = {
await expect(component).toHaveScreenshot();
});
test('read-only', async({ mount }) => {
const component = await mount(
<TestApp>
<FancySelect
{ ...defaultProps }
size={ size }
value={ OPTIONS[0] }
isReadOnly
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
});
});
......@@ -3,6 +3,9 @@ import type { Size, ChakraStylesConfig } from 'chakra-react-select';
import type { Option } from './types';
import theme from 'theme';
import getFormStyles from 'theme/utils/getFormStyles';
function getValueContainerStyles(size?: Size) {
switch (size) {
case 'sm':
......@@ -42,13 +45,12 @@ function getSingleValueStyles(size?: Size) {
}
const getChakraStyles: (colorMode: ColorMode) => ChakraStylesConfig<Option> = (colorMode) => {
const emptyInputBorderColor = colorMode === 'dark' ? 'gray.700' : 'gray.100';
const filledInputBorderColor = colorMode === 'dark' ? 'gray.600' : 'gray.300';
const formColor = getFormStyles({ colorMode, colorScheme: 'blue', theme });
return {
control: (provided, state) => ({
...provided,
borderColor: state.hasValue ? filledInputBorderColor : emptyInputBorderColor,
borderColor: state.hasValue ? formColor.input.filled.borderColor : formColor.input.empty.borderColor,
}),
inputContainer: (provided) => ({
...provided,
......
......@@ -6,12 +6,10 @@ interface Props {
text: string;
icon?: React.ReactNode;
error?: Partial<FieldError>;
className?: string;
isFancy?: boolean;
isInModal?: boolean;
}
const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }: Props) => {
const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') {
......@@ -20,10 +18,10 @@ const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }:
return (
<FormLabel
className={ className }
alignItems="center"
{ ...(isFancy ? { 'data-fancy': true } : {}) }
{ ...(isInModal ? { 'data-in-modal': true } : {}) }
variant="floating"
bgColor="deeppink"
>
{ icon }
<chakra.span>{ text }</chakra.span>
......@@ -32,4 +30,4 @@ const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }:
);
};
export default chakra(InputPlaceholder);
export default React.memo(InputPlaceholder);
......@@ -11,13 +11,29 @@ interface Props {
rightSlot?: React.ReactNode;
beforeSlot?: React.ReactNode;
textareaMaxHeight?: string;
textareaMinHeight?: string;
showCopy?: boolean;
isLoading?: boolean;
contentProps?: ChakraProps;
}
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading, contentProps }: Props) => {
// see issue in theme/components/Textarea.ts
const RawDataSnippet = ({
data,
className,
title,
rightSlot,
beforeSlot,
textareaMaxHeight,
textareaMinHeight,
showCopy = true,
isLoading,
contentProps,
}: Props) => {
// https://bugs.chromium.org/p/chromium/issues/detail?id=1362573
// there is a problem with scrollbar color in chromium
// so blackAlpha.50 here is replaced with #f5f5f6
// and whiteAlpha.50 is replaced with #1a1b1b
// const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return (
<Box className={ className } as="section" title={ title }>
......@@ -33,7 +49,7 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare
p={ 4 }
bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
minH={ textareaMinHeight || (isLoading ? '200px' : undefined) }
fontSize="sm"
borderRadius="md"
wordBreak="break-all"
......
import { Box, Flex, Select, Textarea } from '@chakra-ui/react';
import { Select } from '@chakra-ui/react';
import React from 'react';
import hexToUtf8 from 'lib/hexToUtf8';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
type DataType = 'Hex' | 'UTF-8'
const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ];
......@@ -18,24 +18,20 @@ const RawInputData = ({ hex }: Props) => {
setSelectedDataType(event.target.value as DataType);
}, []);
return (
<Box w="100%">
<Flex justifyContent="space-between" alignItems="center">
<Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } focusBorderColor="none" w="auto">
const select = (
<Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } w="auto" mr="auto">
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
<CopyToClipboard text={ hex }/>
</Flex>
<Textarea
value={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
);
return (
<RawDataSnippet
data={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
rightSlot={ select }
textareaMaxHeight="220px"
textareaMinHeight="160px"
w="100%"
maxH="220px"
mt={ 2 }
p={ 4 }
variant="filledInactive"
fontSize="sm"
/>
</Box>
);
};
......
......@@ -12,16 +12,17 @@ const TAG_MAX_LENGTH = 35;
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
error?: FieldError;
backgroundColor?: string;
bgColor?: string;
}
function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field, error, backgroundColor }: Props<Inputs, Name>) {
function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field, error, bgColor }: Props<Inputs, Name>) {
return (
<FormControl variant="floating" id="tag" isRequired backgroundColor={ backgroundColor }>
<FormControl variant="floating" id="tag" isRequired bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text="Private tag (max 35 characters)" error={ error }/>
</FormControl>
......
......@@ -11,16 +11,17 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<Field> = {
field: Field;
error?: FieldError;
backgroundColor?: string;
bgColor?: string;
}
function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValues, 'transaction'>>>({ field, error, backgroundColor }: Props<Field>) {
function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValues, 'transaction'>>>({ field, error, bgColor }: Props<Field>) {
return (
<FormControl variant="floating" id="transaction" isRequired backgroundColor={ backgroundColor }>
<FormControl variant="floating" id="transaction" isRequired bgColor={ bgColor }>
<Input
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH }
bgColor={ bgColor }
/>
<InputPlaceholder text="Transaction hash (0x...)" error={ error }/>
</FormControl>
......
......@@ -18,9 +18,10 @@ interface Props {
className?: string;
isLoading?: boolean;
withFullscreen?: boolean;
autoplayVideo?: boolean;
}
const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen }: Props) => {
const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen, autoplayVideo }: Props) => {
const [ isMediaLoading, setIsMediaLoading ] = React.useState(true);
const [ isLoadingError, setIsLoadingError ] = React.useState(false);
......@@ -71,7 +72,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen
switch (type) {
case 'video':
return <NftVideo { ...props }/>;
return <NftVideo { ...props } autoPlay={ autoplayVideo }/>;
case 'html':
return <NftHtml { ...props }/>;
case 'image':
......
......@@ -5,15 +5,17 @@ import { mediaStyleProps, videoPlayProps } from './utils';
interface Props {
src: string;
autoPlay?: boolean;
onLoad: () => void;
onError: () => void;
onClick?: () => void;
}
const NftVideo = ({ src, onLoad, onError, onClick }: Props) => {
const NftVideo = ({ src, autoPlay = true, onLoad, onError, onClick }: Props) => {
return (
<chakra.video
{ ...videoPlayProps }
autoPlay={ autoPlay }
src={ src }
onCanPlayThrough={ onLoad }
onError={ onError }
......
......@@ -18,6 +18,7 @@ const NftVideoFullscreen = ({ src, isOpen, onClose }: Props) => {
src={ src }
maxH="90vh"
maxW="90vw"
autoPlay={ true }
/>
</NftMediaFullscreenModal>
);
......
......@@ -41,7 +41,6 @@ export const mediaStyleProps = {
};
export const videoPlayProps = {
autoPlay: true,
disablePictureInPicture: true,
loop: true,
muted: true,
......
......@@ -46,7 +46,7 @@ const NetworkMenuContentMobile = ({ items, tabs }: Props) => {
) : (
<>
{ tabs.length > 1 && (
<Select size="xs" borderRadius="base" value={ selectedTab } onChange={ handleSelectChange } focusBorderColor="none" mb={ 6 }>
<Select size="xs" borderRadius="base" value={ selectedTab } onChange={ handleSelectChange } mb={ 6 }>
{ tabs.map((tab) => <option key={ tab } value={ tab }>{ capitalize(tab) }</option>) }
</Select>
) }
......
......@@ -2,9 +2,9 @@ import { Box, Flex, useColorMode } from '@chakra-ui/react';
import React from 'react';
import * as cookies from 'lib/cookies';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import SettingsSample from './SettingsSample';
import { COLOR_THEMES } from './utils';
const SettingsColorTheme = () => {
const { setColorMode } = useColorMode();
......
......@@ -3,9 +3,9 @@ import React from 'react';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
import { IDENTICONS } from 'lib/settings/identIcon';
import SettingsSample from './SettingsSample';
import { IDENTICONS } from './utils';
const SettingsIdentIcon = () => {
const [ activeId, setActiveId ] = React.useState<string>();
......
......@@ -7,13 +7,17 @@ import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getCurrencyValue from 'lib/getCurrencyValue';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMounted from 'lib/hooks/useIsMounted';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import TruncatedValue from 'ui/shared/TruncatedValue';
......@@ -27,6 +31,8 @@ interface Props {
const TokenDetails = ({ tokenQuery }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
const hash = router.query.hash?.toString();
const tokenCountersQuery = useApiQuery('token_counters', {
......@@ -34,6 +40,8 @@ const TokenDetails = ({ tokenQuery }: Props) => {
queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
});
const appActionData = useAppActionData(hash, isActionButtonExperiment);
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
router.push(
{ pathname: '/token/[hash]', query: { hash: hash || '', tab } },
......@@ -167,7 +175,26 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</DetailsInfoItem>
) }
{ type !== 'ERC-20' && <TokenNftMarketplaces hash={ hash } isLoading={ tokenQuery.isPlaceholderData }/> }
{ type !== 'ERC-20' && (
<TokenNftMarketplaces
hash={ hash }
isLoading={ tokenQuery.isPlaceholderData }
appActionData={ appActionData }
source="NFT collection"
isActionButtonExperiment={ isActionButtonExperiment }
/>
) }
{ (type !== 'ERC-20' && config.UI.views.nft.marketplaces.length === 0 && appActionData && isActionButtonExperiment) && (
<DetailsInfoItem
title="Dapp"
hint="Link to the dapp"
alignSelf="center"
py={ 1 }
>
<AppActionButton data={ appActionData } height="30px" source="NFT collection"/>
</DetailsInfoItem>
) }
<DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid>
......
......@@ -23,6 +23,7 @@ const TokenInventoryItem = ({ item, token, isLoading }: Props) => {
animationUrl={ item.animation_url }
imageUrl={ item.image_url }
isLoading={ isLoading }
autoplayVideo={ false }
/>
);
......
import { Image, Link, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import config from 'configs/app';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props {
hash: string | undefined;
id?: string;
isLoading?: boolean;
appActionData?: AddressMetadataTagFormatted['meta'];
source: 'NFT collection' | 'NFT item';
isActionButtonExperiment?: boolean;
}
const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
const TokenNftMarketplaces = ({ hash, id, isLoading, appActionData, source, isActionButtonExperiment }: Props) => {
if (!hash || config.UI.views.nft.marketplaces.length === 0) {
return null;
}
......@@ -21,8 +28,9 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
hint="Marketplaces trading this NFT"
alignSelf="center"
isLoading={ isLoading }
py={ (appActionData && isActionButtonExperiment) ? 1 : { base: 1, lg: 2 } }
>
<Skeleton isLoaded={ !isLoading } display="flex" columnGap={ 3 } flexWrap="wrap">
<Skeleton isLoaded={ !isLoading } display="flex" columnGap={ 3 } flexWrap="wrap" alignItems="center">
{ config.UI.views.nft.marketplaces.map((item) => {
const hrefTemplate = id ? item.instance_url : item.collection_url;
......@@ -41,6 +49,12 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
</Tooltip>
);
}) }
{ (appActionData && isActionButtonExperiment) && (
<>
<TextSeparator color="gray.500" margin={ 0 }/>
<AppActionButton data={ appActionData } height="30px" source={ source }/>
</>
) }
</Skeleton>
</DetailsInfoItem>
);
......
......@@ -18,7 +18,7 @@ const TokenInfoFieldAddress = ({ control }: Props) => {
<Input
{ ...field }
required
isDisabled
isReadOnly
/>
<InputPlaceholder text="Token contract address"/>
</FormControl>
......
......@@ -19,7 +19,8 @@ const TokenInfoFieldComment = ({ control, isReadOnly }: Props) => {
<Textarea
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
......
......@@ -20,7 +20,8 @@ const TokenInfoFieldDocs = ({ control, isReadOnly }: Props) => {
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Docs" error={ fieldState.error }/>
......
......@@ -58,7 +58,8 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
{ ...field }
onBlur={ handleBlur }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
required
/>
......
......@@ -22,7 +22,8 @@ const TokenInfoFieldPriceTicker = ({ control, isReadOnly, name, label }: Props)
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text={ label } error={ fieldState.error }/>
......
......@@ -20,7 +20,8 @@ const TokenInfoFieldProjectDescription = ({ control, isReadOnly }: Props) => {
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
......
......@@ -21,7 +21,8 @@ const TokenInfoFieldProjectEmail = ({ control, isReadOnly }: Props) => {
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Official project email address" error={ fieldState.error }/>
......
......@@ -20,7 +20,8 @@ const TokenInfoFieldProjectName = ({ control, isReadOnly }: Props) => {
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Project name" error={ fieldState.error }/>
......
......@@ -29,7 +29,8 @@ const TokenInfoFieldProjectSector = ({ control, isReadOnly, config }: Props) =>
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Project industry"
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
error={ fieldState.error }
/>
);
......
......@@ -20,7 +20,8 @@ const TokenInfoFieldProjectWebsite = ({ control, isReadOnly }: Props) => {
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
required
/>
......
......@@ -21,7 +21,8 @@ const TokenInfoFieldRequesterEmail = ({ control, isReadOnly }: Props) => {
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Requester email" error={ fieldState.error }/>
......
......@@ -20,7 +20,8 @@ const TokenInfoFieldRequesterName = ({ control, isReadOnly }: Props) => {
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Requester name" error={ fieldState.error }/>
......
......@@ -42,7 +42,8 @@ const TokenInfoFieldSocialLink = ({ control, isReadOnly, name }: Props) => {
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text={ SETTINGS[name].label } error={ fieldState.error }/>
......
......@@ -22,7 +22,8 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => {
<Input
{ ...field }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly }
isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Support URL or email" error={ fieldState.error }/>
......
......@@ -18,7 +18,7 @@ const TokenInfoFieldTokenName = ({ control }: Props) => {
<Input
{ ...field }
required
isDisabled
isReadOnly
/>
<InputPlaceholder text="Token name"/>
</FormControl>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { AddressMetadataInfo, AddressMetadataTagApi } from 'types/api/addressMetadata';
import config from 'configs/app';
import * as addressMock from 'mocks/address/address';
import { protocolTagWithMeta } from 'mocks/metadata/address';
import { tokenInfoERC721a } from 'mocks/tokens/tokenInfo';
import * as tokenInstanceMock from 'mocks/tokens/tokenInstance';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import TokenInstanceDetails from './TokenInstanceDetails';
const hash = tokenInfoERC721a.address;
const API_URL_ADDRESS = buildApiUrl('address', { hash });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.unique.id,
hash,
const addressMetadataQueryParams = {
addresses: [ hash ],
chainId: config.chain.id,
tagsLimit: '20',
};
function generateAddressMetadataResponse(tag: AddressMetadataTagApi) {
return {
addresses: {
[ hash.toLowerCase() as string ]: {
tags: [ {
...tag,
meta: JSON.stringify(tag.meta),
} ],
},
},
} as AddressMetadataInfo;
}
test.beforeEach(async({ mockApiResponse, mockAssetResponse }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse('token_instance_transfers_count', { transfers_count: 42 }, { pathParams: { id: tokenInstanceMock.unique.id, hash } });
await mockAssetResponse('http://localhost:3000/nft-marketplace-logo.png', './playwright/mocks/image_s.jpg');
});
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route('http://localhost:3000/nft-marketplace-logo.png', (route) => route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
}));
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_TOKEN_TRANSFERS_COUNT, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ transfers_count: 42 }),
}));
const component = await mount(
test('base view +@dark-mode +@mobile', async({ render, page }) => {
const component = await render(
<TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.unique } token={ tokenInfoERC721a }/>
</TestApp>,
......@@ -43,3 +53,40 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => {
maskColor: configs.maskColor,
});
});
test.describe('action button', () => {
test.beforeEach(async({ mockFeatures, mockApiResponse, mockAssetResponse }) => {
await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
});
test('base view +@dark-mode +@mobile', async({ render, page }) => {
const component = await render(
<TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.unique } token={ tokenInfoERC721a }/>
</TestApp>,
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
});
});
test('without marketplaces +@dark-mode +@mobile', async({ render, page, mockEnvs }) => {
mockEnvs(ENVS_MAP.noNftMarketplaces);
const component = await render(
<TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.unique } token={ tokenInfoERC721a }/>
</TestApp>,
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
});
});
});
......@@ -3,6 +3,10 @@ import React from 'react';
import type { TokenInfo, TokenInstance } from 'types/api/token';
import config from 'configs/app';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
......@@ -24,6 +28,9 @@ interface Props {
}
const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
const appActionData = useAppActionData(token?.address, isActionButtonExperiment && !isLoading);
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
......@@ -71,7 +78,24 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
</Flex>
</DetailsInfoItem>
<TokenInstanceTransfersCount hash={ isLoading ? '' : token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/>
<TokenNftMarketplaces isLoading={ isLoading } hash={ token.address } id={ data.id }/>
<TokenNftMarketplaces
isLoading={ isLoading }
hash={ token.address }
id={ data.id }
appActionData={ appActionData }
source="NFT item"
isActionButtonExperiment={ isActionButtonExperiment }
/>
{ (config.UI.views.nft.marketplaces.length === 0 && appActionData && isActionButtonExperiment) && (
<DetailsInfoItem
title="Dapp"
hint="Link to the dapp"
alignSelf="center"
py={ 1 }
>
<AppActionButton data={ appActionData } height="30px" source="NFT item"/>
</DetailsInfoItem>
) }
</Grid>
<NftMedia
animationUrl={ data.animation_url }
......
......@@ -39,7 +39,7 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
<Box>
<Flex alignItems="center" mb={ 6 }>
<chakra.span fontWeight={ 500 }>Metadata</chakra.span>
<Select size="xs" borderRadius="base" value={ format } onChange={ handleSelectChange } focusBorderColor="none" w="auto" ml={ 5 }>
<Select size="xs" borderRadius="base" value={ format } onChange={ handleSelectChange } w="auto" ml={ 5 }>
<option value="Table">Table</option>
<option value="JSON">JSON</option>
</Select>
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import compareBns from 'lib/bigint/compareBns';
// import { apos } from 'lib/html-entities';
import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils';
......@@ -31,23 +32,20 @@ const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefine
const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) {
case 'value-desc': {
const result = a.value > b.value ? -1 : 1;
return a.value === b.value ? 0 : result;
return compareBns(b.value, a.value);
}
case 'value-asc': {
const result = a.value > b.value ? 1 : -1;
return a.value === b.value ? 0 : result;
return compareBns(a.value, b.value);
}
case 'gas-limit-desc': {
const result = a.gas_limit > b.gas_limit ? -1 : 1;
return a.gas_limit === b.gas_limit ? 0 : result;
return compareBns(b.gas_limit, a.gas_limit);
}
case 'gas-limit-asc': {
const result = a.gas_limit > b.gas_limit ? 1 : -1;
return a.gas_limit === b.gas_limit ? 0 : result;
return compareBns(a.gas_limit, b.gas_limit);
}
default:
......
import React from 'react';
import type { AddressMetadataInfo, AddressMetadataTagApi } from 'types/api/addressMetadata';
import config from 'configs/app';
import { protocolTagWithMeta } from 'mocks/metadata/address';
import * as txMock from 'mocks/txs/tx';
import { txInterpretation } from 'mocks/txs/txInterpretation';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
......@@ -16,6 +20,25 @@ const txQuery = {
isError: false,
} as TxQuery;
const addressMetadataQueryParams = {
addresses: [ txMock.base.to?.hash as string ],
chainId: config.chain.id,
tagsLimit: '20',
};
function generateAddressMetadataResponse(tag: AddressMetadataTagApi) {
return {
addresses: {
[ txMock.base.to?.hash?.toLowerCase() as string ]: {
tags: [ {
...tag,
meta: JSON.stringify(tag.meta),
} ],
},
},
} as AddressMetadataInfo;
}
test('no interpretation +@mobile', async({ render }) => {
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot();
......@@ -32,6 +55,16 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot();
});
test('with interpretation and action button +@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse, mockFeatures }) => {
await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
await mockApiResponse('tx_interpretation', txInterpretation, { pathParams: { hash } });
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot();
});
test('with interpretation and view all link +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse(
'tx_interpretation',
......@@ -42,13 +75,40 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot();
});
test('no interpretation, has method called', async({ render, mockApiResponse }) => {
test('with interpretation and view all link, and action button (external link) +@mobile', async({
render, mockApiResponse, mockAssetResponse, mockFeatures,
}) => {
await mockFeatures([ [ 'action_button_exp', true ] ]);
delete protocolTagWithMeta?.meta?.appID;
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
await mockApiResponse(
'tx_interpretation',
{ data: { summaries: [ ...txInterpretation.data.summaries, ...txInterpretation.data.summaries ] } },
{ pathParams: { hash } },
);
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot();
});
test('no interpretation, has method called', async({ render, mockApiResponse, mockFeatures }) => {
// the action button should not render if there is no interpretation
await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockApiResponse('tx_interpretation', { data: { summaries: [] } }, { pathParams: { hash } });
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot();
});
test('no interpretation', async({ render, mockApiResponse }) => {
test('no interpretation', async({ render, mockApiResponse, mockFeatures }) => {
// the action button should not render if there is no interpretation
await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
const txPendingQuery = {
data: txMock.pending,
isPlaceholderData: false,
......
......@@ -3,9 +3,12 @@ import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
......@@ -26,6 +29,9 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const hasInterpretationFeature = feature.isEnabled;
const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves';
const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
const appActionData = useAppActionData(txQuery.data?.to?.hash, isActionButtonExperiment && !txQuery.isPlaceholderData);
const txInterpretationQuery = useApiQuery('tx_interpretation', {
pathParams: { hash },
queryOptions: {
......@@ -42,7 +48,6 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
},
});
const content = (() => {
const hasNovesInterpretation = isNovesInterpretation &&
(novesInterpretationQuery.isPlaceholderData || Boolean(novesInterpretationQuery.data?.classificationData.description));
......@@ -52,6 +57,11 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const hasViewAllInterpretationsLink =
!txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1;
const hasAnyInterpretation =
(hasNovesInterpretation && novesInterpretationQuery.data && !novesInterpretationQuery.isPlaceholderData) ||
(hasInternalInterpretation && !txInterpretationQuery.isPlaceholderData);
const content = (() => {
if (hasNovesInterpretation && novesInterpretationQuery.data) {
const novesSummary = createNovesSummaryObject(novesInterpretationQuery.data);
......@@ -108,9 +118,18 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
return (
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
{ content }
<Flex alignItems="center" justifyContent={{ base: 'start', lg: 'space-between' }} flexGrow={ 1 }>
{ !hasTag && <AccountActionsMenu mr={ 3 } mt={{ base: 3, lg: 0 }}/> }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }} mt={{ base: 3, lg: 0 }}/>
<Flex
alignItems="center"
justifyContent={{ base: 'start', lg: 'space-between' }}
flexGrow={ 1 }
gap={ 3 }
mt={{ base: 3, lg: 0 }}
>
{ !hasTag && <AccountActionsMenu/> }
{ (appActionData && isActionButtonExperiment && hasAnyInterpretation) && (
<AppActionButton data={ appActionData } txHash={ hash } source="Txn"/>
) }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }}/>
</Flex>
</Box>
);
......
......@@ -2,7 +2,6 @@ import {
Box,
Button,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
......@@ -69,7 +68,6 @@ type Checkboxes = 'notification' |
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => {
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
let notificationsDefault = {} as Inputs['notification_settings'];
if (!data?.notification_settings) {
......@@ -142,15 +140,15 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
return (
<AddressInput<Inputs, 'address'>
field={ field }
backgroundColor={ formBackgroundColor }
bgColor="dialog_bg"
error={ errors.address }
/>
);
}, [ errors, formBackgroundColor ]);
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]);
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors ]);
// eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
......
......@@ -10135,15 +10135,10 @@ focus-visible@^5.2.0:
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.4:
version "1.15.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
follow-redirects@^1.15.0, follow-redirects@^1.15.4:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
fontfaceobserver@2.1.0:
version "2.1.0"
......@@ -15001,7 +14996,16 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
......@@ -15136,7 +15140,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
......@@ -15822,9 +15833,9 @@ undici-types@~5.26.4:
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^5.24.0:
version "5.26.3"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.3.tgz#ab3527b3d5bb25b12f898dfd22165d472dd71b79"
integrity sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==
version "5.28.4"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
dependencies:
"@fastify/busboy" "^2.0.0"
......@@ -16300,7 +16311,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
......@@ -16318,6 +16329,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment