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();
});
}); });
}); });
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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