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: ...@@ -55,7 +55,10 @@ jobs:
label_description: Tasks in pre-release right now label_description: Tasks in pre-release right now
secrets: inherit secrets: inherit
# Temporary disable this step because it is broken
# There is an issue with building web3modal deps
upload_source_maps: upload_source_maps:
name: Upload source maps to Sentry name: Upload source maps to Sentry
if: false
uses: './.github/workflows/upload-source-maps.yml' uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit secrets: inherit
...@@ -28,7 +28,7 @@ jobs: ...@@ -28,7 +28,7 @@ jobs:
issues: "[${{ github.event.issue.number }}]" issues: "[${{ github.event.issue.number }}]"
secrets: inherit secrets: inherit
review_requested_issues: pr_linked_issues:
name: Get issues linked to PR name: Get issues linked to PR
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.pull_request && github.event.action == 'review_requested' }} if: ${{ github.event.pull_request && github.event.action == 'review_requested' }}
...@@ -76,14 +76,24 @@ jobs: ...@@ -76,14 +76,24 @@ jobs:
return issues; return issues;
review_requested_tasks: issues_in_review:
name: Update status for issues in review name: Update status for issues in review
needs: [ review_requested_issues ] needs: [ pr_linked_issues ]
if: ${{ needs.review_requested_issues.outputs.issues }} if: ${{ needs.pr_linked_issues.outputs.issues }}
uses: './.github/workflows/update-project-cards.yml' uses: './.github/workflows/update-project-cards.yml'
secrets: inherit secrets: inherit
with: with:
project_name: ${{ vars.PROJECT_NAME }} project_name: ${{ vars.PROJECT_NAME }}
field_name: Status field_name: Status
field_value: Review 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: ...@@ -81,8 +81,11 @@ jobs:
with: with:
platforms: linux/amd64,linux/arm64/v8 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: upload_source_maps:
name: Upload source maps to Sentry name: Upload source maps to Sentry
if: false
needs: publish_image needs: publish_image
uses: './.github/workflows/upload-source-maps.yml' uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit secrets: inherit
...@@ -2,6 +2,9 @@ import type { ContractCodeIde } from 'types/client/contract'; ...@@ -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 { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items';
import type { ChainIndicatorId } from 'types/homepage'; import type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks'; 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 * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils'; import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
...@@ -21,6 +24,11 @@ const hiddenLinks = (() => { ...@@ -21,6 +24,11 @@ const hiddenLinks = (() => {
return result; 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 // 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)'; 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({ ...@@ -70,6 +78,9 @@ const UI = Object.freeze({
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [], items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
}, },
hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false, hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false,
colorTheme: {
'default': defaultColorTheme,
},
}); });
export default UI; export default UI;
...@@ -46,6 +46,7 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout ...@@ -46,6 +46,7 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global 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_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_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json 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 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=/ ...@@ -24,8 +24,8 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
## homepage ## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(51, 53, 67, 1)' 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_TEXT_COLOR=rgba(165,252,122,1)
## sidebar ## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json 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 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 ...@@ -51,5 +51,6 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004 NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 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_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
\ No newline at end of file
...@@ -28,6 +28,7 @@ import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; ...@@ -28,6 +28,7 @@ import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage'; import type { ChainIndicatorId } from '../../../types/homepage';
import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; 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 type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address'; import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
...@@ -567,6 +568,7 @@ const schema = yup ...@@ -567,6 +568,7 @@ const schema = yup
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
// 5. Features configuration // 5. Features configuration
NEXT_PUBLIC_API_SPEC_URL: yup.string().test(urlTest), NEXT_PUBLIC_API_SPEC_URL: yup.string().test(urlTest),
......
...@@ -21,6 +21,7 @@ NEXT_PUBLIC_APP_PORT=3000 ...@@ -21,6 +21,7 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_PROTOCOL=http 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_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_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_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_CONTRACT_INFO_API_HOST=https://example.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
......
...@@ -275,6 +275,7 @@ Settings for meta tags, OG tags and SEO ...@@ -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_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_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_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 #### Network explorer configuration properties
......
...@@ -16,7 +16,7 @@ const PAGE_PROPS = { ...@@ -16,7 +16,7 @@ const PAGE_PROPS = {
cookies: '', cookies: '',
referrer: '', referrer: '',
query: {}, query: {},
adBannerProvider: undefined, adBannerProvider: null,
apiData: null, apiData: null,
}; };
......
...@@ -21,6 +21,10 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr ...@@ -21,6 +21,10 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
'tooltipTitle', 'tooltipTitle',
'tooltipDescription', 'tooltipDescription',
'tooltipUrl', 'tooltipUrl',
'appID',
'appMarketplaceURL',
'appLogoURL',
'appActionButtonText',
]; ];
for (const stringField of stringFields) { for (const stringField of stringFields) {
......
...@@ -12,7 +12,7 @@ const AppContext = createContext<PageProps>({ ...@@ -12,7 +12,7 @@ const AppContext = createContext<PageProps>({
cookies: '', cookies: '',
referrer: '', referrer: '',
query: {}, query: {},
adBannerProvider: undefined, adBannerProvider: null,
apiData: null, apiData: null,
}); });
......
...@@ -6,11 +6,13 @@ import { ...@@ -6,11 +6,13 @@ import {
import type { ChakraProviderProps } from '@chakra-ui/react'; import type { ChakraProviderProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import theme from 'theme';
interface Props extends ChakraProviderProps { interface Props extends ChakraProviderProps {
cookies?: string; cookies?: string;
} }
export function ChakraProvider({ cookies, theme, children }: Props) { export function ChakraProvider({ cookies, children }: Props) {
const colorModeManager = const colorModeManager =
typeof cookies === 'string' ? typeof cookies === 'string' ?
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) : cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
......
...@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; ...@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures { export interface GrowthBookFeatures {
test_value: string; test_value: string;
action_button_exp: boolean;
} }
export const growthBook = (() => { export const growthBook = (() => {
......
...@@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? ( ...@@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? (
} | { } | {
'Type': 'Security score'; 'Type': 'Security score';
'Source': 'Analyzed contracts popup'; 'Source': 'Analyzed contracts popup';
} | {
'Type': 'Action button';
'Info': string;
'Source': 'Txn' | 'NFT collection' | 'NFT item';
} | { } | {
'Type': 'Address tag'; 'Type': 'Address tag';
'Info': string; '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'; 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 }> = [ export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [
{ {
label: 'GitHub', label: 'GitHub',
......
...@@ -19,8 +19,12 @@ export function middleware(req: NextRequest) { ...@@ -19,8 +19,12 @@ export function middleware(req: NextRequest) {
return accountResponse; return accountResponse;
} }
const end = Date.now();
const res = NextResponse.next(); const res = NextResponse.next();
middlewares.colorTheme(req, res);
const end = Date.now();
res.headers.append('Content-Security-Policy', cspPolicy); res.headers.append('Content-Security-Policy', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`); res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
res.headers.append('Docker-ID', process.env.HOSTNAME || ''); res.headers.append('Docker-ID', process.env.HOSTNAME || '');
......
...@@ -61,3 +61,18 @@ export const protocolTag: AddressMetadataTagApi = { ...@@ -61,3 +61,18 @@ export const protocolTag: AddressMetadataTagApi = {
ordinal: 0, ordinal: 0,
meta: null, 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 { ...@@ -56,6 +56,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
getFeaturePayload(config.features.addressVerification)?.api.endpoint, getFeaturePayload(config.features.addressVerification)?.api.endpoint,
getFeaturePayload(config.features.nameService)?.api.endpoint, getFeaturePayload(config.features.nameService)?.api.endpoint,
getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
marketplaceFeaturePayload && 'api' in marketplaceFeaturePayload ? marketplaceFeaturePayload.api.endpoint : '', marketplaceFeaturePayload && 'api' in marketplaceFeaturePayload ? marketplaceFeaturePayload.api.endpoint : '',
// chain RPC server // chain RPC server
......
...@@ -14,7 +14,7 @@ export interface Props<Pathname extends Route['pathname'] = never> { ...@@ -14,7 +14,7 @@ export interface Props<Pathname extends Route['pathname'] = never> {
query: Route['query']; query: Route['query'];
cookies: string; cookies: string;
referrer: string; referrer: string;
adBannerProvider: AdBannerProviders | undefined; adBannerProvider: AdBannerProviders | null;
// if apiData is undefined, Next.js will complain that it is not serializable // 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 // so we force it to be always present in the props but it can be null
apiData: metadata.ApiData<Pathname> | null; apiData: metadata.ApiData<Pathname> | null;
...@@ -32,7 +32,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => { ...@@ -32,7 +32,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
return adBannerFeature.provider; return adBannerFeature.provider;
} }
} }
return; return null;
})(); })();
return { return {
...@@ -40,7 +40,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => { ...@@ -40,7 +40,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
query, query,
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
adBannerProvider, adBannerProvider: adBannerProvider,
apiData: null, 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 { account } from './account';
export { default as colorTheme } from './colorTheme';
...@@ -18,7 +18,6 @@ import { growthBook } from 'lib/growthbook/init'; ...@@ -18,7 +18,6 @@ import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout'; import Layout from 'ui/shared/layout/Layout';
...@@ -57,7 +56,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -57,7 +56,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>); const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
return ( return (
<ChakraProvider theme={ theme } cookies={ pageProps.cookies }> <ChakraProvider cookies={ pageProps.cookies }>
<AppErrorBoundary <AppErrorBoundary
{ ...ERROR_SCREEN_STYLES } { ...ERROR_SCREEN_STYLES }
onError={ handleError } onError={ handleError }
......
...@@ -60,4 +60,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -60,4 +60,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
noWalletClient: [ noWalletClient: [
[ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ], [ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ],
], ],
noNftMarketplaces: [
[ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ],
],
}; };
import { formAnatomy as parts } from '@chakra-ui/anatomy'; import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; 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 FancySelect from './FancySelect';
import FormLabel from './FormLabel'; import FormLabel from './FormLabel';
import Input from './Input'; import Input from './Input';
...@@ -13,8 +12,7 @@ const { definePartsStyle, defineMultiStyleConfig } = ...@@ -13,8 +12,7 @@ const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) { function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const { theme } = props; const formStyles = getFormStyles(props);
const { focusPlaceholderColor, errorColor } = getDefaultFormColors(props);
const activeLabelStyles = { const activeLabelStyles = {
...FormLabel.variants?.floating?.(props)._focusWithin, ...FormLabel.variants?.floating?.(props)._focusWithin,
...@@ -63,12 +61,29 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction ...@@ -63,12 +61,29 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
// label styles // label styles
label: FormLabel.sizes?.[size](props) || {}, label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles, '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, input[aria-invalid=true] + label,
textarea[aria-invalid=true] + label, textarea[aria-invalid=true] + label,
&[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 // input styles
...@@ -79,31 +94,24 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction ...@@ -79,31 +94,24 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
padding: inputPx, padding: inputPx,
}, },
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles, '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 // indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { '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, input[aria-invalid=true] + label .chakra-form__required-indicator,
textarea[aria-invalid=true] + label .chakra-form__required-indicator, textarea[aria-invalid=true] + label .chakra-form__required-indicator,
&[aria-invalid=true] .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', () => { ...@@ -65,6 +65,19 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot(); 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 }) => { test('filled error', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
......
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; 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({ const baseStyle = defineStyle({
display: 'flex', display: 'flex',
...@@ -13,14 +12,12 @@ const baseStyle = defineStyle({ ...@@ -13,14 +12,12 @@ const baseStyle = defineStyle({
transitionDuration: 'normal', transitionDuration: 'normal',
opacity: 1, opacity: 1,
_disabled: { _disabled: {
opacity: 0.4, opacity: 0.2,
}, },
}); });
const variantFloating = defineStyle((props) => { const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props; const formStyles = getFormStyles(props);
const { focusPlaceholderColor } = getDefaultFormColors(props);
const bc = backgroundColor || 'transparent';
return { return {
left: '2px', left: '2px',
...@@ -29,8 +26,8 @@ const variantFloating = defineStyle((props) => { ...@@ -29,8 +26,8 @@ const variantFloating = defineStyle((props) => {
position: 'absolute', position: 'absolute',
borderRadius: 'base', borderRadius: 'base',
boxSizing: 'border-box', boxSizing: 'border-box',
color: 'gray.500', color: formStyles.placeholder.default.color,
backgroundColor: 'transparent', backgroundColor: props.bgColor || props.backgroundColor || 'transparent',
pointerEvents: 'none', pointerEvents: 'none',
margin: 0, margin: 0,
transformOrigin: 'top left', transformOrigin: 'top left',
...@@ -39,8 +36,8 @@ const variantFloating = defineStyle((props) => { ...@@ -39,8 +36,8 @@ const variantFloating = defineStyle((props) => {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
_focusWithin: { _focusWithin: {
backgroundColor: bc, backgroundColor: props.bgColor || props.backgroundColor || 'transparent',
color: getColor(theme, focusPlaceholderColor), color: formStyles.placeholder.default.color,
fontSize: 'xs', fontSize: 'xs',
lineHeight: '16px', lineHeight: '16px',
borderTopRightRadius: 'none', borderTopRightRadius: 'none',
...@@ -70,7 +67,7 @@ const sizes = { ...@@ -70,7 +67,7 @@ const sizes = {
return { return {
fontSize: 'md', fontSize: 'md',
lineHeight: '24px', lineHeight: '24px',
padding: '28px 4px 28px 24px', padding: '26px 4px 26px 24px',
right: '22px', right: '22px',
_focusWithin: { _focusWithin: {
padding: '16px 0 2px 24px', padding: '16px 0 2px 24px',
......
...@@ -10,11 +10,11 @@ import { runIfFn } from '@chakra-ui/utils'; ...@@ -10,11 +10,11 @@ import { runIfFn } from '@chakra-ui/utils';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const baseStyleDialog = defineStyle((props) => { const baseStyleDialog = defineStyle(() => {
return { return {
padding: 8, padding: 8,
borderRadius: 'lg', borderRadius: 'lg',
bg: mode('white', 'gray.900')(props), bg: 'dialog_bg',
margin: 'auto', margin: 'auto',
}; };
}); });
...@@ -61,7 +61,7 @@ const baseStyleOverlay = defineStyle({ ...@@ -61,7 +61,7 @@ const baseStyleOverlay = defineStyle({
}); });
const baseStyle = definePartsStyle((props) => ({ const baseStyle = definePartsStyle((props) => ({
dialog: runIfFn(baseStyleDialog, props), dialog: runIfFn(baseStyleDialog),
dialogContainer: baseStyleDialogContainer, dialogContainer: baseStyleDialogContainer,
header: runIfFn(baseStyleHeader, props), header: runIfFn(baseStyleHeader, props),
......
import { Textarea as TextareaComponent } from '@chakra-ui/react'; import { Textarea as TextareaComponent } from '@chakra-ui/react';
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles'; 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 = { const sizes = {
md: defineStyle({ md: defineStyle({
fontSize: 'md', fontSize: 'md',
...@@ -38,7 +24,6 @@ const Textarea = defineStyleConfig({ ...@@ -38,7 +24,6 @@ const Textarea = defineStyleConfig({
sizes, sizes,
variants: { variants: {
outline: defineStyle(getOutlinedFieldStyles), outline: defineStyle(getOutlinedFieldStyles),
filledInactive: variantFilledInactive,
}, },
defaultProps: { defaultProps: {
variant: 'outline', variant: 'outline',
......
import { type ThemeConfig } from '@chakra-ui/react'; import { type ThemeConfig } from '@chakra-ui/react';
import appConfig from 'configs/app';
const config: ThemeConfig = { const config: ThemeConfig = {
initialColorMode: 'system', initialColorMode: appConfig.UI.colorTheme.default?.colorMode ?? 'system',
useSystemColorMode: false, useSystemColorMode: false,
disableTransitionOnChange: false, disableTransitionOnChange: false,
}; };
......
...@@ -23,6 +23,10 @@ const semanticTokens = { ...@@ -23,6 +23,10 @@ const semanticTokens = {
'default': 'red.400', 'default': 'red.400',
_dark: 'red.300', _dark: 'red.300',
}, },
dialog_bg: {
'default': 'white',
_dark: 'gray.900',
},
}, },
shadows: { shadows: {
action_bar: '0 4px 4px -4px rgb(0 0 0 / 10%), 0 2px 4px -4px rgb(0 0 0 / 6%)', 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 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 getDefaultTransitionProps from './getDefaultTransitionProps';
import getFormStyles from './getFormStyles';
export default function getOutlinedFieldStyles(props: StyleFunctionProps) { export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
const { theme, borderColor } = props; const formStyles = getFormStyles(props);
const { focusBorderColor, errorColor } = getDefaultFormColors(props);
const transitionProps = getDefaultTransitionProps(); const transitionProps = getDefaultTransitionProps();
return { return {
border: '2px solid', border: '2px solid',
// filled input // filled input
backgroundColor: 'transparent', ...formStyles.input.filled,
borderColor: mode('gray.300', 'gray.600')(props),
...transitionProps, ...transitionProps,
_hover: { _hover: {
borderColor: mode('gray.200', 'gray.500')(props), ...formStyles.input.hover,
}, },
_readOnly: { _readOnly: {
boxShadow: 'none !important', boxShadow: 'none !important',
userSelect: 'all', userSelect: 'all',
pointerEvents: 'none',
...formStyles.input.readOnly,
_hover: {
...formStyles.input.readOnly,
},
_focus: {
...formStyles.input.readOnly,
},
}, },
_disabled: { _disabled: {
opacity: 1, ...formStyles.input.disabled,
backgroundColor: mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderColor: 'transparent',
cursor: 'not-allowed', cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
':-webkit-autofill': { ':-webkit-autofill': {
// background color for disabled input which value was selected from browser autocomplete popup // 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`, '-webkit-box-shadow': `0 0 0px 1000px ${ mode('rgba(16, 17, 18, 0.08)', 'rgba(255, 255, 255, 0.08)')(props) } inset`,
}, },
}, },
_invalid: { _invalid: {
borderColor: getColor(theme, errorColor), ...formStyles.input.error,
boxShadow: `none`, boxShadow: `none`,
_placeholder: {
color: formStyles.placeholder.error.color,
},
}, },
_focusVisible: { _focusVisible: {
...formStyles.input.focus,
zIndex: 1, zIndex: 1,
borderColor: getColor(theme, focusBorderColor),
boxShadow: 'md', boxShadow: 'md',
}, },
_placeholder: { _placeholder: {
color: mode('blackAlpha.600', 'whiteAlpha.600')(props), color: formStyles.placeholder.default.color,
}, },
// not filled input // 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 filled input with type="date"
':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true])': { ':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true]):not([aria-readonly=true])': {
borderColor: borderColor || mode('gray.100', 'gray.700')(props), ...formStyles.input.empty,
color: 'gray.500',
}, },
':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' }, ':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
......
...@@ -26,6 +26,10 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'> ...@@ -26,6 +26,10 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
tooltipTitle?: string; tooltipTitle?: string;
tooltipDescription?: string; tooltipDescription?: string;
tooltipUrl?: string; tooltipUrl?: string;
appID?: string;
appMarketplaceURL?: string;
appLogoURL?: string;
appActionButtonText?: string;
} | null; } | 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) => { ...@@ -128,7 +128,6 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
size="xs" size="xs"
value={ sourceType } value={ sourceType }
onChange={ handleSelectChange } onChange={ handleSelectChange }
focusBorderColor="none"
w="auto" w="auto"
fontWeight={ 600 } fontWeight={ 600 }
borderRadius="base" borderRadius="base"
......
...@@ -24,7 +24,6 @@ interface Props { ...@@ -24,7 +24,6 @@ interface Props {
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => { const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0; const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
...@@ -39,7 +38,7 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onI ...@@ -39,7 +38,7 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onI
placeholder="Search by token name" placeholder="Search by token name"
ml="1px" ml="1px"
onChange={ onInputChange } onChange={ onInputChange }
borderColor={ inputBorderColor } bgColor="dialog_bg"
/> />
</InputGroup> </InputGroup>
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
......
...@@ -31,6 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P ...@@ -31,6 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
animationUrl={ tokenInstance?.animation_url ?? null } animationUrl={ tokenInstance?.animation_url ?? null }
imageUrl={ tokenInstance?.image_url ?? null } imageUrl={ tokenInstance?.image_url ?? null }
isLoading={ isLoading } isLoading={ isLoading }
autoplayVideo={ false }
/> />
</Link> </Link>
<Flex justifyContent="space-between" w="100%" flexWrap="wrap"> <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 React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
...@@ -15,13 +15,11 @@ interface Props { ...@@ -15,13 +15,11 @@ interface Props {
} }
const AddressVerificationFieldAddress = ({ formState, control }: Props) => { const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined; const error = 'address' in formState.errors ? formState.errors.address : undefined;
return ( 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 <Input
{ ...field } { ...field }
required required
...@@ -29,11 +27,12 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => { ...@@ -29,11 +27,12 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/> <InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting, backgroundColor ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<Controller <Controller
......
import { FormControl, Textarea, useColorModeValue } from '@chakra-ui/react'; import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
...@@ -15,25 +15,24 @@ interface Props { ...@@ -15,25 +15,24 @@ interface Props {
} }
const AddressVerificationFieldMessage = ({ formState, control }: Props) => { const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined; const error = 'message' in formState.errors ? formState.errors.message : undefined;
return ( 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 <Textarea
{ ...field } { ...field }
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
isDisabled isReadOnly
autoComplete="off" autoComplete="off"
maxH={{ base: '140px', lg: '80px' }} maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Message to sign" error={ error } isInModal/> <InputPlaceholder text="Message to sign" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, backgroundColor ]); }, [ formState.errors ]);
return ( return (
<Controller <Controller
......
import { FormControl, Input, useColorModeValue } from '@chakra-ui/react'; import { FormControl, Input } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form'; import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
...@@ -16,24 +16,23 @@ interface Props { ...@@ -16,24 +16,23 @@ interface Props {
} }
const AddressVerificationFieldSignature = ({ formState, control }: Props) => { const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined; const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return ( 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 <Input
{ ...field } { ...field }
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Signature hash" error={ error }/> <InputPlaceholder text="Signature hash" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting, backgroundColor ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<Controller <Controller
......
...@@ -105,7 +105,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -105,7 +105,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> } { rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/> <AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}> <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 Continue
</Button> </Button>
<AdminSupportText/> <AdminSupportText/>
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -42,7 +41,6 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -42,7 +41,6 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const updateApiKey = (data: Inputs) => { const updateApiKey = (data: Inputs) => {
const body = { name: data.name }; const body = { name: data.name };
...@@ -102,25 +100,27 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -102,25 +100,27 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<FormControl variant="floating" id="address"> <FormControl variant="floating" id="address">
<Input <Input
{ ...field } { ...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> </FormControl>
); );
}, []); }, []);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => { const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return ( return (
<FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }> <FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(errors.name) } isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH } maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/> <InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/>
</FormControl> </FormControl>
); );
}, [ errors, formBackgroundColor ]); }, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
...@@ -108,7 +108,6 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -108,7 +108,6 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
borderRadius="base" borderRadius="base"
value={ format } value={ format }
onChange={ handleSelectChange } onChange={ handleSelectChange }
focusBorderColor="none"
w="auto" w="auto"
> >
{ formats.map((format) => ( { formats.map((format) => (
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
FormControl, FormControl,
Input, Input,
Textarea, Textarea,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -61,8 +60,6 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -61,8 +60,6 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
}; };
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const mutation = useMutation({ const mutation = useMutation({
mutationFn: customAbiKey, mutationFn: customAbiKey,
onSuccess: (data) => { onSuccess: (data) => {
...@@ -109,38 +106,40 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -109,38 +106,40 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<AddressInput<Inputs, 'contract_address_hash'> <AddressInput<Inputs, 'contract_address_hash'>
field={ field } field={ field }
error={ errors.contract_address_hash } error={ errors.contract_address_hash }
backgroundColor={ formBackgroundColor } bgColor="dialog_bg"
placeholder="Smart contract address (0x...)" placeholder="Smart contract address (0x...)"
/> />
); );
}, [ errors, formBackgroundColor ]); }, [ errors ]);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => { const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return ( return (
<FormControl variant="floating" id="name" isRequired backgroundColor={ formBackgroundColor }> <FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(errors.name) } isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH } maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Project name" error={ errors.name }/> <InputPlaceholder text="Project name" error={ errors.name }/>
</FormControl> </FormControl>
); );
}, [ errors, formBackgroundColor ]); }, [ errors ]);
const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => { const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => {
return ( return (
<FormControl variant="floating" id="abi" isRequired backgroundColor={ formBackgroundColor }> <FormControl variant="floating" id="abi" isRequired bgColor="dialog_bg">
<Textarea <Textarea
{ ...field } { ...field }
size="lg" size="lg"
minH="300px" minH="300px"
isInvalid={ Boolean(errors.abi) } isInvalid={ Boolean(errors.abi) }
bgColor="dialog_bg"
/> />
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/> <InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/>
</FormControl> </FormControl>
); );
}, [ errors, formBackgroundColor ]); }, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
...@@ -2,7 +2,7 @@ import { Box, Center, useColorMode, Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box, Center, useColorMode, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe'; import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe';
import { useRouter } from 'next/router'; 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'; import type { MarketplaceAppOverview } from 'types/client/marketplace';
...@@ -16,6 +16,7 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; ...@@ -16,6 +16,7 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar'; import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar';
...@@ -36,9 +37,10 @@ type Props = { ...@@ -36,9 +37,10 @@ type Props = {
address: string | undefined; address: string | undefined;
data: MarketplaceAppOverview | undefined; data: MarketplaceAppOverview | undefined;
isPending: boolean; isPending: boolean;
appUrl?: string;
}; };
const MarketplaceAppContent = ({ address, data, isPending }: Props) => { const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
const { iframeRef, isReady } = useDappscoutIframe(); const { iframeRef, isReady } = useDappscoutIframe();
const [ iframeKey, setIframeKey ] = useState(0); const [ iframeKey, setIframeKey ] = useState(0);
...@@ -89,7 +91,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => { ...@@ -89,7 +91,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => {
h="100%" h="100%"
w="100%" w="100%"
display={ isFrameLoading ? 'none' : 'block' } display={ isFrameLoading ? 'none' : 'block' }
src={ data.url } src={ appUrl }
title={ data.title } title={ data.title }
onLoad={ handleIframeLoad } onLoad={ handleIframeLoad }
/> />
...@@ -132,6 +134,26 @@ const MarketplaceApp = () => { ...@@ -132,6 +134,26 @@ const MarketplaceApp = () => {
const { data, isPending } = query; const { data, isPending } = query;
const { setIsAutoConnectDisabled } = useMarketplaceContext(); 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(() => { useEffect(() => {
if (data) { if (data) {
metadata.update( metadata.update(
...@@ -153,13 +175,13 @@ const MarketplaceApp = () => { ...@@ -153,13 +175,13 @@ const MarketplaceApp = () => {
/> />
<DappscoutIframeProvider <DappscoutIframeProvider
address={ address } address={ address }
appUrl={ data?.url } appUrl={ appUrl }
rpcUrl={ config.chain.rpcUrl } rpcUrl={ config.chain.rpcUrl }
sendTransaction={ sendTransaction } sendTransaction={ sendTransaction }
signMessage={ signMessage } signMessage={ signMessage }
signTypedData={ signTypedData } signTypedData={ signTypedData }
> >
<MarketplaceAppContent address={ address } data={ data } isPending={ isPending }/> <MarketplaceAppContent address={ address } data={ data } isPending={ isPending } appUrl={ appUrl }/>
</DappscoutIframeProvider> </DappscoutIframeProvider>
</Flex> </Flex>
); );
......
...@@ -27,7 +27,7 @@ const MyProfile = () => { ...@@ -27,7 +27,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
required required
disabled readOnly
value={ data.name || '' } value={ data.name || '' }
/> />
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
...@@ -35,7 +35,7 @@ const MyProfile = () => { ...@@ -35,7 +35,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="nickname" isRequired size="lg"> <FormControl variant="floating" id="nickname" isRequired size="lg">
<Input <Input
required required
disabled readOnly
value={ data.nickname || '' } value={ data.nickname || '' }
/> />
<FormLabel>Nickname</FormLabel> <FormLabel>Nickname</FormLabel>
...@@ -43,7 +43,7 @@ const MyProfile = () => { ...@@ -43,7 +43,7 @@ const MyProfile = () => {
<FormControl variant="floating" id="email" isRequired size="lg"> <FormControl variant="floating" id="email" isRequired size="lg">
<Input <Input
required required
disabled readOnly
value={ data.email || '' } value={ data.email || '' }
/> />
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
......
import { import {
Box, Box,
Button, Button,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -42,8 +41,6 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl ...@@ -42,8 +41,6 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
}, },
}); });
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { mutate } = useMutation({ const { mutate } = useMutation({
mutationFn: (formData: Inputs) => { mutationFn: (formData: Inputs) => {
const body = { const body = {
...@@ -87,12 +84,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl ...@@ -87,12 +84,12 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
}; };
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } backgroundColor={ formBackgroundColor }/>; return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } bgColor="dialog_bg"/>;
}, [ errors, formBackgroundColor ]); }, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors, formBackgroundColor ]); }, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
import { import {
Box, Box,
Button, Button,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -34,7 +33,6 @@ type Inputs = { ...@@ -34,7 +33,6 @@ type Inputs = {
const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => { const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
...@@ -90,12 +88,12 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi ...@@ -90,12 +88,12 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
}; };
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => { const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } error={ errors.transaction } backgroundColor={ formBackgroundColor }/>; return <TransactionInput field={ field } error={ errors.transaction } bgColor="dialog_bg"/>;
}, [ errors, formBackgroundColor ]); }, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors, formBackgroundColor ]); }, [ errors ]);
return ( return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
......
...@@ -13,7 +13,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { ...@@ -13,7 +13,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>; field: ControllerRenderProps<TInputs, TInputName>;
size?: InputProps['size']; size?: InputProps['size'];
placeholder?: string; placeholder?: string;
backgroundColor?: string; bgColor?: string;
error?: FieldError; error?: FieldError;
} }
...@@ -23,14 +23,15 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa ...@@ -23,14 +23,15 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
field, field,
size, size,
placeholder = 'Address (0x...)', placeholder = 'Address (0x...)',
backgroundColor, bgColor,
}: Props<Inputs, Name>) { }: Props<Inputs, Name>) {
return ( return (
<FormControl variant="floating" id="address" isRequired backgroundColor={ backgroundColor } size={ size }> <FormControl variant="floating" id="address" isRequired size={ size } bgColor={ bgColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
bgColor={ bgColor }
/> />
<InputPlaceholder text={ placeholder } error={ error }/> <InputPlaceholder text={ placeholder } error={ error }/>
</FormControl> </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 = { ...@@ -92,5 +92,20 @@ const defaultProps = {
await expect(component).toHaveScreenshot(); 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'; ...@@ -3,6 +3,9 @@ import type { Size, ChakraStylesConfig } from 'chakra-react-select';
import type { Option } from './types'; import type { Option } from './types';
import theme from 'theme';
import getFormStyles from 'theme/utils/getFormStyles';
function getValueContainerStyles(size?: Size) { function getValueContainerStyles(size?: Size) {
switch (size) { switch (size) {
case 'sm': case 'sm':
...@@ -42,13 +45,12 @@ function getSingleValueStyles(size?: Size) { ...@@ -42,13 +45,12 @@ function getSingleValueStyles(size?: Size) {
} }
const getChakraStyles: (colorMode: ColorMode) => ChakraStylesConfig<Option> = (colorMode) => { const getChakraStyles: (colorMode: ColorMode) => ChakraStylesConfig<Option> = (colorMode) => {
const emptyInputBorderColor = colorMode === 'dark' ? 'gray.700' : 'gray.100'; const formColor = getFormStyles({ colorMode, colorScheme: 'blue', theme });
const filledInputBorderColor = colorMode === 'dark' ? 'gray.600' : 'gray.300';
return { return {
control: (provided, state) => ({ control: (provided, state) => ({
...provided, ...provided,
borderColor: state.hasValue ? filledInputBorderColor : emptyInputBorderColor, borderColor: state.hasValue ? formColor.input.filled.borderColor : formColor.input.empty.borderColor,
}), }),
inputContainer: (provided) => ({ inputContainer: (provided) => ({
...provided, ...provided,
......
...@@ -6,12 +6,10 @@ interface Props { ...@@ -6,12 +6,10 @@ interface Props {
text: string; text: string;
icon?: React.ReactNode; icon?: React.ReactNode;
error?: Partial<FieldError>; error?: Partial<FieldError>;
className?: string;
isFancy?: boolean; isFancy?: boolean;
isInModal?: boolean;
} }
const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }: Props) => { const InputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
let errorMessage = error?.message; let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') { if (!errorMessage && error?.type === 'pattern') {
...@@ -20,10 +18,10 @@ const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }: ...@@ -20,10 +18,10 @@ const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }:
return ( return (
<FormLabel <FormLabel
className={ className }
alignItems="center" alignItems="center"
{ ...(isFancy ? { 'data-fancy': true } : {}) } { ...(isFancy ? { 'data-fancy': true } : {}) }
{ ...(isInModal ? { 'data-in-modal': true } : {}) } variant="floating"
bgColor="deeppink"
> >
{ icon } { icon }
<chakra.span>{ text }</chakra.span> <chakra.span>{ text }</chakra.span>
...@@ -32,4 +30,4 @@ const InputPlaceholder = ({ text, icon, error, className, isFancy, isInModal }: ...@@ -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 { ...@@ -11,13 +11,29 @@ interface Props {
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
beforeSlot?: React.ReactNode; beforeSlot?: React.ReactNode;
textareaMaxHeight?: string; textareaMaxHeight?: string;
textareaMinHeight?: string;
showCopy?: boolean; showCopy?: boolean;
isLoading?: boolean; isLoading?: boolean;
contentProps?: ChakraProps; contentProps?: ChakraProps;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading, contentProps }: Props) => { const RawDataSnippet = ({
// see issue in theme/components/Textarea.ts 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'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
...@@ -33,7 +49,7 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare ...@@ -33,7 +49,7 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare
p={ 4 } p={ 4 }
bgColor={ isLoading ? 'inherit' : bgColor } bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' } maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined } minH={ textareaMinHeight || (isLoading ? '200px' : undefined) }
fontSize="sm" fontSize="sm"
borderRadius="md" borderRadius="md"
wordBreak="break-all" wordBreak="break-all"
......
import { Box, Flex, Select, Textarea } from '@chakra-ui/react'; import { Select } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import hexToUtf8 from 'lib/hexToUtf8'; import hexToUtf8 from 'lib/hexToUtf8';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
type DataType = 'Hex' | 'UTF-8' type DataType = 'Hex' | 'UTF-8'
const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ]; const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ];
...@@ -18,24 +18,20 @@ const RawInputData = ({ hex }: Props) => { ...@@ -18,24 +18,20 @@ const RawInputData = ({ hex }: Props) => {
setSelectedDataType(event.target.value as DataType); setSelectedDataType(event.target.value as DataType);
}, []); }, []);
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>
);
return ( return (
<Box w="100%"> <RawDataSnippet
<Flex justifyContent="space-between" alignItems="center"> data={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
<Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } focusBorderColor="none" w="auto"> rightSlot={ select }
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } textareaMaxHeight="220px"
</Select> textareaMinHeight="160px"
<CopyToClipboard text={ hex }/> w="100%"
</Flex> />
<Textarea
value={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
w="100%"
maxH="220px"
mt={ 2 }
p={ 4 }
variant="filledInactive"
fontSize="sm"
/>
</Box>
); );
}; };
......
...@@ -12,16 +12,17 @@ const TAG_MAX_LENGTH = 35; ...@@ -12,16 +12,17 @@ const TAG_MAX_LENGTH = 35;
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>; field: ControllerRenderProps<TInputs, TInputName>;
error?: FieldError; 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 ( return (
<FormControl variant="floating" id="tag" isRequired backgroundColor={ backgroundColor }> <FormControl variant="floating" id="tag" isRequired bgColor={ bgColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH } maxLength={ TAG_MAX_LENGTH }
bgColor={ bgColor }
/> />
<InputPlaceholder text="Private tag (max 35 characters)" error={ error }/> <InputPlaceholder text="Private tag (max 35 characters)" error={ error }/>
</FormControl> </FormControl>
......
...@@ -11,16 +11,17 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder'; ...@@ -11,16 +11,17 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<Field> = { type Props<Field> = {
field: Field; field: Field;
error?: FieldError; 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 ( return (
<FormControl variant="floating" id="transaction" isRequired backgroundColor={ backgroundColor }> <FormControl variant="floating" id="transaction" isRequired bgColor={ bgColor }>
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH } maxLength={ TRANSACTION_HASH_LENGTH }
bgColor={ bgColor }
/> />
<InputPlaceholder text="Transaction hash (0x...)" error={ error }/> <InputPlaceholder text="Transaction hash (0x...)" error={ error }/>
</FormControl> </FormControl>
......
...@@ -18,9 +18,10 @@ interface Props { ...@@ -18,9 +18,10 @@ interface Props {
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
withFullscreen?: 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 [ isMediaLoading, setIsMediaLoading ] = React.useState(true);
const [ isLoadingError, setIsLoadingError ] = React.useState(false); const [ isLoadingError, setIsLoadingError ] = React.useState(false);
...@@ -71,7 +72,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen ...@@ -71,7 +72,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen
switch (type) { switch (type) {
case 'video': case 'video':
return <NftVideo { ...props }/>; return <NftVideo { ...props } autoPlay={ autoplayVideo }/>;
case 'html': case 'html':
return <NftHtml { ...props }/>; return <NftHtml { ...props }/>;
case 'image': case 'image':
......
...@@ -5,15 +5,17 @@ import { mediaStyleProps, videoPlayProps } from './utils'; ...@@ -5,15 +5,17 @@ import { mediaStyleProps, videoPlayProps } from './utils';
interface Props { interface Props {
src: string; src: string;
autoPlay?: boolean;
onLoad: () => void; onLoad: () => void;
onError: () => void; onError: () => void;
onClick?: () => void; onClick?: () => void;
} }
const NftVideo = ({ src, onLoad, onError, onClick }: Props) => { const NftVideo = ({ src, autoPlay = true, onLoad, onError, onClick }: Props) => {
return ( return (
<chakra.video <chakra.video
{ ...videoPlayProps } { ...videoPlayProps }
autoPlay={ autoPlay }
src={ src } src={ src }
onCanPlayThrough={ onLoad } onCanPlayThrough={ onLoad }
onError={ onError } onError={ onError }
......
...@@ -18,6 +18,7 @@ const NftVideoFullscreen = ({ src, isOpen, onClose }: Props) => { ...@@ -18,6 +18,7 @@ const NftVideoFullscreen = ({ src, isOpen, onClose }: Props) => {
src={ src } src={ src }
maxH="90vh" maxH="90vh"
maxW="90vw" maxW="90vw"
autoPlay={ true }
/> />
</NftMediaFullscreenModal> </NftMediaFullscreenModal>
); );
......
...@@ -41,7 +41,6 @@ export const mediaStyleProps = { ...@@ -41,7 +41,6 @@ export const mediaStyleProps = {
}; };
export const videoPlayProps = { export const videoPlayProps = {
autoPlay: true,
disablePictureInPicture: true, disablePictureInPicture: true,
loop: true, loop: true,
muted: true, muted: true,
......
...@@ -46,7 +46,7 @@ const NetworkMenuContentMobile = ({ items, tabs }: Props) => { ...@@ -46,7 +46,7 @@ const NetworkMenuContentMobile = ({ items, tabs }: Props) => {
) : ( ) : (
<> <>
{ tabs.length > 1 && ( { 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>) } { tabs.map((tab) => <option key={ tab } value={ tab }>{ capitalize(tab) }</option>) }
</Select> </Select>
) } ) }
......
...@@ -2,9 +2,9 @@ import { Box, Flex, useColorMode } from '@chakra-ui/react'; ...@@ -2,9 +2,9 @@ import { Box, Flex, useColorMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import SettingsSample from './SettingsSample'; import SettingsSample from './SettingsSample';
import { COLOR_THEMES } from './utils';
const SettingsColorTheme = () => { const SettingsColorTheme = () => {
const { setColorMode } = useColorMode(); const { setColorMode } = useColorMode();
......
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import { IDENTICONS } from 'lib/settings/identIcon';
import SettingsSample from './SettingsSample'; import SettingsSample from './SettingsSample';
import { IDENTICONS } from './utils';
const SettingsIdentIcon = () => { const SettingsIdentIcon = () => {
const [ activeId, setActiveId ] = React.useState<string>(); const [ activeId, setActiveId ] = React.useState<string>();
......
...@@ -7,13 +7,17 @@ import { scroller } from 'react-scroll'; ...@@ -7,13 +7,17 @@ import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMounted from 'lib/hooks/useIsMounted'; import useIsMounted from 'lib/hooks/useIsMounted';
import { TOKEN_COUNTERS } from 'stubs/token'; import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/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 DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
...@@ -27,6 +31,8 @@ interface Props { ...@@ -27,6 +31,8 @@ interface Props {
const TokenDetails = ({ tokenQuery }: Props) => { const TokenDetails = ({ tokenQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
const hash = router.query.hash?.toString(); const hash = router.query.hash?.toString();
const tokenCountersQuery = useApiQuery('token_counters', { const tokenCountersQuery = useApiQuery('token_counters', {
...@@ -34,6 +40,8 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -34,6 +40,8 @@ const TokenDetails = ({ tokenQuery }: Props) => {
queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS }, queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
}); });
const appActionData = useAppActionData(hash, isActionButtonExperiment);
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => { const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
router.push( router.push(
{ pathname: '/token/[hash]', query: { hash: hash || '', tab } }, { pathname: '/token/[hash]', query: { hash: hash || '', tab } },
...@@ -167,7 +175,26 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -167,7 +175,26 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</DetailsInfoItem> </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 }/> <DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid> </Grid>
......
...@@ -23,6 +23,7 @@ const TokenInventoryItem = ({ item, token, isLoading }: Props) => { ...@@ -23,6 +23,7 @@ const TokenInventoryItem = ({ item, token, isLoading }: Props) => {
animationUrl={ item.animation_url } animationUrl={ item.animation_url }
imageUrl={ item.image_url } imageUrl={ item.image_url }
isLoading={ isLoading } isLoading={ isLoading }
autoplayVideo={ false }
/> />
); );
......
import { Image, Link, Skeleton, Tooltip } from '@chakra-ui/react'; import { Image, Link, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import config from 'configs/app'; import config from 'configs/app';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props { interface Props {
hash: string | undefined; hash: string | undefined;
id?: string; id?: string;
isLoading?: boolean; 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) { if (!hash || config.UI.views.nft.marketplaces.length === 0) {
return null; return null;
} }
...@@ -21,8 +28,9 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => { ...@@ -21,8 +28,9 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
hint="Marketplaces trading this NFT" hint="Marketplaces trading this NFT"
alignSelf="center" alignSelf="center"
isLoading={ isLoading } 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) => { { config.UI.views.nft.marketplaces.map((item) => {
const hrefTemplate = id ? item.instance_url : item.collection_url; const hrefTemplate = id ? item.instance_url : item.collection_url;
...@@ -41,6 +49,12 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => { ...@@ -41,6 +49,12 @@ const TokenNftMarketplaces = ({ hash, id, isLoading }: Props) => {
</Tooltip> </Tooltip>
); );
}) } }) }
{ (appActionData && isActionButtonExperiment) && (
<>
<TextSeparator color="gray.500" margin={ 0 }/>
<AppActionButton data={ appActionData } height="30px" source={ source }/>
</>
) }
</Skeleton> </Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
); );
......
...@@ -18,7 +18,7 @@ const TokenInfoFieldAddress = ({ control }: Props) => { ...@@ -18,7 +18,7 @@ const TokenInfoFieldAddress = ({ control }: Props) => {
<Input <Input
{ ...field } { ...field }
required required
isDisabled isReadOnly
/> />
<InputPlaceholder text="Token contract address"/> <InputPlaceholder text="Token contract address"/>
</FormControl> </FormControl>
......
...@@ -19,7 +19,8 @@ const TokenInfoFieldComment = ({ control, isReadOnly }: Props) => { ...@@ -19,7 +19,8 @@ const TokenInfoFieldComment = ({ control, isReadOnly }: Props) => {
<Textarea <Textarea
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
maxH="160px" maxH="160px"
maxLength={ 300 } maxLength={ 300 }
......
...@@ -20,7 +20,8 @@ const TokenInfoFieldDocs = ({ control, isReadOnly }: Props) => { ...@@ -20,7 +20,8 @@ const TokenInfoFieldDocs = ({ control, isReadOnly }: Props) => {
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Docs" error={ fieldState.error }/> <InputPlaceholder text="Docs" error={ fieldState.error }/>
......
...@@ -58,7 +58,8 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => { ...@@ -58,7 +58,8 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
{ ...field } { ...field }
onBlur={ handleBlur } onBlur={ handleBlur }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
required required
/> />
......
...@@ -22,7 +22,8 @@ const TokenInfoFieldPriceTicker = ({ control, isReadOnly, name, label }: Props) ...@@ -22,7 +22,8 @@ const TokenInfoFieldPriceTicker = ({ control, isReadOnly, name, label }: Props)
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text={ label } error={ fieldState.error }/> <InputPlaceholder text={ label } error={ fieldState.error }/>
......
...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectDescription = ({ control, isReadOnly }: Props) => { ...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectDescription = ({ control, isReadOnly }: Props) => {
{ ...field } { ...field }
required required
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
maxH="160px" maxH="160px"
maxLength={ 300 } maxLength={ 300 }
......
...@@ -21,7 +21,8 @@ const TokenInfoFieldProjectEmail = ({ control, isReadOnly }: Props) => { ...@@ -21,7 +21,8 @@ const TokenInfoFieldProjectEmail = ({ control, isReadOnly }: Props) => {
{ ...field } { ...field }
required required
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Official project email address" error={ fieldState.error }/> <InputPlaceholder text="Official project email address" error={ fieldState.error }/>
......
...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectName = ({ control, isReadOnly }: Props) => { ...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectName = ({ control, isReadOnly }: Props) => {
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Project name" error={ fieldState.error }/> <InputPlaceholder text="Project name" error={ fieldState.error }/>
......
...@@ -29,7 +29,8 @@ const TokenInfoFieldProjectSector = ({ control, isReadOnly, config }: Props) => ...@@ -29,7 +29,8 @@ const TokenInfoFieldProjectSector = ({ control, isReadOnly, config }: Props) =>
options={ options } options={ options }
size={ isMobile ? 'md' : 'lg' } size={ isMobile ? 'md' : 'lg' }
placeholder="Project industry" placeholder="Project industry"
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
error={ fieldState.error } error={ fieldState.error }
/> />
); );
......
...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectWebsite = ({ control, isReadOnly }: Props) => { ...@@ -20,7 +20,8 @@ const TokenInfoFieldProjectWebsite = ({ control, isReadOnly }: Props) => {
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
required required
/> />
......
...@@ -21,7 +21,8 @@ const TokenInfoFieldRequesterEmail = ({ control, isReadOnly }: Props) => { ...@@ -21,7 +21,8 @@ const TokenInfoFieldRequesterEmail = ({ control, isReadOnly }: Props) => {
{ ...field } { ...field }
required required
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Requester email" error={ fieldState.error }/> <InputPlaceholder text="Requester email" error={ fieldState.error }/>
......
...@@ -20,7 +20,8 @@ const TokenInfoFieldRequesterName = ({ control, isReadOnly }: Props) => { ...@@ -20,7 +20,8 @@ const TokenInfoFieldRequesterName = ({ control, isReadOnly }: Props) => {
{ ...field } { ...field }
required required
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Requester name" error={ fieldState.error }/> <InputPlaceholder text="Requester name" error={ fieldState.error }/>
......
...@@ -42,7 +42,8 @@ const TokenInfoFieldSocialLink = ({ control, isReadOnly, name }: Props) => { ...@@ -42,7 +42,8 @@ const TokenInfoFieldSocialLink = ({ control, isReadOnly, name }: Props) => {
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text={ SETTINGS[name].label } error={ fieldState.error }/> <InputPlaceholder text={ SETTINGS[name].label } error={ fieldState.error }/>
......
...@@ -22,7 +22,8 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => { ...@@ -22,7 +22,8 @@ const TokenInfoFieldSupport = ({ control, isReadOnly }: Props) => {
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
isDisabled={ formState.isSubmitting || isReadOnly } isDisabled={ formState.isSubmitting }
isReadOnly={ isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Support URL or email" error={ fieldState.error }/> <InputPlaceholder text="Support URL or email" error={ fieldState.error }/>
......
...@@ -18,7 +18,7 @@ const TokenInfoFieldTokenName = ({ control }: Props) => { ...@@ -18,7 +18,7 @@ const TokenInfoFieldTokenName = ({ control }: Props) => {
<Input <Input
{ ...field } { ...field }
required required
isDisabled isReadOnly
/> />
<InputPlaceholder text="Token name"/> <InputPlaceholder text="Token name"/>
</FormControl> </FormControl>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from '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 * as addressMock from 'mocks/address/address';
import { protocolTagWithMeta } from 'mocks/metadata/address';
import { tokenInfoERC721a } from 'mocks/tokens/tokenInfo'; import { tokenInfoERC721a } from 'mocks/tokens/tokenInfo';
import * as tokenInstanceMock from 'mocks/tokens/tokenInstance'; 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 TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import TokenInstanceDetails from './TokenInstanceDetails'; import TokenInstanceDetails from './TokenInstanceDetails';
const hash = tokenInfoERC721a.address; const hash = tokenInfoERC721a.address;
const API_URL_ADDRESS = buildApiUrl('address', { hash }); const addressMetadataQueryParams = {
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', { addresses: [ hash ],
id: tokenInstanceMock.unique.id, chainId: config.chain.id,
hash, 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 }) => { test('base view +@dark-mode +@mobile', async({ render, page }) => {
await page.route('http://localhost:3000/nft-marketplace-logo.png', (route) => route.fulfill({ const component = await render(
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(
<TestApp> <TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.unique } token={ tokenInfoERC721a }/> <TokenInstanceDetails data={ tokenInstanceMock.unique } token={ tokenInfoERC721a }/>
</TestApp>, </TestApp>,
...@@ -43,3 +53,40 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -43,3 +53,40 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => {
maskColor: configs.maskColor, 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'; ...@@ -3,6 +3,10 @@ import React from 'react';
import type { TokenInfo, TokenInstance } from 'types/api/token'; 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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
...@@ -24,6 +28,9 @@ interface Props { ...@@ -24,6 +28,9 @@ interface Props {
} }
const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: 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(() => { const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => { window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little // cannot do scroll instantly, have to wait a little
...@@ -71,7 +78,24 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => { ...@@ -71,7 +78,24 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
</Flex> </Flex>
</DetailsInfoItem> </DetailsInfoItem>
<TokenInstanceTransfersCount hash={ isLoading ? '' : token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/> <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> </Grid>
<NftMedia <NftMedia
animationUrl={ data.animation_url } animationUrl={ data.animation_url }
......
...@@ -39,7 +39,7 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => { ...@@ -39,7 +39,7 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
<Box> <Box>
<Flex alignItems="center" mb={ 6 }> <Flex alignItems="center" mb={ 6 }>
<chakra.span fontWeight={ 500 }>Metadata</chakra.span> <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="Table">Table</option>
<option value="JSON">JSON</option> <option value="JSON">JSON</option>
</Select> </Select>
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
import compareBns from 'lib/bigint/compareBns';
// import { apos } from 'lib/html-entities'; // import { apos } from 'lib/html-entities';
import { INTERNAL_TX } from 'stubs/internalTx'; import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -31,23 +32,20 @@ const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefine ...@@ -31,23 +32,20 @@ const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefine
const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => { const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) { switch (sort) {
case 'value-desc': { case 'value-desc': {
const result = a.value > b.value ? -1 : 1; return compareBns(b.value, a.value);
return a.value === b.value ? 0 : result;
} }
case 'value-asc': { case 'value-asc': {
const result = a.value > b.value ? 1 : -1; return compareBns(a.value, b.value);
return a.value === b.value ? 0 : result;
} }
case 'gas-limit-desc': { case 'gas-limit-desc': {
const result = a.gas_limit > b.gas_limit ? -1 : 1; return compareBns(b.gas_limit, a.gas_limit);
return a.gas_limit === b.gas_limit ? 0 : result;
} }
case 'gas-limit-asc': { case 'gas-limit-asc': {
const result = a.gas_limit > b.gas_limit ? 1 : -1; return compareBns(a.gas_limit, b.gas_limit);
return a.gas_limit === b.gas_limit ? 0 : result;
} }
default: default:
......
import React from 'react'; 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 * as txMock from 'mocks/txs/tx';
import { txInterpretation } from 'mocks/txs/txInterpretation'; import { txInterpretation } from 'mocks/txs/txInterpretation';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
...@@ -16,6 +20,25 @@ const txQuery = { ...@@ -16,6 +20,25 @@ const txQuery = {
isError: false, isError: false,
} as TxQuery; } 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 }) => { test('no interpretation +@mobile', async({ render }) => {
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>); const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -32,6 +55,16 @@ test.describe('blockscout provider', () => { ...@@ -32,6 +55,16 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot(); 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 }) => { test('with interpretation and view all link +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse( await mockApiResponse(
'tx_interpretation', 'tx_interpretation',
...@@ -42,13 +75,40 @@ test.describe('blockscout provider', () => { ...@@ -42,13 +75,40 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot(); 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 } }); await mockApiResponse('tx_interpretation', { data: { summaries: [] } }, { pathParams: { hash } });
const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>); const component = await render(<TxSubHeading hash={ hash } hasTag={ false } txQuery={ txQuery }/>);
await expect(component).toHaveScreenshot(); 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 = { const txPendingQuery = {
data: txMock.pending, data: txMock.pending,
isPlaceholderData: false, isPlaceholderData: false,
......
...@@ -3,9 +3,12 @@ import React from 'react'; ...@@ -3,9 +3,12 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; 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 { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
...@@ -26,6 +29,9 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -26,6 +29,9 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const hasInterpretationFeature = feature.isEnabled; const hasInterpretationFeature = feature.isEnabled;
const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves'; 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', { const txInterpretationQuery = useApiQuery('tx_interpretation', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { queryOptions: {
...@@ -42,16 +48,20 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -42,16 +48,20 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
}, },
}); });
const content = (() => { const hasNovesInterpretation = isNovesInterpretation &&
const hasNovesInterpretation = isNovesInterpretation &&
(novesInterpretationQuery.isPlaceholderData || Boolean(novesInterpretationQuery.data?.classificationData.description)); (novesInterpretationQuery.isPlaceholderData || Boolean(novesInterpretationQuery.data?.classificationData.description));
const hasInternalInterpretation = (hasInterpretationFeature && !isNovesInterpretation) && const hasInternalInterpretation = (hasInterpretationFeature && !isNovesInterpretation) &&
(txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length)); (txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length));
const hasViewAllInterpretationsLink = const hasViewAllInterpretationsLink =
!txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1; !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) { if (hasNovesInterpretation && novesInterpretationQuery.data) {
const novesSummary = createNovesSummaryObject(novesInterpretationQuery.data); const novesSummary = createNovesSummaryObject(novesInterpretationQuery.data);
...@@ -108,9 +118,18 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -108,9 +118,18 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
return ( return (
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%"> <Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
{ content } { content }
<Flex alignItems="center" justifyContent={{ base: 'start', lg: 'space-between' }} flexGrow={ 1 }> <Flex
{ !hasTag && <AccountActionsMenu mr={ 3 } mt={{ base: 3, lg: 0 }}/> } alignItems="center"
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }} mt={{ base: 3, lg: 0 }}/> 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> </Flex>
</Box> </Box>
); );
......
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
Box, Box,
Button, Button,
Text, Text,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -69,7 +68,6 @@ type Checkboxes = 'notification' | ...@@ -69,7 +68,6 @@ type Checkboxes = 'notification' |
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => { const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
let notificationsDefault = {} as Inputs['notification_settings']; let notificationsDefault = {} as Inputs['notification_settings'];
if (!data?.notification_settings) { if (!data?.notification_settings) {
...@@ -142,15 +140,15 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -142,15 +140,15 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
return ( return (
<AddressInput<Inputs, 'address'> <AddressInput<Inputs, 'address'>
field={ field } field={ field }
backgroundColor={ formBackgroundColor } bgColor="dialog_bg"
error={ errors.address } error={ errors.address }
/> />
); );
}, [ errors, formBackgroundColor ]); }, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } bgColor="dialog_bg"/>;
}, [ errors, formBackgroundColor ]); }, [ errors ]);
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => ( const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
......
...@@ -10135,15 +10135,10 @@ focus-visible@^5.2.0: ...@@ -10135,15 +10135,10 @@ focus-visible@^5.2.0:
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.15.0: follow-redirects@^1.15.0, follow-redirects@^1.15.4:
version "1.15.2" version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
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==
fontfaceobserver@2.1.0: fontfaceobserver@2.1.0:
version "2.1.0" version "2.1.0"
...@@ -15001,7 +14996,16 @@ string-template@~0.2.1: ...@@ -15001,7 +14996,16 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== 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" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
...@@ -15136,7 +15140,14 @@ string_decoder@~1.1.1: ...@@ -15136,7 +15140,14 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" 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" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
...@@ -15822,9 +15833,9 @@ undici-types@~5.26.4: ...@@ -15822,9 +15833,9 @@ undici-types@~5.26.4:
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^5.24.0: undici@^5.24.0:
version "5.26.3" version "5.28.4"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.3.tgz#ab3527b3d5bb25b12f898dfd22165d472dd71b79" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
integrity sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw== integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
dependencies: dependencies:
"@fastify/busboy" "^2.0.0" "@fastify/busboy" "^2.0.0"
...@@ -16300,7 +16311,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3: ...@@ -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" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== 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" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
...@@ -16318,6 +16329,15 @@ wrap-ansi@^6.2.0: ...@@ -16318,6 +16329,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.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: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 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