diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml index c5edbc8c50..ad7b75540f 100644 --- a/.github/workflows/deploy-review.yml +++ b/.github/workflows/deploy-review.yml @@ -2,6 +2,25 @@ name: Deploy review environment on: workflow_dispatch: + inputs: + envs_preset: + description: ENVs preset + required: false + default: "" + type: choice + options: + - none + - gnosis + - eth + - eth_sepolia + - eth_goerli + - optimism + - optimism_sepolia + - polygon + - rootstock + - stability + - zkevm + - zksync jobs: make_slug: @@ -23,6 +42,7 @@ jobs: uses: './.github/workflows/publish-image.yml' with: tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }} + build_args: ENVS_PRESET=${{ inputs.envs_preset }} secrets: inherit deploy_review: diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index ba0382b078..97c1102e8f 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -7,6 +7,10 @@ on: description: Image tags required: false type: string + build_args: + description: Build-time variables + required: false + type: string platforms: description: Image platforms (you can specify multiple platforms separated by comma) required: false @@ -18,6 +22,10 @@ on: description: Image tags required: false type: string + build_args: + description: Build-time variables + required: false + type: string platforms: description: Image platforms (you can specify multiple platforms separated by comma) required: false @@ -72,4 +80,5 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | GIT_COMMIT_SHA=${{ env.SHORT_SHA }} - GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }} \ No newline at end of file + GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }} + ${{ inputs.build_args }} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ad9f96bd7f..5e4a6ab56c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -338,17 +338,18 @@ "options": [ "main", "main.L2", + "localhost", + "gnosis", "eth", "eth_goerli", - "sepolia", + "eth_sepolia", + "optimism", + "optimism_sepolia", "polygon", - "zkevm", - "zksync", - "gnosis", "rootstock", "stability", - "poa_core", - "localhost", + "zkevm", + "zksync", ], "default": "main" }, diff --git a/Dockerfile b/Dockerfile index 5448b274ea..bfe83d3a2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -119,6 +119,11 @@ RUN ["chmod", "-R", "777", "./public"] COPY --from=builder /app/.env.registry . COPY --from=builder /app/.env . +# Copy ENVs presets +ARG ENVS_PRESET +ENV ENVS_PRESET=$ENVS_PRESET +COPY ./configs/envs ./configs/envs + # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ diff --git a/configs/app/chain.ts b/configs/app/chain.ts index 1ec156e4e0..32f5376aac 100644 --- a/configs/app/chain.ts +++ b/configs/app/chain.ts @@ -12,8 +12,8 @@ const chain = Object.freeze({ symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'), decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS, }, - governanceToken: { - symbol: getEnvValue('NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL'), + secondaryCoin: { + symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'), }, rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts index b136ce0d09..937397be6a 100644 --- a/configs/app/features/marketplace.ts +++ b/configs/app/features/marketplace.ts @@ -11,6 +11,9 @@ const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_F const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL'); +const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP'); +const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL'); +const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL'); const title = 'Marketplace'; @@ -22,31 +25,38 @@ const config: Feature<( categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined; securityReportsUrl: string | undefined; -} -> = (() => { + featuredApp: string | undefined; + banner: { contentUrl: string; linkUrl: string } | undefined; +}> = (() => { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { + const props = { + submitFormUrl, + categoriesUrl, + suggestIdeasFormUrl, + securityReportsUrl, + featuredApp, + banner: bannerContentUrl && bannerLinkUrl ? { + contentUrl: bannerContentUrl, + linkUrl: bannerLinkUrl, + } : undefined, + }; + if (configUrl) { return Object.freeze({ title, isEnabled: true, configUrl, - submitFormUrl, - categoriesUrl, - suggestIdeasFormUrl, - securityReportsUrl, + ...props, }); } else if (adminServiceApiHost) { return Object.freeze({ title, isEnabled: true, - submitFormUrl, - categoriesUrl, - suggestIdeasFormUrl, - securityReportsUrl, api: { endpoint: adminServiceApiHost, basePath: '', }, + ...props, }); } } diff --git a/configs/app/meta.ts b/configs/app/meta.ts index 70930b2fe3..1c2979a2c3 100644 --- a/configs/app/meta.ts +++ b/configs/app/meta.ts @@ -8,6 +8,7 @@ const meta = Object.freeze({ og: { description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '', imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl), + enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true', }, }); diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth index 2715974d4b..a2f22e39d1 100644 --- a/configs/envs/.env.eth +++ b/configs/envs/.env.eth @@ -22,21 +22,21 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap'] +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth.json ## footer ##views NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}] ## misc -NEXT_PUBLIC_NETWORK_EXPLORERS="[{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]" +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}},{'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}},{'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}},{'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}}] # app features NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com diff --git a/configs/envs/.env.sepolia b/configs/envs/.env.eth_sepolia similarity index 96% rename from configs/envs/.env.sepolia rename to configs/envs/.env.eth_sepolia index 0b837f2f1b..60e96d9ea9 100644 --- a/configs/envs/.env.sepolia +++ b/configs/envs/.env.eth_sepolia @@ -44,7 +44,7 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://sepolia.e NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C @@ -59,6 +59,7 @@ NEXT_PUBLIC_HAS_USER_OPS=true NEXT_PUBLIC_AD_BANNER_PROVIDER=getit NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com #meta NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png diff --git a/configs/envs/.env.gnosis b/configs/envs/.env.gnosis index 1e20f6c303..4c5b09260f 100644 --- a/configs/envs/.env.gnosis +++ b/configs/envs/.env.gnosis @@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=100 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com @@ -22,9 +23,9 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(46, 74, 60)" -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgb(255, 255, 255)" +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','secondary_coin_price','market_cap'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(46,74,60) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/gnosis-chain-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/gnosis.svg @@ -42,6 +43,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/gnosis-chain.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrmiO9mDGJoPNmJe +NEXT_PUBLIC_STATS_API_HOST=https://stats-gnosis-mainnet.k8s-prod-1.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com diff --git a/configs/envs/.env.main b/configs/envs/.env.main index 48eee58328..9322f0dbca 100644 --- a/configs/envs/.env.main +++ b/configs/envs/.env.main @@ -13,7 +13,6 @@ NEXT_PUBLIC_NETWORK_ID=5 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 -NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=ETH NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_IS_TESTNET=true @@ -51,6 +50,9 @@ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blocksc NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +# NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com diff --git a/configs/envs/.env.optimism b/configs/envs/.env.optimism new file mode 100644 index 0000000000..0c3416c034 --- /dev/null +++ b/configs/envs/.env.optimism @@ -0,0 +1,68 @@ +# Set of ENVs for Optimism (dev only) +# https://optimism.blockscout.com/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=OP Mainnet +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP +NEXT_PUBLIC_NETWORK_ID=10 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io + +# api configuration +NEXT_PUBLIC_API_HOST=optimism.blockscout.com +NEXT_PUBLIC_API_PORT=80 +NEXT_PUBLIC_API_PROTOCOL=http +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap','secondary_coin_price'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg,rgb(232,52,53)0%,rgb(139,28,232)100%) +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +## views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +## misc +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}] +# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-mainnet.k8s-prod-1.blockscout.com +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +# rollup +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ \ No newline at end of file diff --git a/configs/envs/.env.optimism_goerli b/configs/envs/.env.optimism_goerli deleted file mode 100644 index c20a941ab9..0000000000 --- a/configs/envs/.env.optimism_goerli +++ /dev/null @@ -1,48 +0,0 @@ -# Set of ENVs for zkevm (dev only) -# https://eth.blockscout.com/ - -# app configuration -NEXT_PUBLIC_APP_PROTOCOL=http -NEXT_PUBLIC_APP_HOST=localhost -NEXT_PUBLIC_APP_PORT=3000 - -# blockchain parameters -NEXT_PUBLIC_NETWORK_NAME='OP Goerli' -NEXT_PUBLIC_NETWORK_SHORT_NAME='OP Goerli' -NEXT_PUBLIC_NETWORK_ID=420 -NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether -NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH -NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 -NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation -NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.optimism.io - -# api configuration -NEXT_PUBLIC_API_HOST=optimism-goerli.blockscout.com -NEXT_PUBLIC_API_PORT=80 -NEXT_PUBLIC_API_PROTOCOL=http -NEXT_PUBLIC_API_BASE_PATH=/ - -# ui config -## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -## sidebar -NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json -## footer -## misc -NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] -# app features -NEXT_PUBLIC_APP_INSTANCE=local -NEXT_PUBLIC_APP_ENV=development -NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d -NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -# NEXT_PUBLIC_AUTH_URL=http://localhost:3000 -NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws -NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout -NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com -NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com -NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com -NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com -# rollup -NEXT_PUBLIC_ROLLUP_TYPE='optimistic' -NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw -NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-goerli.blockscout.com/ \ No newline at end of file diff --git a/configs/envs/.env.optimism_sepolia b/configs/envs/.env.optimism_sepolia new file mode 100644 index 0000000000..7bf13dc4a1 --- /dev/null +++ b/configs/envs/.env.optimism_sepolia @@ -0,0 +1,63 @@ +# Set of ENVs for Optimism (dev only) +# https://optimism.blockscout.com/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=OP Sepolia +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Sepolia +NEXT_PUBLIC_NETWORK_ID=11155420 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.optimism.io + +# api configuration +NEXT_PUBLIC_API_HOST=optimism-sepolia.blockscout.com + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg,rgb(232,52,53)0%,rgb(139,28,232)100%) +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-sepolia.json +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}] +## footer +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +## views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +## misc +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic-sepolia'}}] +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Build faster with the Superchain Dev Console: Get testnet ETH and tools to help you build,launch,and grow your app on the Superchain

+# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x59d26836041ab35169bdce431d68d070b7b8acb589fa52e126e6c828b6ece5e9 +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-sepolia.k8s.blockscout.com +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +# rollup +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/ diff --git a/configs/envs/.env.poa_core b/configs/envs/.env.poa_core deleted file mode 100644 index f874d89f5d..0000000000 --- a/configs/envs/.env.poa_core +++ /dev/null @@ -1,40 +0,0 @@ -# Set of ENVs for POA network explorer -# https://blockscout.com/poa/core/ - -# app configuration -NEXT_PUBLIC_APP_PROTOCOL=http -NEXT_PUBLIC_APP_HOST=localhost -NEXT_PUBLIC_APP_PORT=3000 - -# blockchain parameters -NEXT_PUBLIC_NETWORK_NAME=POA -NEXT_PUBLIC_NETWORK_SHORT_NAME=POA -NEXT_PUBLIC_NETWORK_ID=99 -NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA -NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA -NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 - -# api configuration -NEXT_PUBLIC_API_HOST=blockscout.com -NEXT_PUBLIC_API_BASE_PATH=/poa/core - -# ui config -## homepage -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='no-repeat bottom 20% right 0px/100% url(https://neon-labs.org/images/index/banner.jpg)' -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76 -## sidebar -NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json -NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/poa.svg -NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/poa.svg -## footer -## misc -NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}] - -# app features -NEXT_PUBLIC_APP_ENV=development -NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -NEXT_PUBLIC_AUTH_URL=http://localhost:3000 -NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout -NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation -NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network diff --git a/configs/envs/.env.polygon b/configs/envs/.env.polygon index 2cc023145d..e39066d6a6 100644 --- a/configs/envs/.env.polygon +++ b/configs/envs/.env.polygon @@ -23,8 +23,8 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)" -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgba(255, 255, 255, 1)" +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg,rgba(162,41,197,1)0%,rgba(123,63,228,1)100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg @@ -35,9 +35,7 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c # app features NEXT_PUBLIC_APP_ENV=development -# NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efdee49d7cda76a50001017 NEXT_PUBLIC_HAS_BEACON_CHAIN=false -# NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] diff --git a/configs/envs/.env.rootstock b/configs/envs/.env.rootstock index ad4bcfb880..97b6ae0a48 100644 --- a/configs/envs/.env.rootstock +++ b/configs/envs/.env.rootstock @@ -23,7 +23,7 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(255, 145, 0)" +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(255,145,0) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/rootstock.svg diff --git a/configs/envs/.env.stability b/configs/envs/.env.stability index a395322118..6642687f41 100644 --- a/configs/envs/.env.stability +++ b/configs/envs/.env.stability @@ -24,9 +24,8 @@ NEXT_PUBLIC_API_BASE_PATH=/ # ui config ## homepage NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(255, 145, 0)" -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgba(46, 51, 81, 1)" -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgba(122, 235, 246, 1)" +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(46,51,81,1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(122,235,246,1) ## sidebar NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability.svg @@ -35,10 +34,10 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short-dark.svg ## footer ## views -NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS="['top_accounts']" -NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS="['value','fee_currency','gas_price','gas_fees','burnt_fees']" -NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS="['fee_per_gas']" -NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS="['burnt_fees','total_reward']" +NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts'] +NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','gas_fees','burnt_fees'] +NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas'] +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward'] ## misc # app features @@ -47,14 +46,16 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false NEXT_PUBLIC_STATS_API_HOST=https://stats-stability-testnet.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com -NEXT_PUBLIC_CONTRACT_CODE_IDES="[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com/ NEXT_PUBLIC_GAS_TRACKER_ENABLED=false -NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE='stability' +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability #meta NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/stability.png diff --git a/configs/envs/.env.zkevm b/configs/envs/.env.zkevm index 9d1ef2487a..b85f4dbcc4 100644 --- a/configs/envs/.env.zkevm +++ b/configs/envs/.env.zkevm @@ -29,8 +29,8 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)' -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(255, 255, 255, 1)' +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg,rgba(162,41,197,1)0%,rgba(123,63,228,1)100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) ## footer ## misc NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] @@ -39,7 +39,6 @@ NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -# NEXT_PUBLIC_AUTH_URL=http://localhost:3000 NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com diff --git a/configs/envs/.env.zksync b/configs/envs/.env.zksync index 6693f0ca4e..f9871ef506 100644 --- a/configs/envs/.env.zksync +++ b/configs/envs/.env.zksync @@ -31,8 +31,8 @@ NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync-dark.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short.svg NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short-dark.svg -NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(53, 103, 246, 1)' -NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(255, 255, 255, 1)' +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(53,103,246,1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255,255,255,1) NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://zksync.drpc.org?ref=559183','text':'Public RPC'}] ## footer NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/zksync.json @@ -46,9 +46,8 @@ NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648 NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true -# NEXT_PUBLIC_AUTH_URL=http://localhost:3000 -NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh index cc9f4a09a7..747b7269c9 100755 --- a/deploy/scripts/download_assets.sh +++ b/deploy/scripts/download_assets.sh @@ -17,6 +17,7 @@ ASSETS_ENVS=( "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL" "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" + "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL" "NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_NETWORK_LOGO" diff --git a/deploy/scripts/entrypoint.sh b/deploy/scripts/entrypoint.sh index 2924f09189..8af928da13 100755 --- a/deploy/scripts/entrypoint.sh +++ b/deploy/scripts/entrypoint.sh @@ -1,5 +1,39 @@ #!/bin/bash + +export_envs_from_preset() { + if [ -z "$ENVS_PRESET" ]; then + return + fi + + if [ "$ENVS_PRESET" = "none" ]; then + return + fi + + local preset_file="./configs/envs/.env.$ENVS_PRESET" + + if [ ! -f "$preset_file" ]; then + return + fi + + local blacklist=( + "NEXT_PUBLIC_APP_PROTOCOL" + "NEXT_PUBLIC_APP_HOST" + "NEXT_PUBLIC_APP_PORT" + "NEXT_PUBLIC_APP_ENV" + ) + + while IFS='=' read -r name value; do + name="${name#"${name%%[![:space:]]*}"}" # Trim leading whitespace + if [[ -n $name && $name == "NEXT_PUBLIC_"* && ! "${blacklist[*]}" =~ "$name" ]]; then + export "$name"="$value" + fi + done < <(grep "^[^#;]" "$preset_file") +} + +# If there is a preset, load the environment variables from the its file +export_envs_from_preset + # Download external assets ./download_assets.sh ./public/assets diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index de24cbb3ef..191eedd27d 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -25,6 +25,7 @@ import type { ValidatorsChainType } from '../../../types/client/validators'; import type { WalletType } from '../../../types/client/wallets'; import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; +import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; import type { ChainIndicatorId } from '../../../types/homepage'; import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; import type { AddressViewId } from '../../../types/views/address'; @@ -194,6 +195,30 @@ const marketplaceSchema = yup // eslint-disable-next-line max-len otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), }), + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), }); const beaconChainSchema = yup @@ -457,7 +482,7 @@ const schema = yup NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(), - NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(), + NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string().oneOf([ 'validation', 'mining' ]), NEXT_PUBLIC_IS_TESTNET: yup.boolean(), @@ -474,7 +499,7 @@ const schema = yup .array() .transform(replaceQuotes) .json() - .of(yup.string().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])), + .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), @@ -577,6 +602,7 @@ const schema = yup NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(), NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 432d4973b7..d8ef046d25 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -30,7 +30,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] -NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=gETH +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png @@ -40,6 +40,7 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Test NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}] NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global diff --git a/deploy/tools/envs-validator/test/.env.marketplace b/deploy/tools/envs-validator/test/.env.marketplace index eaf12c6dc9..01eab57086 100644 --- a/deploy/tools/envs-validator/test/.env.marketplace +++ b/deploy/tools/envs-validator/test/.env.marketplace @@ -5,3 +5,6 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index 91313e4b12..c8f3c440b1 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -71,6 +71,9 @@ frontend: NEXT_PUBLIC_NETWORK_ID: '11155111' NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]" NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: https://www.basename.app NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar @@ -82,7 +85,7 @@ frontend: NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" NEXT_PUBLIC_HAS_USER_OPS: true NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" - NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: noves + NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true NEXT_PUBLIC_AD_BANNER_PROVIDER: getit @@ -90,7 +93,7 @@ frontend: NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }" NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }" NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true - PROMETHEUS_METRICS_ENABLED: true + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true envFromSecret: NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md index b46ea04108..00c6ef1629 100644 --- a/docs/DEPRECATED_ENVS.md +++ b/docs/DEPRECATED_ENVS.md @@ -6,4 +6,5 @@ | NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE | | NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | | NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL | -| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | \ No newline at end of file +| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | +| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | \ No newline at end of file diff --git a/docs/ENVS.md b/docs/ENVS.md index 8802bbc267..79bfe48e3d 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -73,6 +73,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ## Blockchain parameters +*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain). + | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | @@ -83,7 +85,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | -| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | +| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | | NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | @@ -107,7 +109,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | +| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | | NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `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)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | @@ -176,6 +178,7 @@ Settings for meta tags and OG tags | NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` | | NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | | NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | +| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` |   @@ -451,6 +454,9 @@ This feature is **always enabled**, but you can configure its behavior by passin | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | | NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | | NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | +| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | +| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | +| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | #### Marketplace app configuration properties diff --git a/icons/apps_xs.svg b/icons/apps_xs.svg index c3dd9f6867..4daa74955d 100644 --- a/icons/apps_xs.svg +++ b/icons/apps_xs.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/beta.svg b/icons/beta.svg index b5db1b60db..bba1309f3a 100644 --- a/icons/beta.svg +++ b/icons/beta.svg @@ -1,4 +1,4 @@ - + diff --git a/icons/beta_xs.svg b/icons/beta_xs.svg index d22d754e68..a6dc48ee4e 100644 --- a/icons/beta_xs.svg +++ b/icons/beta_xs.svg @@ -1,4 +1,4 @@ - + diff --git a/icons/blob.svg b/icons/blob.svg index 523a29bac5..9b40d72ecb 100644 --- a/icons/blob.svg +++ b/icons/blob.svg @@ -1,5 +1,5 @@ - + diff --git a/icons/contracts.svg b/icons/contracts.svg index 68fb55c349..1f0b62afd2 100644 --- a/icons/contracts.svg +++ b/icons/contracts.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/contracts_verified.svg b/icons/contracts_verified.svg index dbb95dd759..2a004f596d 100644 --- a/icons/contracts_verified.svg +++ b/icons/contracts_verified.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/social/twitter_filled.svg b/icons/social/twitter_filled.svg index 1850ec857b..0d73b850a0 100644 --- a/icons/social/twitter_filled.svg +++ b/icons/social/twitter_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/star_filled.svg b/icons/star_filled.svg index 63f95055c9..2bdea23a41 100644 --- a/icons/star_filled.svg +++ b/icons/star_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/star_outline.svg b/icons/star_outline.svg index bf6ad8c3e4..bf2eca9845 100644 --- a/icons/star_outline.svg +++ b/icons/star_outline.svg @@ -1,3 +1,3 @@ - + diff --git a/jest/lib.tsx b/jest/lib.tsx index 048f26733a..823e4b1f6e 100644 --- a/jest/lib.tsx +++ b/jest/lib.tsx @@ -15,13 +15,9 @@ import 'lib/setLocale'; const PAGE_PROPS = { cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', - adBannerProvider: '', + query: {}, + adBannerProvider: undefined, + apiData: null, }; const TestApp = ({ children }: {children: React.ReactNode}) => { diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 0a6aaa0a90..5f8453420c 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -29,11 +29,12 @@ import type { AddressNFTsResponse, AddressCollectionsResponse, AddressNFTTokensFilter, + AddressCoinBalanceHistoryChartOld, } from 'types/api/address'; import type { AddressesResponse } from 'types/api/addresses'; import type { TxBlobs, Blob } from 'types/api/blobs'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; -import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; +import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { BackendVersionConfig } from 'types/api/configs'; import type { SmartContract, @@ -87,6 +88,7 @@ import type { TransactionsResponseWatchlist, TransactionsSorting, TransactionsResponseWithBlobs, + TransactionsStats, } from 'types/api/transaction'; import type { TxInterpretationResponse } from 'types/api/txInterpretation'; import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; @@ -275,6 +277,9 @@ export const RESOURCES = { pathParams: [ 'height_or_hash' as const ], filterFields: [], }, + txs_stats: { + path: '/api/v2/transactions/stats', + }, txs_validated: { path: '/api/v2/transactions', filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], @@ -539,6 +544,9 @@ export const RESOURCES = { stats_charts_market: { path: '/api/v2/stats/charts/market', }, + stats_charts_secondary_coin_price: { + path: '/api/v2/stats/charts/secondary-coin-market', + }, // HOMEPAGE homepage_blocks: { @@ -822,6 +830,7 @@ Q extends 'token_info_applications' ? TokenInfoApplications : Q extends 'stats' ? HomeStats : Q extends 'stats_charts_txs' ? ChartTransactionResponse : Q extends 'stats_charts_market' ? ChartMarketResponse : +Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse : Q extends 'homepage_blocks' ? Array : Q extends 'homepage_txs' ? Array : Q extends 'homepage_txs_watchlist' ? Array : @@ -837,6 +846,7 @@ Q extends 'blocks' ? BlocksResponse : Q extends 'block' ? Block : Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : +Q extends 'txs_stats' ? TransactionsStats : Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs : @@ -859,7 +869,7 @@ Q extends 'address_internal_txs' ? AddressInternalTxsResponse : Q extends 'address_token_transfers' ? AddressTokenTransferResponse : Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : -Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : +Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart : Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_nfts' ? AddressNFTsResponse : diff --git a/lib/contexts/app.tsx b/lib/contexts/app.tsx index 1d7c7bf8c3..880f2bece1 100644 --- a/lib/contexts/app.tsx +++ b/lib/contexts/app.tsx @@ -10,13 +10,9 @@ type Props = { const AppContext = createContext({ cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', - adBannerProvider: '', + query: {}, + adBannerProvider: undefined, + apiData: null, }); export function AppContextProvider({ children, pageProps }: Props) { diff --git a/lib/contexts/marketplace.tsx b/lib/contexts/marketplace.tsx new file mode 100644 index 0000000000..2aba76e39b --- /dev/null +++ b/lib/contexts/marketplace.tsx @@ -0,0 +1,48 @@ +import { useRouter } from 'next/router'; +import React, { createContext, useContext, useEffect, useState, useMemo } from 'react'; + +type Props = { + children: React.ReactNode; +} + +type TMarketplaceContext = { + isAutoConnectDisabled: boolean; + setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void; +} + +const MarketplaceContext = createContext({ + isAutoConnectDisabled: false, + setIsAutoConnectDisabled: () => {}, +}); + +export function MarketplaceContextProvider({ children }: Props) { + const router = useRouter(); + const [ isAutoConnectDisabled, setIsAutoConnectDisabled ] = useState(false); + + useEffect(() => { + const handleRouteChange = () => { + setIsAutoConnectDisabled(false); + }; + + router.events.on('routeChangeStart', handleRouteChange); + + return () => { + router.events.off('routeChangeStart', handleRouteChange); + }; + }, [ router.events ]); + + const value = useMemo(() => ({ + isAutoConnectDisabled, + setIsAutoConnectDisabled, + }), [ isAutoConnectDisabled, setIsAutoConnectDisabled ]); + + return ( + + { children } + + ); +} + +export function useMarketplaceContext() { + return useContext(MarketplaceContext); +} diff --git a/lib/hooks/useAdblockDetect.tsx b/lib/hooks/useAdblockDetect.tsx index e0ee312647..415b150f07 100644 --- a/lib/hooks/useAdblockDetect.tsx +++ b/lib/hooks/useAdblockDetect.tsx @@ -1,15 +1,35 @@ import { useEffect } from 'react'; +import type { AdBannerProviders } from 'types/client/adProviders'; + +import config from 'configs/app'; import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import isBrowser from 'lib/isBrowser'; +const DEFAULT_URL = 'https://request-global.czilladx.com'; + +// in general, detect should work with any ad-provider url (that is alive) +// but we see some false-positive results in certain browsers +const TEST_URLS: Record = { + slise: 'https://v1.slise.xyz/serve', + coinzilla: 'https://request-global.czilladx.com', + adbutler: 'https://servedbyadbutler.com/app.js', + hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js', + // I don't have an url for getit to test + getit: DEFAULT_URL, + none: DEFAULT_URL, +}; + +const feature = config.features.adsBanner; + export default function useAdblockDetect() { const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); + const provider = feature.isEnabled && feature.provider; useEffect(() => { - if (isBrowser() && !hasAdblockCookie) { - const url = 'https://request-global.czilladx.com'; + if (isBrowser() && !hasAdblockCookie && provider) { + const url = TEST_URLS[provider] || DEFAULT_URL; fetch(url, { method: 'HEAD', mode: 'no-cors', diff --git a/lib/hooks/useIsMounted.tsx b/lib/hooks/useIsMounted.tsx new file mode 100644 index 0000000000..d14880ae1b --- /dev/null +++ b/lib/hooks/useIsMounted.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function useIsMounted() { + const [ isMounted, setIsMounted ] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, [ ]); + + return isMounted; +} diff --git a/lib/metadata/generate.test.ts b/lib/metadata/generate.test.ts index 5a37e98fe0..b9cbe2f806 100644 --- a/lib/metadata/generate.test.ts +++ b/lib/metadata/generate.test.ts @@ -4,26 +4,29 @@ import type { Route } from 'nextjs-routes'; import generate from './generate'; -interface TestCase { +interface TestCase { title: string; - route: R; - apiData?: ApiData; + route: { + pathname: Pathname; + query?: Route['query']; + }; + apiData?: ApiData; } -const TEST_CASES: Array> = [ +const TEST_CASES = [ { title: 'static route', route: { pathname: '/blocks', }, - }, + } as TestCase<'/blocks'>, { title: 'dynamic route', route: { pathname: '/tx/[hash]', query: { hash: '0x12345' }, }, - }, + } as TestCase<'/tx/[hash]'>, { title: 'dynamic route with API data', route: { @@ -31,7 +34,7 @@ const TEST_CASES: Array> = [ query: { hash: '0x12345' }, }, apiData: { symbol: 'USDT' }, - } as TestCase<{ pathname: '/token/[hash]'; query: { hash: string }}>, + } as TestCase<'/token/[hash]'>, ]; describe('generates correct metadata for:', () => { diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts index 8d24fd1606..3c16903583 100644 --- a/lib/metadata/generate.ts +++ b/lib/metadata/generate.ts @@ -1,4 +1,5 @@ import type { ApiData, Metadata } from './types'; +import type { RouteParams } from 'nextjs/types'; import type { Route } from 'nextjs-routes'; @@ -9,7 +10,7 @@ import compileValue from './compileValue'; import getPageOgType from './getPageOgType'; import * as templates from './templates'; -export default function generate(route: R, apiData?: ApiData): Metadata { +export default function generate(route: RouteParams, apiData: ApiData = null): Metadata { const params = { ...route.query, ...apiData, @@ -17,7 +18,7 @@ export default function generate(route: R, apiData?: ApiData network_title: getNetworkTitle(), }; - const compiledTitle = compileValue(templates.title.make(route.pathname), params); + const compiledTitle = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); const title = compiledTitle ? compiledTitle + (config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : '') : ''; const description = compileValue(templates.description.make(route.pathname), params); diff --git a/lib/metadata/getApiDataForSocialPreview.ts b/lib/metadata/getApiDataForSocialPreview.ts deleted file mode 100644 index 8357d1e2ef..0000000000 --- a/lib/metadata/getApiDataForSocialPreview.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'http'; - -import { httpLogger } from 'nextjs/utils/logger'; - -import metrics from 'lib/monitoring/metrics'; - -export default async function getApiDataForSocialPreview(req: IncomingMessage | undefined, res: ServerResponse | undefined, pathname: string) { - if (!req || !res || !metrics) { - return; - } - - const userAgent = req.headers['user-agent']; - - if (!userAgent) { - return; - } - - if (userAgent.toLowerCase().includes('twitter')) { - httpLogger(req, res); - metrics.requestCounter.inc({ route: pathname, bot: 'twitter' }); - } - - if (userAgent.toLowerCase().includes('facebook')) { - httpLogger(req, res); - metrics.requestCounter.inc({ route: pathname, bot: 'facebook' }); - } - - if (userAgent.toLowerCase().includes('telegram')) { - httpLogger(req, res); - metrics.requestCounter.inc({ route: pathname, bot: 'telegram' }); - } -} diff --git a/lib/metadata/index.ts b/lib/metadata/index.ts index 6cf182a7b7..903bd988e8 100644 --- a/lib/metadata/index.ts +++ b/lib/metadata/index.ts @@ -1,2 +1,3 @@ export { default as generate } from './generate'; export { default as update } from './update'; +export * from './types'; diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index acf0c4f87e..c82feb2c28 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -13,10 +13,10 @@ const TEMPLATE_MAP: Record = { '/contract-verification': 'verify contract', '/address/[hash]/contract-verification': 'contract verification for %hash%', '/tokens': 'tokens', - '/token/[hash]': '%symbol% token details', + '/token/[hash]': 'token details', '/token/[hash]/instance/[id]': 'NFT instance', '/apps': 'apps marketplace', - '/apps/[id]': '- %app_name%', + '/apps/[id]': 'marketplace app', '/stats': 'statistics', '/api-docs': 'REST API', '/graphiql': 'GraphQL', @@ -56,8 +56,15 @@ const TEMPLATE_MAP: Record = { '/auth/unverified-email': 'unverified email', }; -export function make(pathname: Route['pathname']) { - const template = TEMPLATE_MAP[pathname]; +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/token/[hash]': '%symbol% token details', + '/token/[hash]/instance/[id]': 'token instance for %symbol%', + '/apps/[id]': '- %app_name%', + '/address/[hash]': 'address details for %domain_name%', +}; + +export function make(pathname: Route['pathname'], isEnriched = false) { + const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname]; return `%network_name% ${ template }`; } diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts index 252dbc29cf..270970338f 100644 --- a/lib/metadata/types.ts +++ b/lib/metadata/types.ts @@ -1,11 +1,14 @@ import type { Route } from 'nextjs-routes'; /* eslint-disable @typescript-eslint/indent */ -export type ApiData = -R['pathname'] extends '/token/[hash]' ? { symbol: string } : -R['pathname'] extends '/token/[hash]/instance/[id]' ? { symbol: string } : -R['pathname'] extends '/apps/[id]' ? { app_name: string } : -never; +export type ApiData = +( + Pathname extends '/address/[hash]' ? { domain_name: string } : + Pathname extends '/token/[hash]' ? { symbol: string } : + Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : + Pathname extends '/apps/[id]' ? { app_name: string } : + never +) | null; export interface Metadata { title: string; diff --git a/lib/metadata/update.ts b/lib/metadata/update.ts index f6168c1ae6..123e3ca100 100644 --- a/lib/metadata/update.ts +++ b/lib/metadata/update.ts @@ -1,10 +1,11 @@ import type { ApiData } from './types'; +import type { RouteParams } from 'nextjs/types'; import type { Route } from 'nextjs-routes'; import generate from './generate'; -export default function update(route: R, apiData: ApiData) { +export default function update(route: RouteParams, apiData: ApiData) { const { title, description } = generate(route, apiData); window.document.title = title; diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index 20c4b92970..7abb8b5ee0 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -19,6 +19,7 @@ export enum EventTypes { EXPERIMENT_STARTED = 'Experiment started', FILTERS = 'Filters', BUTTON_CLICK = 'Button click', + PROMO_BANNER = 'Promo banner', } /* eslint-disable @typescript-eslint/indent */ @@ -100,7 +101,7 @@ Type extends EventTypes.PAGE_WIDGET ? ( } | { 'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts'; 'Info': string; - 'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup'; + 'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner'; } | { 'Type': 'Security score'; 'Source': 'Analyzed contracts popup'; @@ -122,5 +123,9 @@ Type extends EventTypes.BUTTON_CLICK ? { 'Content': 'Swap button'; 'Source': string; } : +Type extends EventTypes.PROMO_BANNER ? { + 'Source': 'Marketplace'; + 'Link': string; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/monitoring/metrics.ts b/lib/monitoring/metrics.ts index db722561ce..f6f6b1a3e7 100644 --- a/lib/monitoring/metrics.ts +++ b/lib/monitoring/metrics.ts @@ -8,13 +8,26 @@ const metrics = (() => { promClient.register.clear(); - const requestCounter = new promClient.Counter({ - name: 'request_counter', - help: 'Number of incoming requests', + const socialPreviewBotRequests = new promClient.Counter({ + name: 'social_preview_bot_requests_total', + help: 'Number of incoming requests from social preview bots', labelNames: [ 'route', 'bot' ] as const, }); - return { requestCounter }; + const searchEngineBotRequests = new promClient.Counter({ + name: 'search_engine_bot_requests_total', + help: 'Number of incoming requests from search engine bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const apiRequestDuration = new promClient.Histogram({ + name: 'api_request_duration_seconds', + help: 'Duration of requests to API in seconds', + labelNames: [ 'route', 'code' ], + buckets: [ 0.2, 0.5, 1, 3, 10 ], + }); + + return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration }; })(); export default metrics; diff --git a/mocks/address/coinBalanceHistory.ts b/mocks/address/coinBalanceHistory.ts index 7bd121dafe..cc78d75602 100644 --- a/mocks/address/coinBalanceHistory.ts +++ b/mocks/address/coinBalanceHistory.ts @@ -35,33 +35,36 @@ export const baseResponse: AddressCoinBalanceHistoryResponse = { next_page_params: null, }; -export const chartResponse: AddressCoinBalanceHistoryChart = [ - { - date: '2022-11-02', - value: '128238612887883515', - }, - { - date: '2022-11-03', - value: '199807583157570922', - }, - { - date: '2022-11-04', - value: '114487912907005778', - }, - { - date: '2022-11-05', - value: '219533112907005778', - }, - { - date: '2022-11-06', - value: '116487912907005778', - }, - { - date: '2022-11-07', - value: '199807583157570922', - }, - { - date: '2022-11-08', - value: '216488112907005778', - }, -]; +export const chartResponse: AddressCoinBalanceHistoryChart = { + items: [ + { + date: '2022-11-02', + value: '128238612887883515', + }, + { + date: '2022-11-03', + value: '199807583157570922', + }, + { + date: '2022-11-04', + value: '114487912907005778', + }, + { + date: '2022-11-05', + value: '219533112907005778', + }, + { + date: '2022-11-06', + value: '116487912907005778', + }, + { + date: '2022-11-07', + value: '199807583157570922', + }, + { + date: '2022-11-08', + value: '216488112907005778', + }, + ], + days: 10, +}; diff --git a/mocks/search/index.ts b/mocks/search/index.ts index af2555aa28..571eb24976 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -7,6 +7,7 @@ import type { SearchResult, SearchResultUserOp, SearchResultBlob, + SearchResultDomain, } from 'types/api/search'; export const token1: SearchResultToken = { @@ -123,6 +124,20 @@ export const blob1: SearchResultBlob = { timestamp: null, }; +export const domain1: SearchResultDomain = { + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + ens_info: { + address_hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + expiry_date: '2039-09-01T07:36:18.000Z', + name: 'vitalik.eth', + names_count: 1, + }, + is_smart_contract_verified: false, + name: null, + type: 'ens_domain', + url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', +}; + export const baseResponse: SearchResult = { items: [ token1, @@ -132,6 +147,7 @@ export const baseResponse: SearchResult = { contract1, tx1, blob1, + domain1, ], next_page_params: null, }; diff --git a/mocks/stats/index.ts b/mocks/stats/index.ts index d9d00e36e1..47d13cbb88 100644 --- a/mocks/stats/index.ts +++ b/mocks/stats/index.ts @@ -61,6 +61,11 @@ export const withoutBothPrices = { gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), }; +export const withSecondaryCoin = { + ...base, + secondary_coin_price: '3.398', +}; + export const noChartData = { ...base, transactions_today: null, diff --git a/mocks/txs/stats.ts b/mocks/txs/stats.ts new file mode 100644 index 0000000000..7b05dc975a --- /dev/null +++ b/mocks/txs/stats.ts @@ -0,0 +1,8 @@ +import type { TransactionsStats } from 'types/api/transaction'; + +export const base: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/nextjs/PageNextJs.tsx b/nextjs/PageNextJs.tsx index 8b3c6e7611..fe99868cb7 100644 --- a/nextjs/PageNextJs.tsx +++ b/nextjs/PageNextJs.tsx @@ -2,6 +2,7 @@ import Head from 'next/head'; import React from 'react'; import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; import config from 'configs/app'; import useAdblockDetect from 'lib/hooks/useAdblockDetect'; @@ -10,14 +11,17 @@ import * as metadata from 'lib/metadata'; import * as mixpanel from 'lib/mixpanel'; import { init as initSentry } from 'lib/sentry/config'; -type Props = Route & { +interface Props { + pathname: Pathname; children: React.ReactNode; + query?: PageProps['query']; + apiData?: PageProps['apiData']; } initSentry(); -const PageNextJs = (props: Props) => { - const { title, description, opengraph } = metadata.generate(props); +const PageNextJs = (props: Props) => { + const { title, description, opengraph } = metadata.generate(props, props.apiData); useGetCsrfToken(); useAdblockDetect(); diff --git a/nextjs/csp/policies/ad.ts b/nextjs/csp/policies/ad.ts index 55f23ff913..24ac82a0bb 100644 --- a/nextjs/csp/policies/ad.ts +++ b/nextjs/csp/policies/ad.ts @@ -13,12 +13,16 @@ export function ad(): CspDev.DirectiveDescriptor { '*.coinzilla.com', 'https://request-global.czilladx.com', + // adbutler + 'servedbyadbutler.com', + // slise '*.slise.xyz', // hype 'api.hypelab.com', '*.ixncdn.com', + '*.cloudfront.net', //getit 'v1.getittech.io', diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 6b3d959ab2..5135904673 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -1,23 +1,27 @@ -import type { GetServerSideProps } from 'next'; +import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; + +import type { AdBannerProviders } from 'types/client/adProviders'; + +import type { Route } from 'nextjs-routes'; import config from 'configs/app'; import isNeedProxy from 'lib/api/isNeedProxy'; const rollupFeature = config.features.rollup; const adBannerFeature = config.features.adsBanner; +import type * as metadata from 'lib/metadata'; -export type Props = { +export interface Props { + query: Route['query']; cookies: string; referrer: string; - id: string; - height_or_hash: string; - hash: string; - number: string; - q: string; - name: string; - adBannerProvider: string; + adBannerProvider: AdBannerProviders | undefined; + // if apiData is undefined, Next.js will complain that it is not serializable + // so we force it to be always present in the props but it can be null + apiData: metadata.ApiData | null; } -export const base: GetServerSideProps = async({ req, query }) => { +export const base = async ({ req, query }: GetServerSidePropsContext): +Promise>> => { const adBannerProvider = (() => { if (adBannerFeature.isEnabled) { if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) { @@ -28,20 +32,16 @@ export const base: GetServerSideProps = async({ req, query }) => { return adBannerFeature.provider; } } - return ''; + return; })(); return { props: { + query, cookies: req.headers.cookie || '', referrer: req.headers.referer || '', - id: query.id?.toString() || '', - hash: query.hash?.toString() || '', - height_or_hash: query.height_or_hash?.toString() || '', - number: query.number?.toString() || '', - q: query.q?.toString() || '', - name: query.name?.toString() || '', adBannerProvider, + apiData: null, }, }; }; @@ -119,14 +119,15 @@ export const batch: GetServerSideProps = async(context) => { return base(context); }; -export const marketplace: GetServerSideProps = async(context) => { +export const marketplace = async (context: GetServerSidePropsContext): +Promise>> => { if (!config.features.marketplace.isEnabled) { return { notFound: true, }; } - return base(context); + return base(context); }; export const apiDocs: GetServerSideProps = async(context) => { diff --git a/nextjs/types.ts b/nextjs/types.ts index 60c007daff..c0366ae090 100644 --- a/nextjs/types.ts +++ b/nextjs/types.ts @@ -1,6 +1,13 @@ import type { NextPage } from 'next'; +import type { Route } from 'nextjs-routes'; + // eslint-disable-next-line @typescript-eslint/ban-types export type NextPageWithLayout

= NextPage & { getLayout?: (page: React.ReactElement) => React.ReactNode; } + +export interface RouteParams { + pathname: Pathname; + query?: Route['query']; +} diff --git a/nextjs/utils/detectBotRequest.ts b/nextjs/utils/detectBotRequest.ts new file mode 100644 index 0000000000..1eecf6333a --- /dev/null +++ b/nextjs/utils/detectBotRequest.ts @@ -0,0 +1,52 @@ +import type { IncomingMessage } from 'http'; + +type SocialPreviewBot = 'twitter' | 'facebook' | 'telegram' | 'slack'; +type SearchEngineBot = 'google' | 'bing' | 'yahoo' | 'duckduckgo'; + +type ReturnType = { + type: 'social_preview'; + bot: SocialPreviewBot; +} | { + type: 'search_engine'; + bot: SearchEngineBot; +} | undefined + +export default function detectBotRequest(req: IncomingMessage): ReturnType { + const userAgent = req.headers['user-agent']; + + if (!userAgent) { + return; + } + + if (userAgent.toLowerCase().includes('twitter')) { + return { type: 'social_preview', bot: 'twitter' }; + } + + if (userAgent.toLowerCase().includes('facebook')) { + return { type: 'social_preview', bot: 'facebook' }; + } + + if (userAgent.toLowerCase().includes('telegram')) { + return { type: 'social_preview', bot: 'telegram' }; + } + + if (userAgent.toLowerCase().includes('slack')) { + return { type: 'social_preview', bot: 'slack' }; + } + + if (userAgent.toLowerCase().includes('googlebot')) { + return { type: 'search_engine', bot: 'google' }; + } + + if (userAgent.toLowerCase().includes('bingbot')) { + return { type: 'search_engine', bot: 'bing' }; + } + + if (userAgent.toLowerCase().includes('yahoo')) { + return { type: 'search_engine', bot: 'yahoo' }; + } + + if (userAgent.toLowerCase().includes('duckduck')) { + return { type: 'search_engine', bot: 'duckduckgo' }; + } +} diff --git a/nextjs/utils/fetchApi.ts b/nextjs/utils/fetchApi.ts new file mode 100644 index 0000000000..63eff42384 --- /dev/null +++ b/nextjs/utils/fetchApi.ts @@ -0,0 +1,53 @@ +import fetch, { AbortError } from 'node-fetch'; + +import buildUrl from 'nextjs/utils/buildUrl'; +import { httpLogger } from 'nextjs/utils/logger'; + +import { RESOURCES } from 'lib/api/resources'; +import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources'; +import { SECOND } from 'lib/consts'; +import metrics from 'lib/monitoring/metrics'; + +type Params = ( + { + resource: R; + pathParams?: ResourcePathParams; + } | { + url: string; + route: string; + } +) & { + timeout?: number; +} + +export default async function fetchApi>(params: Params): Promise { + const controller = new AbortController(); + + const timeout = setTimeout(() => { + controller.abort(); + }, params.timeout || SECOND); + + const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams); + const route = 'route' in params ? params.route : RESOURCES[params.resource]['path']; + + const end = metrics?.apiRequestDuration.startTimer(); + + try { + const response = await fetch(url, { signal: controller.signal }); + + const duration = end?.({ route, code: response.status }); + if (response.status === 200) { + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + } else { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); + } + + return await response.json() as Promise; + } catch (error) { + const code = error instanceof AbortError ? 504 : 500; + const duration = end?.({ route, code }); + httpLogger.logger.error({ message: 'API fetch', url, code, duration }); + } finally { + clearTimeout(timeout); + } +} diff --git a/nextjs/utils/fetch.ts b/nextjs/utils/fetchProxy.ts similarity index 91% rename from nextjs/utils/fetch.ts rename to nextjs/utils/fetchProxy.ts index 22bfc32ff6..0081781215 100644 --- a/nextjs/utils/fetch.ts +++ b/nextjs/utils/fetchProxy.ts @@ -30,14 +30,9 @@ export default function fetchFactory( }; httpLogger.logger.info({ - message: 'Trying to call API', + message: 'API fetch via Next.js proxy', url, - req: _req, - }); - - httpLogger.logger.info({ - message: 'API request headers', - headers, + // headers, }); const body = (() => { diff --git a/nextjs/utils/logRequestFromBot.ts b/nextjs/utils/logRequestFromBot.ts new file mode 100644 index 0000000000..6c22b63258 --- /dev/null +++ b/nextjs/utils/logRequestFromBot.ts @@ -0,0 +1,28 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import metrics from 'lib/monitoring/metrics'; + +import detectBotRequest from './detectBotRequest'; + +export default async function logRequestFromBot(req: IncomingMessage | undefined, res: ServerResponse | undefined, pathname: string) { + if (!req || !res || !metrics) { + return; + } + + const botInfo = detectBotRequest(req); + + if (!botInfo) { + return; + } + + switch (botInfo.type) { + case 'search_engine': { + metrics.searchEngineBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + case 'social_preview': { + metrics.socialPreviewBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + } +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 15fc5edd91..38b3c1d358 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,6 +12,7 @@ import config from 'configs/app'; import useQueryClientConfig from 'lib/api/useQueryClientConfig'; import { AppContextProvider } from 'lib/contexts/app'; import { ChakraProvider } from 'lib/contexts/chakra'; +import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { growthBook } from 'lib/growthbook/init'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; @@ -67,7 +68,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - { getLayout() } + + { getLayout() } + diff --git a/pages/_document.tsx b/pages/_document.tsx index 2101c04c4f..fafa653e85 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -3,9 +3,9 @@ import type { DocumentContext } from 'next/document'; import Document, { Html, Head, Main, NextScript } from 'next/document'; import React from 'react'; +import logRequestFromBot from 'nextjs/utils/logRequestFromBot'; import * as serverTiming from 'nextjs/utils/serverTiming'; -import getApiDataForSocialPreview from 'lib/metadata/getApiDataForSocialPreview'; import theme from 'theme'; import * as svgSprite from 'ui/shared/IconSvg'; @@ -22,7 +22,7 @@ class MyDocument extends Document { return result; }; - await getApiDataForSocialPreview(ctx.req, ctx.res, ctx.pathname); + await logRequestFromBot(ctx.req, ctx.res, ctx.pathname); const initialProps = await Document.getInitialProps(ctx); diff --git a/pages/address/[hash]/contract-verification.tsx b/pages/address/[hash]/contract-verification.tsx index 7e7fd704d2..25f56fa4c6 100644 --- a/pages/address/[hash]/contract-verification.tsx +++ b/pages/address/[hash]/contract-verification.tsx @@ -8,7 +8,7 @@ import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddr const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/address/[hash]/index.tsx b/pages/address/[hash]/index.tsx index 2319a31605..fc13029cc0 100644 --- a/pages/address/[hash]/index.tsx +++ b/pages/address/[hash]/index.tsx @@ -1,15 +1,22 @@ -import type { NextPage } from 'next'; -import dynamic from 'next/dynamic'; +import type { GetServerSideProps, NextPage } from 'next'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; -const Address = dynamic(() => import('ui/pages/Address'), { ssr: false }); +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import Address from 'ui/pages/Address'; -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/address/[hash]'; + +const Page: NextPage> = (props: Props) => { return ( - +

); @@ -17,4 +24,24 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + const addressData = await fetchApi({ + resource: 'address', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 1_000, + }); + + (await baseResponse.props).apiData = addressData && addressData.ens_domain_name ? { + domain_name: addressData.ens_domain_name, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/api/csrf.ts b/pages/api/csrf.ts index 8d4857b6f1..409ab8a1da 100644 --- a/pages/api/csrf.ts +++ b/pages/api/csrf.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import buildUrl from 'nextjs/utils/buildUrl'; -import fetchFactory from 'nextjs/utils/fetch'; +import fetchFactory from 'nextjs/utils/fetchProxy'; import { httpLogger } from 'nextjs/utils/logger'; export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) { diff --git a/pages/api/media-type.ts b/pages/api/media-type.ts index 7489fecdf1..95d4b3728a 100644 --- a/pages/api/media-type.ts +++ b/pages/api/media-type.ts @@ -3,16 +3,20 @@ import nodeFetch from 'node-fetch'; import { httpLogger } from 'nextjs/utils/logger'; +import metrics from 'lib/monitoring/metrics'; import getQueryParamString from 'lib/router/getQueryParamString'; export default async function mediaTypeHandler(req: NextApiRequest, res: NextApiResponse) { - httpLogger(req, res); try { const url = getQueryParamString(req.query.url); + + const end = metrics?.apiRequestDuration.startTimer(); const response = await nodeFetch(url, { method: 'HEAD' }); + const duration = end?.({ route: '/media-type', code: response.status }); if (response.status !== 200) { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); throw new Error(); } @@ -30,6 +34,8 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi return 'html'; } })(); + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + res.status(200).json({ type: mediaType }); } catch (error) { res.status(200).json({ type: undefined }); diff --git a/pages/api/metrics.ts b/pages/api/metrics.ts index d2f00ff430..c474dd7552 100644 --- a/pages/api/metrics.ts +++ b/pages/api/metrics.ts @@ -4,7 +4,7 @@ import * as promClient from 'prom-client'; // eslint-disable-next-line no-restricted-properties const isEnabled = process.env.PROMETHEUS_METRICS_ENABLED === 'true'; -isEnabled && promClient.collectDefaultMetrics(); +isEnabled && promClient.collectDefaultMetrics({ prefix: 'frontend_' }); export default async function metricsHandler(req: NextApiRequest, res: NextApiResponse) { const metrics = await promClient.register.metrics(); diff --git a/pages/api/proxy.ts b/pages/api/proxy.ts index 0756f3639c..95b168413b 100644 --- a/pages/api/proxy.ts +++ b/pages/api/proxy.ts @@ -2,7 +2,7 @@ import _pick from 'lodash/pick'; import _pickBy from 'lodash/pickBy'; import type { NextApiRequest, NextApiResponse } from 'next'; -import fetchFactory from 'nextjs/utils/fetch'; +import fetchFactory from 'nextjs/utils/fetchProxy'; import appConfig from 'configs/app'; diff --git a/pages/apps/[id].tsx b/pages/apps/[id].tsx index f99be74026..936c485f01 100644 --- a/pages/apps/[id].tsx +++ b/pages/apps/[id].tsx @@ -1,18 +1,29 @@ +import type { GetServerSideProps } from 'next'; import dynamic from 'next/dynamic'; import React from 'react'; import type { NextPageWithLayout } from 'nextjs/types'; +import type { MarketplaceAppOverview } from 'types/client/marketplace'; +import type { Route } from 'nextjs-routes'; +import * as gSSP from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; import LayoutApp from 'ui/shared/layout/LayoutApp'; const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false }); -const Page: NextPageWithLayout = (props: Props) => { +const pathname: Route['pathname'] = '/apps/[id]'; +const feature = config.features.marketplace; + +const Page: NextPageWithLayout> = (props: Props) => { return ( - + ); @@ -28,4 +39,40 @@ Page.getLayout = function getLayout(page: React.ReactElement) { export default Page; -export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.marketplace(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse && feature.isEnabled) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + + const appData = await(async() => { + if ('configUrl' in feature) { + const appList = await fetchApi>({ + url: config.app.baseUrl + feature.configUrl, + route: '/marketplace_config', + timeout: 1_000, + }); + + if (appList && Array.isArray(appList)) { + return appList.find(app => app.id === getQueryParamString(ctx.query.id)); + } + + } else { + return await fetchApi({ + resource: 'marketplace_dapp', + pathParams: { dappId: getQueryParamString(ctx.query.id), chainId: config.chain.id }, + timeout: 1_000, + }); + } + })(); + + (await baseResponse.props).apiData = appData && appData.title ? { + app_name: appData.title, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/batches/[number].tsx b/pages/batches/[number].tsx index 870a1cc7b4..814f8358ea 100644 --- a/pages/batches/[number].tsx +++ b/pages/batches/[number].tsx @@ -25,7 +25,7 @@ const Batch = dynamic(() => { const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/blobs/[hash].tsx b/pages/blobs/[hash].tsx index f1056d36fa..c042c862aa 100644 --- a/pages/blobs/[hash].tsx +++ b/pages/blobs/[hash].tsx @@ -9,7 +9,7 @@ const Blob = dynamic(() => import('ui/pages/Blob'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/block/[height_or_hash].tsx b/pages/block/[height_or_hash].tsx index 7d74d59c49..ef2c7652d2 100644 --- a/pages/block/[height_or_hash].tsx +++ b/pages/block/[height_or_hash].tsx @@ -9,7 +9,7 @@ const Block = dynamic(() => import('ui/pages/Block'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/contract-verification.tsx b/pages/contract-verification.tsx index a14df711e1..59dc7064b7 100644 --- a/pages/contract-verification.tsx +++ b/pages/contract-verification.tsx @@ -8,7 +8,7 @@ import ContractVerification from 'ui/pages/ContractVerification'; const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/name-domains/[name].tsx b/pages/name-domains/[name].tsx index 7a01829baa..d9346260b4 100644 --- a/pages/name-domains/[name].tsx +++ b/pages/name-domains/[name].tsx @@ -9,7 +9,7 @@ const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/op/[hash].tsx b/pages/op/[hash].tsx index 63080f817c..8a37663e5e 100644 --- a/pages/op/[hash].tsx +++ b/pages/op/[hash].tsx @@ -9,7 +9,7 @@ const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/search-results.tsx b/pages/search-results.tsx index 460b71b6e3..f69311ef51 100644 --- a/pages/search-results.tsx +++ b/pages/search-results.tsx @@ -12,7 +12,7 @@ const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: fal const Page: NextPageWithLayout = (props: Props) => { return ( - + ); diff --git a/pages/token/[hash]/index.tsx b/pages/token/[hash]/index.tsx index 70f41b66fd..27749fcdfb 100644 --- a/pages/token/[hash]/index.tsx +++ b/pages/token/[hash]/index.tsx @@ -1,15 +1,24 @@ -import type { NextPage } from 'next'; +import type { GetServerSideProps, NextPage } from 'next'; import dynamic from 'next/dynamic'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; const Token = dynamic(() => import('ui/pages/Token'), { ssr: false }); -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/token/[hash]'; + +const Page: NextPage> = (props: Props) => { return ( - + ); @@ -17,4 +26,24 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + const tokenData = await fetchApi({ + resource: 'token', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 1_000, + }); + + (await baseResponse.props).apiData = tokenData && tokenData.symbol ? { + symbol: tokenData.symbol, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/token/[hash]/instance/[id].tsx b/pages/token/[hash]/instance/[id].tsx index 62f0e71b96..973c8fd168 100644 --- a/pages/token/[hash]/instance/[id].tsx +++ b/pages/token/[hash]/instance/[id].tsx @@ -1,15 +1,24 @@ -import type { NextPage } from 'next'; +import type { GetServerSideProps, NextPage } from 'next'; import dynamic from 'next/dynamic'; import React from 'react'; +import type { Route } from 'nextjs-routes'; import type { Props } from 'nextjs/getServerSideProps'; +import * as gSSP from 'nextjs/getServerSideProps'; import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false }); -const Page: NextPage = (props: Props) => { +const pathname: Route['pathname'] = '/token/[hash]/instance/[id]'; + +const Page: NextPage> = (props: Props) => { return ( - + ); @@ -17,4 +26,24 @@ const Page: NextPage = (props: Props) => { export default Page; -export { base as getServerSideProps } from 'nextjs/getServerSideProps'; +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { + const botInfo = detectBotRequest(ctx.req); + + if (botInfo?.type === 'social_preview') { + const tokenData = await fetchApi({ + resource: 'token', + pathParams: { hash: getQueryParamString(ctx.query.hash) }, + timeout: 1_000, + }); + + (await baseResponse.props).apiData = tokenData && tokenData.symbol ? { + symbol: tokenData.symbol, + } : null; + } + } + + return baseResponse; +}; diff --git a/pages/tx/[hash].tsx b/pages/tx/[hash].tsx index 5eae4aedaa..f90b534fc8 100644 --- a/pages/tx/[hash].tsx +++ b/pages/tx/[hash].tsx @@ -9,7 +9,7 @@ const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false } const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/pages/txs/kettle/[hash].tsx b/pages/txs/kettle/[hash].tsx index 9b62f6dd0d..35a4470cb2 100644 --- a/pages/txs/kettle/[hash].tsx +++ b/pages/txs/kettle/[hash].tsx @@ -9,7 +9,7 @@ const KettleTxs = dynamic(() => import('ui/pages/KettleTxs'), { ssr: false }); const Page: NextPage = (props: Props) => { return ( - + ); diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index 8db3aee3b2..51c03e1c14 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -6,6 +6,7 @@ import { WagmiProvider } from 'wagmi'; import type { Props as PageProps } from 'nextjs/getServerSideProps'; +import config from 'configs/app'; import { AppContextProvider } from 'lib/contexts/app'; import { SocketProvider } from 'lib/socket/context'; import wagmiConfig from 'lib/web3/wagmiConfig'; @@ -24,13 +25,9 @@ const defaultAppContext = { pageProps: { cookies: '', referrer: '', - id: '', - height_or_hash: '', - hash: '', - number: '', - q: '', - name: '', - adBannerProvider: 'slise', + query: {}, + adBannerProvider: 'slise' as const, + apiData: null, }, }; @@ -47,7 +44,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props return ( - + diff --git a/playwright/fixtures/auth.ts b/playwright/fixtures/auth.ts index 4a2aea71f1..0bda117392 100644 --- a/playwright/fixtures/auth.ts +++ b/playwright/fixtures/auth.ts @@ -1,8 +1,13 @@ -import type { BrowserContext } from '@playwright/test'; +import type { BrowserContext, TestFixture } from '@playwright/test'; +import config from 'configs/app'; import * as cookies from 'lib/cookies'; -import { domain } from 'playwright/utils/app'; -export default function authFixture(context: BrowserContext) { - context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain, path: '/' } ]); +export function authenticateUser(context: BrowserContext) { + context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain: config.app.host, path: '/' } ]); } + +export const contextWithAuth: TestFixture = async({ context }, use) => { + authenticateUser(context); + use(context); +}; diff --git a/playwright/fixtures/contextWithEnvs.ts b/playwright/fixtures/contextWithEnvs.ts index e703a922fb..d9c1464462 100644 --- a/playwright/fixtures/contextWithEnvs.ts +++ b/playwright/fixtures/contextWithEnvs.ts @@ -7,7 +7,13 @@ interface Env { value: string; } -// keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa) +/** + * @deprecated please use mockEnvs fixture + * + * @export + * @param {Array} envs + * @return {*} {Parameters[0]['context']} + */ export default function contextWithEnvsFixture(envs: Array): Parameters[0]['context'] { return async({ browser }, use) => { const context = await createContextWithStorage(browser, envs); diff --git a/playwright/fixtures/contextWithFeatures.ts b/playwright/fixtures/contextWithFeatures.ts index a9c7836937..84f8a55f20 100644 --- a/playwright/fixtures/contextWithFeatures.ts +++ b/playwright/fixtures/contextWithFeatures.ts @@ -7,6 +7,13 @@ interface Feature { value: unknown; } +/** + * @deprecated please use mockFeatures fixture + * + * @export + * @param {Array} envs + * @return {*} {Parameters[0]['context']} + */ export default function contextWithFeaturesFixture(envs: Array): Parameters[0]['context'] { return async({ browser }, use) => { const storageItems = envs.map(({ id, value }) => ({ name: `pw_feature:${ id }`, value: JSON.stringify(value) })); diff --git a/playwright/fixtures/createContextWithStorage.ts b/playwright/fixtures/createContextWithStorage.ts index f2555b37c0..178dbf8a30 100644 --- a/playwright/fixtures/createContextWithStorage.ts +++ b/playwright/fixtures/createContextWithStorage.ts @@ -1,12 +1,20 @@ import type { Browser } from '@playwright/test'; -import * as app from 'playwright/utils/app'; +import config from 'configs/app'; +/** + * @deprecated please use mockEnvs or mockFeatures fixture + * + * @export + * @param {Browser} browser + * @param {Array<{ name: string; value: string }>} localStorage + * @return {*} + */ export default async function createContextWithEnvs(browser: Browser, localStorage: Array<{ name: string; value: string }>) { return browser.newContext({ storageState: { origins: [ - { origin: app.url, localStorage }, + { origin: config.app.baseUrl, localStorage }, ], cookies: [], }, diff --git a/playwright/fixtures/injectMetaMaskProvider.ts b/playwright/fixtures/injectMetaMaskProvider.ts new file mode 100644 index 0000000000..f09ee81a43 --- /dev/null +++ b/playwright/fixtures/injectMetaMaskProvider.ts @@ -0,0 +1,18 @@ +import type { TestFixture, Page } from '@playwright/test'; + +import type { WalletProvider } from 'types/web3'; + +export type InjectMetaMaskProvider = () => Promise; + +const fixture: TestFixture = async({ page }, use) => { + await use(async() => { + await page.evaluate(() => { + window.ethereum = { + isMetaMask: true, + _events: {}, + } as WalletProvider; + }); + }); +}; + +export default fixture; diff --git a/playwright/fixtures/mockConfigResponse.ts b/playwright/fixtures/mockConfigResponse.ts new file mode 100644 index 0000000000..e8f97b1092 --- /dev/null +++ b/playwright/fixtures/mockConfigResponse.ts @@ -0,0 +1,27 @@ +import type { TestFixture, Page } from '@playwright/test'; + +import config from 'configs/app'; +import { buildExternalAssetFilePath } from 'configs/app/utils'; + +export type MockConfigResponseFixture = (envName: string, envValue: string, content: string, isImage?: boolean) => Promise; + +const fixture: TestFixture = async({ page }, use) => { + await use(async(envName, envValue, content, isImage) => { + const url = config.app.baseUrl + buildExternalAssetFilePath(envName, envValue); + + if (isImage) { + await page.route(url, (route) => route.fulfill({ + status: 200, + path: content, + })); + } else { + await page.route(url, (route) => route.fulfill({ + status: 200, + body: content, + })); + } + + }); +}; + +export default fixture; diff --git a/playwright/fixtures/mockEnvs.ts b/playwright/fixtures/mockEnvs.ts new file mode 100644 index 0000000000..0ad44d5f0d --- /dev/null +++ b/playwright/fixtures/mockEnvs.ts @@ -0,0 +1,30 @@ +/* eslint-disable max-len */ +import type { TestFixture, Page } from '@playwright/test'; + +export type MockEnvsFixture = (envs: Array<[string, string]>) => Promise; + +const fixture: TestFixture = async({ page }, use) => { + await use(async(envs) => { + for (const [ name, value ] of envs) { + await page.evaluate(({ name, value }) => { + window.localStorage.setItem(name, value); + }, { name, value }); + } + }); +}; + +export default fixture; + +export const ENVS_MAP: Record> = { + shibariumRollup: [ + [ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ], + [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], + ], + bridgedTokens: [ + [ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ], + [ 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ], + ], + userOps: [ + [ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ], + ], +}; diff --git a/playwright/fixtures/mockFeatures.ts b/playwright/fixtures/mockFeatures.ts new file mode 100644 index 0000000000..9b82992c70 --- /dev/null +++ b/playwright/fixtures/mockFeatures.ts @@ -0,0 +1,16 @@ +/* eslint-disable max-len */ +import type { TestFixture, Page } from '@playwright/test'; + +export type MockFeaturesFixture = (features: Array<[string, unknown]>) => Promise; + +const fixture: TestFixture = async({ page }, use) => { + await use(async(features) => { + for (const [ name, value ] of features) { + await page.evaluate(({ name, value }) => { + window.localStorage.setItem(`pw_feature:${ name }`, JSON.stringify(value)); + }, { name, value }); + } + }); +}; + +export default fixture; diff --git a/playwright/lib.tsx b/playwright/lib.tsx index 215f71d17e..40a4bc4ea3 100644 --- a/playwright/lib.tsx +++ b/playwright/lib.tsx @@ -3,27 +3,35 @@ import { test as base } from '@playwright/experimental-ct-react'; import * as textAdMock from 'mocks/ad/textAd'; -import type { MockApiResponseFixture } from './fixtures/mockApiResponse'; -import mockApiResponseFixture from './fixtures/mockApiResponse'; -import type { MockAssetResponseFixture } from './fixtures/mockAssetResponse'; -import mockAssetResponseFixture from './fixtures/mockAssetResponse'; -import type { RenderFixture } from './fixtures/render'; -import renderFixture from './fixtures/render'; -import type { CreateSocketFixture } from './fixtures/socketServer'; -import { createSocket as createSocketFixture } from './fixtures/socketServer'; +import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider'; +import * as mockApiResponse from './fixtures/mockApiResponse'; +import * as mockAssetResponse from './fixtures/mockAssetResponse'; +import * as mockConfigResponse from './fixtures/mockConfigResponse'; +import * as mockEnvs from './fixtures/mockEnvs'; +import * as mockFeatures from './fixtures/mockFeatures'; +import * as render from './fixtures/render'; +import * as socketServer from './fixtures/socketServer'; interface Fixtures { - render: RenderFixture; - mockApiResponse: MockApiResponseFixture; - mockAssetResponse: MockAssetResponseFixture; - createSocket: CreateSocketFixture; + render: render.RenderFixture; + mockApiResponse: mockApiResponse.MockApiResponseFixture; + mockAssetResponse: mockAssetResponse.MockAssetResponseFixture; + mockConfigResponse: mockConfigResponse.MockConfigResponseFixture; + mockEnvs: mockEnvs.MockEnvsFixture; + mockFeatures: mockFeatures.MockFeaturesFixture; + createSocket: socketServer.CreateSocketFixture; + injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider; } const test = base.extend({ - render: renderFixture, - mockApiResponse: mockApiResponseFixture, - mockAssetResponse: mockAssetResponseFixture, - createSocket: createSocketFixture, + render: render.default, + mockApiResponse: mockApiResponse.default, + mockAssetResponse: mockAssetResponse.default, + mockConfigResponse: mockConfigResponse.default, + mockEnvs: mockEnvs.default, + mockFeatures: mockFeatures.default, + createSocket: socketServer.createSocket, + injectMetaMaskProvider: injectMetaMaskProvider.default, }); test.beforeEach(async({ page }) => { diff --git a/playwright/utils/app.ts b/playwright/utils/app.ts index 475376830e..466757ee9b 100644 --- a/playwright/utils/app.ts +++ b/playwright/utils/app.ts @@ -1,5 +1 @@ -export const url = `${ process.env.NEXT_PUBLIC_APP_PROTOCOL }://${ process.env.NEXT_PUBLIC_APP_HOST }:${ process.env.NEXT_PUBLIC_APP_PORT }`; - -export const domain = process.env.NEXT_PUBLIC_APP_HOST; - export const socketPort = 3200; diff --git a/playwright/utils/buildApiUrl.ts b/playwright/utils/buildApiUrl.ts index f8ce6bc063..346828c618 100644 --- a/playwright/utils/buildApiUrl.ts +++ b/playwright/utils/buildApiUrl.ts @@ -4,8 +4,6 @@ import config from 'configs/app'; import type { ResourceName, ResourcePathParams } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources'; -// DEPRECATED - /** * @deprecated please use fixture mockApiResponse from playwright/lib.tsx for rendering test suite * diff --git a/playwright/utils/configs.ts b/playwright/utils/configs.ts index d0a7ac6da9..20896e17d2 100644 --- a/playwright/utils/configs.ts +++ b/playwright/utils/configs.ts @@ -20,20 +20,6 @@ export const featureEnvs = { { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, { name: 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' }, ], - shibariumRollup: [ - { name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'shibarium' }, - { name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' }, - ], - bridgedTokens: [ - { - name: 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', - value: '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]', - }, - { - name: 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', - value: '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]', - }, - ], txInterpretation: [ { name: 'NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER', value: 'blockscout' }, ], diff --git a/stubs/tx.ts b/stubs/tx.ts index 1f7eb1c0a6..2e80408689 100644 --- a/stubs/tx.ts +++ b/stubs/tx.ts @@ -1,5 +1,5 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; -import type { Transaction } from 'types/api/transaction'; +import type { Transaction, TransactionsStats } from 'types/api/transaction'; import { ADDRESS_PARAMS } from './addressParams'; @@ -59,3 +59,10 @@ export const TX_ZKEVM_L2: Transaction = { }; export const TX_RAW_TRACE: RawTracesResponse = []; + +export const TXS_STATS: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/theme/components/Tag/Tag.ts b/theme/components/Tag/Tag.ts index 5cd5b2bd97..2224f19c01 100644 --- a/theme/components/Tag/Tag.ts +++ b/theme/components/Tag/Tag.ts @@ -23,7 +23,7 @@ const sizes = { minH: 6, minW: 6, fontSize: 'sm', - px: 2, + px: 1, py: '2px', lineHeight: 5, }, diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png index e2964560a2..5fc7bad237 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png index 8a1efa746b..28d17a8fe0 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png index a956655fb5..6afe64a5a7 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png index 18f0765090..a14814713f 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png index 51d848855b..fd9bb6cfc6 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png index 9fd2e54028..a9b36a72cd 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png index e4b5437ff4..bbaed04293 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png index 475e4b3500..ce7946c035 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png index e2df67339c..1cac3c3c0c 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png index 533fb98865..f37bae331a 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png index 89b0f21d9b..24fb120a47 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png index 78612cf01f..7a3f917391 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png index bcad901b56..f9b325adf1 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png index 6d1b5b96e0..cccd73491f 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png index 3b47811eaa..ee9d7d6dbd 100644 Binary files a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png differ diff --git a/types/api/address.ts b/types/api/address.ts index 37955b7f28..cbaaeafb77 100644 --- a/types/api/address.ts +++ b/types/api/address.ts @@ -148,11 +148,20 @@ export interface AddressCoinBalanceHistoryResponse { } | null; } -export type AddressCoinBalanceHistoryChart = Array<{ +// remove after api release +export type AddressCoinBalanceHistoryChartOld = Array<{ date: string; value: string; }> +export type AddressCoinBalanceHistoryChart = { + items: Array<{ + date: string; + value: string; + }>; + days: number; +}; + export interface AddressBlocksValidatedResponse { items: Array; next_page_params: { diff --git a/types/api/charts.ts b/types/api/charts.ts index 5d74b38410..a5504ded12 100644 --- a/types/api/charts.ts +++ b/types/api/charts.ts @@ -18,3 +18,8 @@ export interface ChartMarketResponse { available_supply: string; chart_data: Array; } + +export interface ChartSecondaryCoinPriceResponse { + available_supply: string; + chart_data: Array; +} diff --git a/types/api/search.ts b/types/api/search.ts index da8194eef5..bb9330a8a0 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -31,6 +31,20 @@ export interface SearchResultAddressOrContract { }; } +export interface SearchResultDomain { + type: 'ens_domain'; + name: string | null; + address: string; + is_smart_contract_verified: boolean; + url?: string; // not used by the frontend, we build the url ourselves + ens_info: { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; + }; +} + export interface SearchResultLabel { type: 'label'; address: string; @@ -69,7 +83,7 @@ export interface SearchResultUserOp { } export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp | -SearchResultBlob; +SearchResultBlob | SearchResultDomain; export interface SearchResult { items: Array; diff --git a/types/api/stats.ts b/types/api/stats.ts index 40fe7d34e2..5f3467597c 100644 --- a/types/api/stats.ts +++ b/types/api/stats.ts @@ -16,6 +16,8 @@ export type HomeStats = { network_utilization_percentage: number; tvl: string | null; rootstock_locked_btc?: string | null; + last_output_root_size?: string | null; + secondary_coin_price?: string | null; } export type GasPrices = { diff --git a/types/api/tokens.ts b/types/api/tokens.ts index adeca8d768..6538e690ec 100644 --- a/types/api/tokens.ts +++ b/types/api/tokens.ts @@ -8,7 +8,7 @@ export type TokensResponse = { items_count: number; name: string; market_cap: string | null; - }; + } | null; } export type TokensFilters = { q: string; type: Array | undefined }; diff --git a/types/api/transaction.ts b/types/api/transaction.ts index 8f1fee017e..f6a3841354 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -97,6 +97,13 @@ export type Transaction = { export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; +export interface TransactionsStats { + pending_transactions_count: string; + transaction_fees_avg_24h: string; + transaction_fees_sum_24h: string; + transactions_count_24h: string; +} + export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export interface TransactionsResponseValidated { diff --git a/types/homepage.ts b/types/homepage.ts index b773176288..2492134e97 100644 --- a/types/homepage.ts +++ b/types/homepage.ts @@ -1 +1,2 @@ -export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap' | 'tvl'; +export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; +export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 5b7cf3aa7e..6d76d04302 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -7,6 +7,7 @@ import type { NovesHistoryFilterValue } from 'types/api/noves'; import { NovesHistoryFilterValues } from 'types/api/noves'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { generateListStub } from 'stubs/utils'; @@ -25,10 +26,12 @@ const getFilterValue = (getFilterValueFromQuery).bind(n type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; } -const AddressAccountHistory = ({ scrollRef }: Props) => { +const AddressAccountHistory = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); @@ -49,6 +52,10 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { setFilterValue(newVal); }, [ ]); + if (!isMounted || !shouldRender) { + return null; + } + const actionBar = ( ; + shouldRender?: boolean; } -const AddressBlocksValidated = ({ scrollRef }: Props) => { +const AddressBlocksValidated = ({ scrollRef, shouldRender = true }: Props) => { const [ socketAlert, setSocketAlert ] = React.useState(false); const queryClient = useQueryClient(); const router = useRouter(); + const isMounted = useIsMounted(); const addressHash = String(router.query.hash); const query = useQueryWithPages({ @@ -84,6 +87,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + const content = query.data?.items ? ( <> { socketAlert && } diff --git a/ui/address/AddressCoinBalance.tsx b/ui/address/AddressCoinBalance.tsx index 1f109cf366..d4b366530d 100644 --- a/ui/address/AddressCoinBalance.tsx +++ b/ui/address/AddressCoinBalance.tsx @@ -6,6 +6,7 @@ import type { SocketMessage } from 'lib/socket/types'; import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; import { getResourceKey } from 'lib/api/useApiQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -17,10 +18,16 @@ import SocketAlert from 'ui/shared/SocketAlert'; import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory'; -const AddressCoinBalance = () => { +type Props = { + shouldRender?: boolean; +} + +const AddressCoinBalance = ({ shouldRender = true }: Props) => { const [ socketAlert, setSocketAlert ] = React.useState(false); const queryClient = useQueryClient(); const router = useRouter(); + const isMounted = useIsMounted(); + const scrollRef = React.useRef(null); const addressHash = getQueryParamString(router.query.hash); @@ -78,6 +85,10 @@ const AddressCoinBalance = () => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + return ( <> { socketAlert && } diff --git a/ui/address/AddressDetails.tsx b/ui/address/AddressDetails.tsx index 662fd86a70..3234694572 100644 --- a/ui/address/AddressDetails.tsx +++ b/ui/address/AddressDetails.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; import React from 'react'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; @@ -60,6 +61,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { has_validated_blocks: false, }), [ addressHash ]); + const isMounted = useIsMounted(); + // error handling (except 404 codes) if (addressQuery.isError) { if (isCustomAppError(addressQuery.error)) { @@ -74,7 +77,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { const data = addressQuery.isError ? error404Data : addressQuery.data; - if (!data) { + if (!data || !isMounted) { return null; } diff --git a/ui/address/AddressInternalTxs.tsx b/ui/address/AddressInternalTxs.tsx index d4d302e39b..ebaf11cbaf 100644 --- a/ui/address/AddressInternalTxs.tsx +++ b/ui/address/AddressInternalTxs.tsx @@ -6,6 +6,7 @@ import type { AddressFromToFilter } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; import { apos } from 'lib/html-entities'; import getQueryParamString from 'lib/router/getQueryParamString'; import { INTERNAL_TX } from 'stubs/internalTx'; @@ -22,8 +23,14 @@ import AddressIntTxsList from './internals/AddressIntTxsList'; const getFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); -const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; +} +const AddressInternalTxs = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); const hash = getQueryParamString(router.query.hash); @@ -55,6 +62,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject diff --git a/ui/address/AddressLogs.tsx b/ui/address/AddressLogs.tsx index 5de923c0fd..286a3c6fac 100644 --- a/ui/address/AddressLogs.tsx +++ b/ui/address/AddressLogs.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router'; import React from 'react'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { LOG } from 'stubs/log'; import { generateListStub } from 'stubs/utils'; @@ -12,8 +13,14 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import AddressCsvExportLink from './AddressCsvExportLink'; -const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props ={ + scrollRef?: React.RefObject; + shouldRender?: boolean; +} + +const AddressLogs = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ @@ -41,6 +48,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject ); + if (!isMounted || !shouldRender) { + return null; + } + const content = data?.items ? data.items.map((item, index) => ) : null; return ( diff --git a/ui/address/AddressTokenTransfers.tsx b/ui/address/AddressTokenTransfers.tsx index 0ebba17f8e..a275478e12 100644 --- a/ui/address/AddressTokenTransfers.tsx +++ b/ui/address/AddressTokenTransfers.tsx @@ -13,6 +13,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import { apos } from 'lib/html-entities'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; @@ -63,14 +64,16 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; // for tests only overloadCount?: number; } -const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { +const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const currentAddress = getQueryParamString(router.query.hash); @@ -179,6 +182,18 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr handler: handleNewSocketMessage, }); + const tokenData = React.useMemo(() => ({ + address: tokenFilter || '', + name: '', + icon_url: '', + symbol: '', + type: 'ERC-20' as const, + }), [ tokenFilter ]); + + if (!isMounted || !shouldRender) { + return null; + } + const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; @@ -218,14 +233,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ) : null; - const tokenData = React.useMemo(() => ({ - address: tokenFilter || '', - name: '', - icon_url: '', - symbol: '', - type: 'ERC-20' as const, - }), [ tokenFilter ]); - const tokenFilterComponent = tokenFilter && ( Filtered by token diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx index ea0059232e..3f2c35a403 100644 --- a/ui/address/AddressTokens.tsx +++ b/ui/address/AddressTokens.tsx @@ -9,6 +9,7 @@ import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; @@ -41,9 +42,14 @@ const TAB_LIST_PROPS_MOBILE = { const getTokenFilterValue = (getFilterValuesFromQuery).bind(null, NFT_TOKEN_TYPE_IDS); -const AddressTokens = () => { +type Props = { + shouldRender?: boolean; +} + +const AddressTokens = ({ shouldRender = true }: Props) => { const router = useRouter(); const isMobile = useIsMobile(); + const isMounted = useIsMounted(); const scrollRef = React.useRef(null); @@ -99,6 +105,10 @@ const AddressTokens = () => { setTokenTypes(value); }, [ nftsQuery, collectionsQuery ]); + if (!isMounted || !shouldRender) { + return null; + } + const nftTypeFilter = ( 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }> nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/> diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx index d272982c71..2bb002b8f7 100644 --- a/ui/address/AddressTxs.tsx +++ b/ui/address/AddressTxs.tsx @@ -10,6 +10,7 @@ import type { Transaction, TransactionsSortingField, TransactionsSortingValue, T import { getResourceKey } from 'lib/api/useApiQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -47,13 +48,15 @@ const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, type Props = { scrollRef?: React.RefObject; + shouldRender?: boolean; // for tests only overloadCount?: number; } -const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { +const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); + const isMounted = useIsMounted(); const [ socketAlert, setSocketAlert ] = React.useState(''); const [ newItemsCount, setNewItemsCount ] = React.useState(0); @@ -156,6 +159,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { handler: handleNewSocketMessage, }); + if (!isMounted || !shouldRender) { + return null; + } + const filter = ( ; + shouldRender?: boolean; } -const AddressUserOps = ({ scrollRef }: Props) => { +const AddressUserOps = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); @@ -29,6 +32,10 @@ const AddressUserOps = ({ scrollRef }: Props) => { filters: { sender: hash }, }); + if (!isMounted || !shouldRender) { + return null; + } + return ; }; diff --git a/ui/address/AddressWithdrawals.tsx b/ui/address/AddressWithdrawals.tsx index bbc61e8730..ee8f9c4651 100644 --- a/ui/address/AddressWithdrawals.tsx +++ b/ui/address/AddressWithdrawals.tsx @@ -2,6 +2,7 @@ import { Show, Hide } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import useIsMounted from 'lib/hooks/useIsMounted'; import getQueryParamString from 'lib/router/getQueryParamString'; import { generateListStub } from 'stubs/utils'; import { WITHDRAWAL } from 'stubs/withdrawals'; @@ -12,8 +13,13 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem'; import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable'; -const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject}) => { +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; +} +const AddressWithdrawals = ({ scrollRef, shouldRender = true }: Props) => { const router = useRouter(); + const isMounted = useIsMounted(); const hash = getQueryParamString(router.query.hash); @@ -28,6 +34,11 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png index 4f76f75b60..6923895237 100644 Binary files a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png index 46777342c1..b6f13d887f 100644 Binary files a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png index 9a5fc4ea5d..4543e5121d 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png index bd3982620b..1bb1638dda 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png index 28f43e103b..e31c1c3b3e 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png index beb7cd8b55..006ae00ba0 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png index 86b0488857..4e2a2f0ded 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png index d38ef06070..2b18eaea58 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png index baa0f2ec89..bf64920931 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png index d1d4f7eab1..fce88a821b 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png index eca7f88c7f..5075d144bd 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png index 6a7c63dc6e..2e9b458cd6 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png index 26e56490ac..77060ddcd5 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png index 7432110133..40bd723d0d 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png index 03bc8552cd..4943e339c3 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/coinBalance/AddressCoinBalanceChart.tsx b/ui/address/coinBalance/AddressCoinBalanceChart.tsx index 4e551f1e8b..15003058a4 100644 --- a/ui/address/coinBalance/AddressCoinBalanceChart.tsx +++ b/ui/address/coinBalance/AddressCoinBalanceChart.tsx @@ -15,10 +15,17 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { pathParams: { hash: addressHash }, }); - const items = React.useMemo(() => data?.map(({ date, value }) => ({ - date: new Date(date), - value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(), - })), [ data ]); + const items = React.useMemo(() => { + if (!data) { + return undefined; + } + + const dataItems = 'items' in data ? data.items : data; + return dataItems.map(({ date, value }) => ({ + date: new Date(date), + value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(), + })); + }, [ data ]); return ( { isLoading={ isPending } h="300px" units={ currencyUnits.ether } + emptyText={ data && 'days' in data && `Insufficient data for the past ${ data.days } days` } /> ); }; diff --git a/ui/address/contract/ContractCode.tsx b/ui/address/contract/ContractCode.tsx index 7b77ebe3b5..2f59c386bc 100644 --- a/ui/address/contract/ContractCode.tsx +++ b/ui/address/contract/ContractCode.tsx @@ -1,4 +1,4 @@ -import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react'; +import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; @@ -16,6 +16,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; import * as stubs from 'stubs/contract'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import Hint from 'ui/shared/Hint'; import LinkExternal from 'ui/shared/LinkExternal'; import LinkInternal from 'ui/shared/LinkInternal'; import RawDataSnippet from 'ui/shared/RawDataSnippet'; @@ -34,11 +35,24 @@ type InfoItemProps = { content: string | React.ReactNode; className?: string; isLoading: boolean; + hint?: string; } -const InfoItem = chakra(({ label, content, className, isLoading }: InfoItemProps) => ( +const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoItemProps) => ( - { label } + + + { label } + { hint && ( + + ) } + + { content } )); @@ -251,7 +265,14 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { { data.name && } { data.compiler_version && } { data.evm_version && } - { licenseLink && } + { licenseLink && ( + + ) } { typeof data.optimization_enabled === 'boolean' && } { data.optimization_runs && } diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png index d4e7526c59..05c6e15e3a 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png index f11a52a153..719afffe43 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png index 6c88ba36e0..fc9a51d73d 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png index 191d800a72..a3c93ab6fe 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png index ab936fc8f6..bdbd0774b3 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png index 539f531700..16444113bf 100644 Binary files a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/ensDomains/AddressEnsDomains.tsx b/ui/address/ensDomains/AddressEnsDomains.tsx index af57b5bd20..a67612535d 100644 --- a/ui/address/ensDomains/AddressEnsDomains.tsx +++ b/ui/address/ensDomains/AddressEnsDomains.tsx @@ -1,4 +1,18 @@ -import { Box, Button, chakra, Flex, Grid, Hide, Popover, PopoverBody, PopoverContent, PopoverTrigger, Show, Skeleton, useDisclosure } from '@chakra-ui/react'; +import { + Box, + Button, + Flex, + Grid, + Hide, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Show, + Skeleton, + useDisclosure, + chakra, +} from '@chakra-ui/react'; import _clamp from 'lodash/clamp'; import React from 'react'; @@ -12,6 +26,7 @@ import dayjs from 'lib/date/dayjs'; import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import IconSvg from 'ui/shared/IconSvg'; import LinkInternal from 'ui/shared/LinkInternal'; +import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip'; interface Props { addressHash: string; @@ -90,25 +105,27 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => { return ( - + + + diff --git a/ui/gasTracker/GasTrackerPriceSnippet.tsx b/ui/gasTracker/GasTrackerPriceSnippet.tsx index 575fc2d732..0cae78edca 100644 --- a/ui/gasTracker/GasTrackerPriceSnippet.tsx +++ b/ui/gasTracker/GasTrackerPriceSnippet.tsx @@ -52,7 +52,7 @@ const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => { { data.price && data.fiat_price && } per transaction - { data.time && / { (data.time / SECOND).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s } + { typeof data.time === 'number' && data.time > 0 && / { (data.time / SECOND).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s } { data.base_fee && Base { data.base_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) } } diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index eb2b49cdd7..d57b296378 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -59,6 +59,7 @@ const Stats = () => { if (data) { !data.gas_prices && itemsCount--; data.rootstock_locked_btc && itemsCount++; + rollupFeature.isEnabled && data.last_output_root_size && itemsCount++; const isOdd = Boolean(itemsCount % 2); const gasInfoTooltip = hasGasTracker && data.gas_prices ? ( @@ -120,6 +121,15 @@ const Stats = () => { url={ route({ pathname: '/txs' }) } isLoading={ isLoading } /> + { rollupFeature.isEnabled && data.last_output_root_size && ( + + ) } { test.beforeEach(async({ page, mount }) => { await page.route(STATS_API_URL, (route) => route.fulfill({ status: 200, - body: JSON.stringify(statsMock.base), + body: JSON.stringify(statsMock.withSecondaryCoin), })); await page.route(TX_CHART_API_URL, (route) => route.fulfill({ status: 200, diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png index f0bd10b718..7974ccd078 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png index 24d453ca53..76d7c5c10c 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png index e54da4cf27..ea54aa9b85 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_no-data-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png index 671945380b..a153779a1c 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_partial-data-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png index 5049e90c57..2199ac671f 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png differ diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png index 31472481b5..54df3a78a4 100644 Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png differ diff --git a/ui/home/indicators/types.ts b/ui/home/indicators/types.ts index 7ae2453ada..05b7680be5 100644 --- a/ui/home/indicators/types.ts +++ b/ui/home/indicators/types.ts @@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types'; import type { ResourcePayload } from 'lib/api/resources'; -export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market'; +export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market' | 'stats_charts_secondary_coin_price'; export interface TChainIndicator { id: ChainIndicatorId; diff --git a/ui/home/indicators/utils/indicators.tsx b/ui/home/indicators/utils/indicators.tsx index 62783b7bee..5f17d1c163 100644 --- a/ui/home/indicators/utils/indicators.tsx +++ b/ui/home/indicators/utils/indicators.tsx @@ -50,13 +50,13 @@ const nativeTokenData = { const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = { id: 'coin_price', - title: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`, + title: `${ config.chain.currency.symbol } price`, value: (stats) => stats.coin_price === null ? '$N/A' : '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), valueDiff: (stats) => stats?.coin_price !== null ? stats?.coin_price_change_percentage : null, icon: , - hint: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } token daily price in USD.`, + hint: `${ config.chain.currency.symbol } token daily price in USD.`, api: { resourceName: 'stats_charts_market', dataFn: (response) => ([ { @@ -65,7 +65,30 @@ const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = { .sort(sortByDateDesc) .reduceRight(nonNullTailReducer, [] as Array) .map(mapNullToZero), - name: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`, + name: `${ config.chain.currency.symbol } price`, + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + } ]), + }, +}; + +const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_price'> = { + id: 'secondary_coin_price', + title: `${ config.chain.secondaryCoin.symbol } price`, + value: (stats) => !stats.secondary_coin_price || stats.secondary_coin_price === null ? + '$N/A' : + '$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + valueDiff: () => null, + icon: , + hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`, + api: { + resourceName: 'stats_charts_secondary_coin_price', + dataFn: (response) => ([ { + items: response.chart_data + .map((item) => ({ date: new Date(item.date), value: item.closing_price })) + .sort(sortByDateDesc) + .reduceRight(nonNullTailReducer, [] as Array) + .map(mapNullToZero), + name: `${ config.chain.secondaryCoin.symbol } price`, valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), } ]), }, @@ -138,6 +161,7 @@ const tvlIndicator: TChainIndicator<'stats_charts_market'> = { const INDICATORS = [ dailyTxsIndicator, coinPriceIndicator, + secondaryCoinPriceIndicator, marketPriceIndicator, tvlIndicator, ]; diff --git a/ui/marketplace/AppSecurityReport.tsx b/ui/marketplace/AppSecurityReport.tsx index 35301af312..70a060f1b5 100644 --- a/ui/marketplace/AppSecurityReport.tsx +++ b/ui/marketplace/AppSecurityReport.tsx @@ -54,6 +54,7 @@ const AppSecurityReport = ({ id, securityReport, height, showContractList, isLoa onClick={ handleButtonClick } height={ height } onlyIcon={ onlyIcon } + label="The security score is based on analysis of a DApp's smart contracts." /> diff --git a/ui/marketplace/Banner.tsx b/ui/marketplace/Banner.tsx new file mode 100644 index 0000000000..c70f6ade9e --- /dev/null +++ b/ui/marketplace/Banner.tsx @@ -0,0 +1,51 @@ +import type { MouseEvent } from 'react'; +import React from 'react'; + +import type { MarketplaceAppPreview } from 'types/client/marketplace'; + +import config from 'configs/app'; +import { apps as appsMock } from 'mocks/apps/apps'; + +import FeaturedApp from './Banner/FeaturedApp'; +import IframeBanner from './Banner/IframeBanner'; + +const feature = config.features.marketplace; + +type BannerProps = { + apps: Array | undefined; + favoriteApps: Array; + isLoading: boolean; + onInfoClick: (id: string) => void; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Banner') => void; + onAppClick: (event: MouseEvent, id: string) => void; +} + +const Banner = ({ apps = [], favoriteApps, isLoading, onInfoClick, onFavoriteClick, onAppClick }: BannerProps) => { + if (!feature.isEnabled) { + return null; + } + + if (feature.featuredApp) { + const app = apps.find(app => app.id === feature.featuredApp); + const isFavorite = favoriteApps.includes(feature.featuredApp); + if (!isLoading && !app) { + return null; + } + return ( + + ); + } else if (feature.banner) { + return ; + } + + return null; +}; + +export default Banner; diff --git a/ui/marketplace/Banner/FeaturedApp.tsx b/ui/marketplace/Banner/FeaturedApp.tsx new file mode 100644 index 0000000000..c9c03021e7 --- /dev/null +++ b/ui/marketplace/Banner/FeaturedApp.tsx @@ -0,0 +1,160 @@ +import { Link, Skeleton, useColorModeValue, LinkBox, Flex, Image, LinkOverlay, IconButton } from '@chakra-ui/react'; +import NextLink from 'next/link'; +import type { MouseEvent } from 'react'; +import React, { useCallback } from 'react'; + +import type { MarketplaceAppPreview } from 'types/client/marketplace'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import * as mixpanel from 'lib/mixpanel/index'; +import IconSvg from 'ui/shared/IconSvg'; + +import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon'; +import FeaturedAppMobile from './FeaturedAppMobile'; + +type FeaturedAppProps = { + app: MarketplaceAppPreview; + isFavorite: boolean; + isLoading: boolean; + onInfoClick: (id: string) => void; + onFavoriteClick: (id: string, isFavorite: boolean, source: 'Banner') => void; + onAppClick: (event: MouseEvent, id: string) => void; +} + +const FeaturedApp = ({ + app, isFavorite, isLoading, onAppClick, + onInfoClick, onFavoriteClick, +}: FeaturedAppProps) => { + const isMobile = useIsMobile(); + + const { id, url, external, title, logo, logoDarkMode, shortDescription, categories, internalWallet } = app; + const logoUrl = useColorModeValue(logo, logoDarkMode || logo); + const categoriesLabel = categories.join(', '); + + const backgroundColor = useColorModeValue('purple.50', 'whiteAlpha.100'); + + const handleInfoClick = useCallback((event: MouseEvent) => { + event.preventDefault(); + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Banner' }); + onInfoClick(id); + }, [ onInfoClick, id ]); + + const handleFavoriteClick = useCallback(() => { + onFavoriteClick(id, isFavorite, 'Banner'); + }, [ onFavoriteClick, id, isFavorite ]); + + if (isMobile) { + return ( + + ); + } + + return ( + + + + { + + + + + + { external ? ( + + { title } + + ) : ( + + + { title } + + + ) } + + + + + { categoriesLabel } + + + { !isLoading && ( + + More info + + ) } + + { !isLoading && ( + : + + } + /> + ) } + + + + { shortDescription } + + + + + ); +}; + +export default FeaturedApp; diff --git a/ui/marketplace/Banner/FeaturedAppMobile.tsx b/ui/marketplace/Banner/FeaturedAppMobile.tsx new file mode 100644 index 0000000000..0c9bb99f69 --- /dev/null +++ b/ui/marketplace/Banner/FeaturedAppMobile.tsx @@ -0,0 +1,158 @@ +import { IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, Flex } from '@chakra-ui/react'; +import type { MouseEvent } from 'react'; +import React from 'react'; + +import type { MarketplaceAppPreview } from 'types/client/marketplace'; + +import IconSvg from 'ui/shared/IconSvg'; + +import MarketplaceAppCardLink from '../MarketplaceAppCardLink'; +import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon'; + +interface Props extends MarketplaceAppPreview { + onInfoClick: (event: MouseEvent) => void; + isFavorite: boolean; + onFavoriteClick: () => void; + isLoading: boolean; + onAppClick: (event: MouseEvent, id: string) => void; +} + +const FeaturedAppMobile = ({ + id, + url, + external, + title, + logo, + logoDarkMode, + shortDescription, + categories, + onInfoClick, + isFavorite, + onFavoriteClick, + isLoading, + internalWallet, + onAppClick, +}: Props) => { + const categoriesLabel = categories.join(', '); + + const logoUrl = useColorModeValue(logo, logoDarkMode || logo); + + return ( + + + + + { + + + { !isLoading && ( + + + More info + + + ) } + + + + + + + + + + { categoriesLabel } + + + + { shortDescription } + + + + { !isLoading && ( + : + + } + /> + ) } + + + ); +}; + +export default React.memo(FeaturedAppMobile); diff --git a/ui/marketplace/Banner/IframeBanner.tsx b/ui/marketplace/Banner/IframeBanner.tsx new file mode 100644 index 0000000000..d3bb52c33a --- /dev/null +++ b/ui/marketplace/Banner/IframeBanner.tsx @@ -0,0 +1,51 @@ +import { Link, Skeleton, Box } from '@chakra-ui/react'; +import React, { useCallback, useState } from 'react'; + +import * as mixpanel from 'lib/mixpanel/index'; + +const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: string }) => { + const [ isFrameLoading, setIsFrameLoading ] = useState(true); + + const handleIframeLoad = useCallback(() => { + setIsFrameLoading(false); + }, []); + + const handleClick = useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PROMO_BANNER, { Source: 'Marketplace', Link: linkUrl }); + }, [ linkUrl ]); + + return ( + + + + + ); +}; + +export default IframeBanner; diff --git a/ui/marketplace/MarketplaceAppAlert.tsx b/ui/marketplace/MarketplaceAppAlert.tsx deleted file mode 100644 index 88436a603d..0000000000 --- a/ui/marketplace/MarketplaceAppAlert.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Alert } from '@chakra-ui/react'; -import React from 'react'; - -import type { IconName } from 'ui/shared/IconSvg'; -import IconSvg from 'ui/shared/IconSvg'; - -type Props = { - internalWallet: boolean | undefined; - isWalletConnected: boolean; -} - -const MarketplaceAppAlert = ({ internalWallet, isWalletConnected }: Props) => { - const message = React.useMemo(() => { - let icon: IconName = 'wallet'; - let text = 'Connect your wallet to Blockscout for full-featured access'; - let status: 'warning' | 'success' = 'warning'; - - if (isWalletConnected && internalWallet) { - icon = 'integration/full'; - text = 'Your wallet is connected with Blockscout'; - status = 'success'; - } else if (!internalWallet) { - icon = 'integration/partial'; - text = 'Connect your wallet in the app below'; - } - - return { icon, text, status }; - }, [ isWalletConnected, internalWallet ]); - - return ( - - - { message.text } - - ); -}; - -export default MarketplaceAppAlert; diff --git a/ui/marketplace/MarketplaceAppCard.tsx b/ui/marketplace/MarketplaceAppCard.tsx index e387cb0809..be9abd3fe1 100644 --- a/ui/marketplace/MarketplaceAppCard.tsx +++ b/ui/marketplace/MarketplaceAppCard.tsx @@ -1,10 +1,9 @@ -import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue } from '@chakra-ui/react'; +import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, chakra, Flex } from '@chakra-ui/react'; import type { MouseEvent } from 'react'; import React, { useCallback } from 'react'; import type { MarketplaceAppPreview } from 'types/client/marketplace'; -import * as mixpanel from 'lib/mixpanel/index'; import IconSvg from 'ui/shared/IconSvg'; import MarketplaceAppCardLink from './MarketplaceAppCardLink'; @@ -13,9 +12,10 @@ import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; interface Props extends MarketplaceAppPreview { onInfoClick: (id: string) => void; isFavorite: boolean; - onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void; + onFavoriteClick: (id: string, isFavorite: boolean) => void; isLoading: boolean; onAppClick: (event: MouseEvent, id: string) => void; + className?: string; } const MarketplaceAppCard = ({ @@ -33,23 +33,24 @@ const MarketplaceAppCard = ({ isLoading, internalWallet, onAppClick, + className, }: Props) => { const categoriesLabel = categories.join(', '); const handleInfoClick = useCallback((event: MouseEvent) => { event.preventDefault(); - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' }); onInfoClick(id); }, [ onInfoClick, id ]); const handleFavoriteClick = useCallback(() => { - onFavoriteClick(id, isFavorite, 'Discovery view'); + onFavoriteClick(id, isFavorite); }, [ onFavoriteClick, id, isFavorite ]); const logoUrl = useColorModeValue(logo, logoDarkMode || logo); return ( - - - { - + + { + - - - - + { !isLoading && ( + + + More info + + + ) } + - - { categoriesLabel } - + + + + - - { shortDescription } - + + { categoriesLabel } + - { !isLoading && ( - - - More info - - - ) } + { shortDescription } + + { !isLoading && ( ) } - + ); }; -export default React.memo(MarketplaceAppCard); +export default React.memo(chakra(MarketplaceAppCard)); diff --git a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx index 8ec443c35d..f884c11ca2 100644 --- a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx +++ b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx @@ -42,7 +42,7 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => { position="relative" cursor="pointer" verticalAlign="middle" - marginBottom={ 1 } + mb={ 1 } /> ); diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx index 1bf7c9f916..f20679e407 100644 --- a/ui/marketplace/MarketplaceAppModal.tsx +++ b/ui/marketplace/MarketplaceAppModal.tsx @@ -98,6 +98,12 @@ const MarketplaceAppModal = ({ const isMobile = useIsMobile(); const logoUrl = useColorModeValue(logo, logoDarkMode || logo); + function getHostname(url: string | undefined) { + try { + return new URL(url || '').hostname; + } catch (err) {} + } + return ( @@ -259,7 +266,7 @@ const MarketplaceAppModal = ({ overflow="hidden" textOverflow="ellipsis" > - { site } + { getHostname(site) } ) } diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx index 0b82faffeb..38654292c1 100644 --- a/ui/marketplace/MarketplaceAppTopBar.tsx +++ b/ui/marketplace/MarketplaceAppTopBar.tsx @@ -1,4 +1,4 @@ -import { chakra, Flex, Tooltip, Skeleton, useBoolean, Box } from '@chakra-ui/react'; +import { chakra, Flex, Tooltip, Skeleton, useBoolean } from '@chakra-ui/react'; import React from 'react'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace'; @@ -6,26 +6,28 @@ import { ContractListTypes } from 'types/client/marketplace'; import { route } from 'nextjs-routes'; +import config from 'configs/app'; import { useAppContext } from 'lib/contexts/app'; import useFeatureValue from 'lib/growthbook/useFeatureValue'; import useIsMobile from 'lib/hooks/useIsMobile'; import IconSvg from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; import LinkInternal from 'ui/shared/LinkInternal'; +import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; +import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; +import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; import AppSecurityReport from './AppSecurityReport'; import ContractListModal from './ContractListModal'; -import MarketplaceAppAlert from './MarketplaceAppAlert'; import MarketplaceAppInfo from './MarketplaceAppInfo'; type Props = { data: MarketplaceAppOverview | undefined; isLoading: boolean; - isWalletConnected: boolean; securityReport?: MarketplaceAppSecurityReport; } -const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityReport }: Props) => { +const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { const [ showContractList, setShowContractList ] = useBoolean(false); const appProps = useAppContext(); const isMobile = useIsMobile(); @@ -46,32 +48,14 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo return ( <> - - - + + { !isMobile && } + + - - - - - - - { (isExperiment && (securityReport || isLoading)) && ( - - - - ) } + + + + { (isExperiment && (securityReport || isLoading)) && ( + + ) } + { !isMobile && ( + + { config.features.account.isEnabled && } + { config.features.blockchainInteraction.isEnabled && } + + ) } { showContractList && ( { + const handleInfoClick = useCallback((id: string) => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' }); + showAppInfo(id); + }, [ showAppInfo ]); + + const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => { + onFavoriteClick(id, isFavorite, 'Discovery view'); + }, [ onFavoriteClick ]); + return apps.length > 0 ? ( ( (null); const [ hasPreviousStep, setHasPreviousStep ] = React.useState(false); - const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal') => { + const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal' | 'Banner') => { mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source }); const favoriteApps = getFavoriteApps(); @@ -146,6 +146,7 @@ export default function useMarketplace() { isError, error, categories, + apps: data, displayedApps, showAppInfo, selectedAppId, @@ -167,6 +168,7 @@ export default function useMarketplace() { categories, clearSelectedAppId, selectedAppId, + data, displayedApps, error, favoriteApps, @@ -179,7 +181,6 @@ export default function useMarketplace() { isAppInfoModalOpen, isDisclaimerModalOpen, showDisclaimer, - data?.length, isCategoriesPlaceholderData, showContractList, contractListModalType, diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 9f3d0b4da6..11bdd7a4af 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -39,7 +39,6 @@ import IconSvg from 'ui/shared/IconSvg'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; -import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ]; @@ -75,19 +74,22 @@ const AddressPageContent = () => { const contractTabs = useContractTabs(addressQuery.data); + const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); + const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData; + const tabs: Array = React.useMemo(() => { return [ { id: 'txs', title: 'Transactions', count: addressTabsCountersQuery.data?.transactions_count, - component: , + component: , }, txInterpretation.isEnabled && txInterpretation.provider === 'noves' ? { id: 'account_history', title: 'Account history', - component: , + component: , } : undefined, config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ? @@ -95,7 +97,7 @@ const AddressPageContent = () => { id: 'user_ops', title: 'User operations', count: userOpsAccountQuery.data?.total_ops, - component: , + component: , } : undefined, config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? @@ -103,39 +105,39 @@ const AddressPageContent = () => { id: 'withdrawals', title: 'Withdrawals', count: addressTabsCountersQuery.data?.withdrawals_count, - component: , + component: , } : undefined, { id: 'token_transfers', title: 'Token transfers', count: addressTabsCountersQuery.data?.token_transfers_count, - component: , + component: , }, { id: 'tokens', title: 'Tokens', count: addressTabsCountersQuery.data?.token_balances_count, - component: , + component: , subTabs: TOKEN_TABS, }, { id: 'internal_txns', title: 'Internal txns', count: addressTabsCountersQuery.data?.internal_txs_count, - component: , + component: , }, { id: 'coin_balance_history', title: 'Coin balance history', - component: , + component: , }, config.chain.verificationType === 'validation' && addressTabsCountersQuery.data?.validations_count ? { id: 'blocks_validated', title: 'Blocks validated', count: addressTabsCountersQuery.data?.validations_count, - component: , + component: , } : undefined, addressTabsCountersQuery.data?.logs_count ? @@ -143,9 +145,10 @@ const AddressPageContent = () => { id: 'logs', title: 'Logs', count: addressTabsCountersQuery.data?.logs_count, - component: , + component: , } : undefined, + addressQuery.data?.is_contract ? { id: 'contract', title: () => { @@ -164,9 +167,7 @@ const AddressPageContent = () => { subTabs: contractTabs.map(tab => tab.id), } : undefined, ].filter(Boolean); - }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data ]); - - const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); + }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]); const tags = ( { /> ); - const content = (addressQuery.isError || addressQuery.isDegradedData) ? null : ; + const content = (addressQuery.isError || addressQuery.isDegradedData) ? + null : + ; const backLink = React.useMemo(() => { const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts'); @@ -250,10 +253,7 @@ const AddressPageContent = () => { { /* should stay before tabs to scroll up with pagination */ } - { (isLoading || addressTabsCountersQuery.isPlaceholderData) ? - : - content - } + { content } ); }; diff --git a/ui/pages/Block.tsx b/ui/pages/Block.tsx index 0c2478c649..abc6d71b2c 100644 --- a/ui/pages/Block.tsx +++ b/ui/pages/Block.tsx @@ -66,7 +66,7 @@ const BlockPageContent = () => { ), }, - blockQuery.data?.blob_tx_count ? + config.features.dataAvailability.isEnabled && blockQuery.data?.blob_tx_count ? { id: 'blob_txs', title: 'Blob txns', diff --git a/ui/pages/Marketplace.pw.tsx b/ui/pages/Marketplace.pw.tsx index 06a2e2cb85..bbf0605f57 100644 --- a/ui/pages/Marketplace.pw.tsx +++ b/ui/pages/Marketplace.pw.tsx @@ -1,86 +1,112 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { apps as appsMock } from 'mocks/apps/apps'; import { securityReports as securityReportsMock } from 'mocks/apps/securityReports'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { test, expect, devices } from 'playwright/lib'; import Marketplace from './Marketplace'; -const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; -const MARKETPLACE_SECURITY_REPORTS_URL = - app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', 'https://marketplace-security-reports.json') || ''; +const MARKETPLACE_CONFIG_URL = 'http://localhost/marketplace-config.json'; -const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg'))); }); -test('base view +@mobile +@dark-mode', async({ mount, page }) => { - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await Promise.all(appsMock.map(app => - page.route(app.logo, (route) => - route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }), - ), - )); - - const component = await mount( - - - , - ); +test('base view +@dark-mode', async({ render }) => { + const component = await render(); await expect(component).toHaveScreenshot(); }); -const testWithScoreFeature = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - { name: 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', value: MARKETPLACE_SECURITY_REPORTS_URL }, - { name: 'pw_feature:security_score_exp', value: 'true' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, +test('with featured app +@dark-mode', async({ render, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP', 'hop-exchange' ], + ]); + const component = await render(); + + await expect(component).toHaveScreenshot(); }); -testWithScoreFeature('with scores +@mobile +@dark-mode', async({ mount, page }) => { - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await page.route(MARKETPLACE_SECURITY_REPORTS_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(securityReportsMock), - })); - - await Promise.all(appsMock.map(app => - page.route(app.logo, (route) => - route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }), - ), - )); - - const component = await mount( - - - , - ); +test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => { + const MARKETPLACE_BANNER_CONTENT_URL = 'https://localhost/marketplace-banner.html'; + const MARKETPLACE_BANNER_LINK_URL = 'https://example.com'; + + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ], + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ], + ]); + await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true); + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + +test('with scores +@dark-mode', async({ render, mockConfigResponse, mockEnvs, mockFeatures }) => { + const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], + ]); + await mockFeatures([ + [ 'security_score_exp', true ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); + const component = await render(); await component.getByText('Apps scores').click(); await expect(component).toHaveScreenshot(); }); + +// I had a memory error while running tests in GH actions +// separate run for mobile tests fixes it +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + + test('base view', async({ render }) => { + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with featured app', async({ render, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP', 'hop-exchange' ], + ]); + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with banner', async({ render, mockEnvs, mockConfigResponse }) => { + const MARKETPLACE_BANNER_CONTENT_URL = 'https://localhost/marketplace-banner.html'; + const MARKETPLACE_BANNER_LINK_URL = 'https://example.com'; + + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ], + [ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ], + ]); + await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true); + const component = await render(); + + await expect(component).toHaveScreenshot(); + }); + + test('with scores', async({ render, mockConfigResponse, mockEnvs, mockFeatures }) => { + const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], + ]); + await mockFeatures([ + [ 'security_score_exp', true ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); + const component = await render(); + await component.getByText('Apps scores').click(); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 7a57b14c97..b310d89a5a 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -9,6 +9,7 @@ import config from 'configs/app'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useFeatureValue from 'lib/growthbook/useFeatureValue'; import useIsMobile from 'lib/hooks/useIsMobile'; +import Banner from 'ui/marketplace/Banner'; import ContractListModal from 'ui/marketplace/ContractListModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; @@ -20,7 +21,6 @@ import type { IconName } from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; import PageTitle from 'ui/shared/Page/PageTitle'; import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup'; -import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import useMarketplace from '../marketplace/useMarketplace'; @@ -55,6 +55,7 @@ const Marketplace = () => { filterQuery, onSearchInputChange, showAppInfo, + apps, displayedApps, selectedAppId, clearSelectedAppId, @@ -92,7 +93,7 @@ const Marketplace = () => { tabs.unshift({ id: MarketplaceCategory.FAVORITES, - title: () => , + title: () => , count: null, component: null, }); @@ -167,17 +168,24 @@ const Marketplace = () => { ) } /> + + + - { (isCategoriesPlaceholderData) ? ( - - ) : ( - - ) } + diff --git a/ui/pages/MarketplaceApp.pw.tsx b/ui/pages/MarketplaceApp.pw.tsx index b6b86599f2..08d1be9b6d 100644 --- a/ui/pages/MarketplaceApp.pw.tsx +++ b/ui/pages/MarketplaceApp.pw.tsx @@ -1,17 +1,11 @@ import { Flex } from '@chakra-ui/react'; -import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { apps as appsMock } from 'mocks/apps/apps'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; +import { test, expect, devices } from 'playwright/lib'; import MarketplaceApp from './MarketplaceApp'; -const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; - const hooksConfig = { router: { query: { id: appsMock[0].id }, @@ -19,39 +13,26 @@ const hooksConfig = { }, }; -const testFn: Parameters[1] = async({ mount, page }) => { - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await page.route(appsMock[0].url, (route) => - route.fulfill({ - status: 200, - path: './mocks/apps/app.html', - }), - ); +const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json'; + +const testFn: Parameters[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html'); - const component = await mount( - - { /* added Flex as a Layout because the iframe has negative margins */ } - - - - , + const component = await render( + + + , { hooksConfig }, ); await expect(component).toHaveScreenshot(); }; -const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, -}); - test('base view +@dark-mode', testFn); test.describe('mobile', () => { diff --git a/ui/pages/MarketplaceApp.tsx b/ui/pages/MarketplaceApp.tsx index c04ba4fecf..44f98d9de2 100644 --- a/ui/pages/MarketplaceApp.tsx +++ b/ui/pages/MarketplaceApp.tsx @@ -1,4 +1,4 @@ -import { Box, Center, useColorMode } from '@chakra-ui/react'; +import { Box, Center, useColorMode, Flex } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe'; import { useRouter } from 'next/router'; @@ -11,6 +11,7 @@ import { route } from 'nextjs-routes'; import config from 'configs/app'; import type { ResourceError } from 'lib/api/resources'; import useApiFetch from 'lib/api/useApiFetch'; +import { useMarketplaceContext } from 'lib/contexts/marketplace'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import useFetch from 'lib/hooks/useFetch'; import * as metadata from 'lib/metadata'; @@ -71,7 +72,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => { return (
{ (isFrameLoading) && ( @@ -129,6 +130,7 @@ const MarketplaceApp = () => { enabled: feature.isEnabled, }); const { data, isPending } = query; + const { setIsAutoConnectDisabled } = useMarketplaceContext(); useEffect(() => { if (data) { @@ -136,17 +138,17 @@ const MarketplaceApp = () => { { pathname: '/apps/[id]', query: { id: data.id } }, { app_name: data.title }, ); + setIsAutoConnectDisabled(!data.internalWallet); } - }, [ data ]); + }, [ data, setIsAutoConnectDisabled ]); throwOnResourceLoadError(query); return ( - <> + { > - + ); }; diff --git a/ui/pages/SearchResults.pw.tsx b/ui/pages/SearchResults.pw.tsx index f773fd56bb..3a492ad3ce 100644 --- a/ui/pages/SearchResults.pw.tsx +++ b/ui/pages/SearchResults.pw.tsx @@ -1,268 +1,184 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { apps as appsMock } from 'mocks/apps/apps'; import * as searchMock from 'mocks/search/index'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import SearchResults from './SearchResults'; test.describe('search by name ', () => { - const extendedTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: '' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - extendedTest('+@mobile +@dark-mode', async({ mount, page }) => { + test('+@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse, mockEnvs }) => { const hooksConfig = { router: { query: { q: 'o' }, }, }; - await page.route(buildApiUrl('search') + '?q=o', (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.token1, - searchMock.token2, - searchMock.contract1, - searchMock.address2, - searchMock.label1, - ], - }), - })); - await page.route(searchMock.token1.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ + searchMock.token1, + searchMock.token2, + searchMock.contract1, + searchMock.address2, + searchMock.label1, + ], + next_page_params: null, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'false' ], + ]); + await mockApiResponse('search', data, { queryParams: { q: 'o' } }); + await mockAssetResponse(searchMock.token1.icon_url as string, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); }); -test('search by address hash +@mobile', async({ mount, page }) => { +test('search by address hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.address1.address }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.address1.address }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.address1, - ], - }), - })); + const data = { + items: [ searchMock.address1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.address1.address } }); - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by block number +@mobile', async({ mount, page }) => { +test('search by block number +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: String(searchMock.block1.block_number) }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.block1, - searchMock.block2, - searchMock.block3, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.block1, searchMock.block2, searchMock.block3 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.block1.block_number } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by block hash +@mobile', async({ mount, page }) => { +test('search by block hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.block1.block_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.block1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.block1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.block1.block_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by tx hash +@mobile', async({ mount, page }) => { +test('search by tx hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.tx1.tx_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.tx1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.tx1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.tx1.tx_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -test('search by blob hash +@mobile', async({ mount, page }) => { +test('search by blob hash +@mobile', async({ render, mockApiResponse }) => { const hooksConfig = { router: { query: { q: searchMock.blob1.blob_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.blob1.blob_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.blob1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.blob1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.blob1.blob_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); -const testWithUserOps = test.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.userOps) as any, +test('search by domain name +@mobile', async({ render, mockApiResponse }) => { + const hooksConfig = { + router: { + query: { q: searchMock.domain1.ens_info.name }, + }, + }; + const data = { + items: [ searchMock.domain1 ], + next_page_params: null, + }; + await mockApiResponse('search', data, { queryParams: { q: searchMock.domain1.ens_info.name } }); + const component = await render(, { hooksConfig }); + await expect(component.locator('main')).toHaveScreenshot(); }); -testWithUserOps('search by user op hash +@mobile', async({ mount, page }) => { +test('search by user op hash +@mobile', async({ render, mockApiResponse, mockEnvs }) => { const hooksConfig = { router: { query: { q: searchMock.userOp1.user_operation_hash }, }, }; - await page.route(buildApiUrl('search') + `?q=${ searchMock.userOp1.user_operation_hash }`, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.userOp1, - ], - }), - })); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.userOp1 ], + next_page_params: null, + }; + await mockEnvs(ENVS_MAP.userOps); + await mockApiResponse('search', data, { queryParams: { q: searchMock.userOp1.user_operation_hash } }); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); test.describe('with apps', () => { - const MARKETPLACE_CONFIG_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || ''; - const extendedTest = test.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - extendedTest('default view +@mobile', async({ mount, page }) => { + test('default view +@mobile', async({ render, mockApiResponse, mockConfigResponse, mockAssetResponse, mockEnvs }) => { + const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json'; const hooksConfig = { router: { query: { q: 'o' }, }, }; - const API_URL = buildApiUrl('search') + '?q=o'; - await page.route(API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ - items: [ - searchMock.token1, - ], - next_page_params: { foo: 'bar' }, - }), - })); - - await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(appsMock), - })); - - await page.route(appsMock[0].logo, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - await page.route(appsMock[1].logo as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + const data = { + items: [ searchMock.token1 ], + next_page_params: { + address_hash: null, + block_hash: null, + holder_count: null, + inserted_at: null, + item_type: 'token' as const, + items_count: 1, + name: 'foo', + q: 'o', + tx_hash: null, + }, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], + [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + ]); + await mockApiResponse('search', data, { queryParams: { q: 'o' } }); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg'); + await mockAssetResponse(appsMock[1].logo, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }); await expect(component.locator('main')).toHaveScreenshot(); }); diff --git a/ui/pages/ShibariumDeposits.pw.tsx b/ui/pages/ShibariumDeposits.pw.tsx index 75fa82ba5e..d4b532aac4 100644 --- a/ui/pages/ShibariumDeposits.pw.tsx +++ b/ui/pages/ShibariumDeposits.pw.tsx @@ -1,43 +1,17 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as depositsData } from 'mocks/shibarium/deposits'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; import ShibariumDeposits from './ShibariumDeposits'; -const DEPOSITS_API_URL = buildApiUrl('shibarium_deposits'); -const DEPOSITS_COUNT_API_URL = buildApiUrl('shibarium_deposits_count'); +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.shibariumRollup); + await mockApiResponse('shibarium_deposits', depositsData); + await mockApiResponse('shibarium_deposits_count', 3971111); -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.shibariumRollup) as any, -}); - -test('base view +@mobile', async({ mount, page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(DEPOSITS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(depositsData), - })); - - await page.route(DEPOSITS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '3971111', - })); - - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/pages/ShibariumWithdrawals.pw.tsx b/ui/pages/ShibariumWithdrawals.pw.tsx index 98d3ff3836..ff7ecb24d1 100644 --- a/ui/pages/ShibariumWithdrawals.pw.tsx +++ b/ui/pages/ShibariumWithdrawals.pw.tsx @@ -1,47 +1,22 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; import { data as withdrawalsData } from 'mocks/shibarium/withdrawals'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; -import * as configs from 'playwright/utils/configs'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import { test, expect } from 'playwright/lib'; -import ShibariuWithdrawals from './ShibariumWithdrawals'; +import ShibariumWithdrawals from './ShibariumWithdrawals'; -const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.shibariumRollup) as any, -}); - -const WITHDRAWALS_API_URL = buildApiUrl('shibarium_withdrawals'); -const WITHDRAWALS_COUNT_API_URL = buildApiUrl('shibarium_withdrawals_count'); - -test('base view +@mobile', async({ mount, page }) => { +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { // test on mobile is flaky // my assumption is there is not enough time to calculate hashes truncation so component is unstable // so I raised the test timeout to check if it helps test.slow(); - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(withdrawalsData), - })); - await page.route(WITHDRAWALS_COUNT_API_URL, (route) => route.fulfill({ - status: 200, - body: '397', - })); + await mockEnvs(ENVS_MAP.shibariumRollup); + await mockApiResponse('shibarium_withdrawals', withdrawalsData); + await mockApiResponse('shibarium_withdrawals_count', 397); - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot({ timeout: 10_000 }); }); diff --git a/ui/pages/Stats.tsx b/ui/pages/Stats.tsx index 228226df27..d419ee3c52 100644 --- a/ui/pages/Stats.tsx +++ b/ui/pages/Stats.tsx @@ -21,6 +21,7 @@ const Stats = () => { handleFilterChange, displayedCharts, filterQuery, + initialFilterQuery, } = useStats(); return ( @@ -33,6 +34,8 @@ const Stats = () => { { ({ - createSocket: socketServer.createSocket, -}); - // FIXME // test cases which use socket cannot run in parallel since the socket server always run on the same port test.describe.configure({ mode: 'serial' }); -test.beforeEach(async({ page }) => { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(TOKEN_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenInfo), - })); - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contract), - })); - await page.route(TOKEN_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenCounters), - })); - await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({}), - })); +test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('token', tokenInfo, { pathParams: { hash: '1' } }); + await mockApiResponse('address', contract, { pathParams: { hash: '1' } }); + await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } }); + await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } }); }); -test('base view', async({ mount, page, createSocket }) => { - const component = await mount( - - - , - { hooksConfig }, - ); +test('base view', async({ render, page, createSocket }) => { + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'tokens:1'); @@ -73,24 +41,11 @@ test('base view', async({ mount, page, createSocket }) => { }); }); -test('with verified info', async({ mount, page, createSocket }) => { - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); +test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); - const component = await mount( - - - , - { hooksConfig }, - ); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'tokens:1'); @@ -104,55 +59,16 @@ test('with verified info', async({ mount, page, createSocket }) => { }); }); -const bridgedTokenTest = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.bridgedTokens) as any, - createSocket: socketServer.createSocket, -}); - -bridgedTokenTest('bridged token', async({ mount, page, createSocket }) => { - - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: '', - })); - - await page.route(TOKEN_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedTokenA), - })); - await page.route(ADDRESS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(contract), - })); - await page.route(TOKEN_COUNTERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(tokenCounters), - })); - await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({}), - })); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); +test('bridged token', async({ render, page, createSocket, mockApiResponse, mockAssetResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.bridgedTokens); + await mockApiResponse('token', bridgedTokenA, { pathParams: { hash: '1' } }); + await mockApiResponse('address', contract, { pathParams: { hash: '1' } }); + await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } }); + await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } }); + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'tokens:1'); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); @@ -165,14 +81,9 @@ bridgedTokenTest('bridged token', async({ mount, page, createSocket }) => { test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test('base view', async({ mount, page, createSocket }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('base view', async({ render, page, createSocket }) => { + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'tokens:1'); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); @@ -183,25 +94,11 @@ test.describe('mobile', () => { }); }); - test('with verified info', async({ mount, page, createSocket }) => { - const VERIFIED_INFO_URL = buildApiUrl('token_verified_info', { chainId: '1', hash: '1' }); - await page.route(VERIFIED_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED), - })); - await page.route(tokenInfo.icon_url as string, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); - - const component = await mount( - - - , - { hooksConfig }, - ); + test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { + await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); + await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, 'tokens:1'); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); diff --git a/ui/pages/TokenInstance.tsx b/ui/pages/TokenInstance.tsx index 6471e822b9..174ba5afc6 100644 --- a/ui/pages/TokenInstance.tsx +++ b/ui/pages/TokenInstance.tsx @@ -174,20 +174,34 @@ const TokenInstanceContent = () => { pagination = holdersQuery.pagination; } + const title = (() => { + if (typeof tokenInstanceQuery.data?.metadata?.name === 'string') { + return tokenInstanceQuery.data.metadata.name; + } + + if (tokenQuery.data?.symbol) { + return (tokenQuery.data.name || tokenQuery.data.symbol) + ' #' + tokenInstanceQuery.data?.id; + } + + return `ID ${ tokenInstanceQuery.data?.id }`; + })(); + const titleSecondRow = ( - + { tokenQuery.data && ( + + ) } { !isLoading && tokenInstanceQuery.data && } @@ -199,7 +213,7 @@ const TokenInstanceContent = () => { <> { - await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(textAdMock.duck), - })); - await page.route(textAdMock.duck.ad.thumbnail, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_s.jpg', - }); - }); -}); - -base('base view +@mobile +@dark-mode', async({ mount, page }) => { +test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { const allTokens = { items: [ tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d, @@ -35,6 +18,7 @@ base('base view +@mobile +@dark-mode', async({ mount, page }) => { holder_count: 1, items_count: 1, name: 'a', + market_cap: '0', }, }; const filteredTokens = { @@ -44,40 +28,25 @@ base('base view +@mobile +@dark-mode', async({ mount, page }) => { next_page_params: null, }; - const ALL_TOKENS_API_URL = buildApiUrl('tokens'); - const FILTERED_TOKENS_API_URL = buildApiUrl('tokens') + '?q=foo'; - - await page.route(ALL_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(allTokens), - })); - - await page.route(FILTERED_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(filteredTokens), - })); + await mockApiResponse('tokens', allTokens); + await mockApiResponse('tokens', filteredTokens, { queryParams: { q: 'foo' } }); - const component = await mount( - + const component = await render( +
- , +
, ); await expect(component).toHaveScreenshot(); await component.getByRole('textbox', { name: 'Token name or symbol' }).focus(); - await component.getByRole('textbox', { name: 'Token name or symbol' }).type('foo'); + await component.getByRole('textbox', { name: 'Token name or symbol' }).fill('foo'); await expect(component).toHaveScreenshot(); }); -base.describe('bridged tokens', async() => { - const test = base.extend({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: contextWithEnvs(configs.featureEnvs.bridgedTokens) as any, - }); - +test.describe('bridged tokens', async() => { const bridgedTokens = { items: [ tokens.bridgedTokenA, @@ -88,6 +57,7 @@ base.describe('bridged tokens', async() => { holder_count: 1, items_count: 1, name: 'a', + market_cap: null, }, }; const bridgedFilteredTokens = { @@ -101,26 +71,17 @@ base.describe('bridged tokens', async() => { query: { tab: 'bridged' }, }, }; - const BRIDGED_TOKENS_API_URL = buildApiUrl('tokens_bridged'); - - test.beforeEach(async({ page }) => { - await page.route(BRIDGED_TOKENS_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedTokens), - })); - }); - test('base view', async({ mount, page }) => { - await page.route(BRIDGED_TOKENS_API_URL + '?chain_ids=99', (route) => route.fulfill({ - status: 200, - body: JSON.stringify(bridgedFilteredTokens), - })); + test('base view', async({ render, page, mockApiResponse, mockEnvs }) => { + await mockEnvs(ENVS_MAP.bridgedTokens); + await mockApiResponse('tokens_bridged', bridgedTokens); + await mockApiResponse('tokens_bridged', bridgedFilteredTokens, { queryParams: { chain_ids: '99' } }); - const component = await mount( - + const component = await render( +
- , +
, { hooksConfig }, ); diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index 8e14c1d6cf..e93fafb237 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -63,7 +63,7 @@ const TransactionPageContent = () => { { id: 'user_ops', title: 'User operations', component: } : undefined, { id: 'internal', title: 'Internal txns', component: }, - txQuery.data?.blob_versioned_hashes?.length ? + config.features.dataAvailability.isEnabled && txQuery.data?.blob_versioned_hashes?.length ? { id: 'blobs', title: 'Blobs', component: } : undefined, { id: 'logs', title: 'Logs', component: }, diff --git a/ui/pages/Transactions.tsx b/ui/pages/Transactions.tsx index 4517b4d985..23a3566c5d 100644 --- a/ui/pages/Transactions.tsx +++ b/ui/pages/Transactions.tsx @@ -14,6 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; +import TxsStats from 'ui/txs/TxsStats'; import TxsWatchlist from 'ui/txs/TxsWatchlist'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting'; @@ -29,12 +30,6 @@ const Transactions = () => { const isMobile = useIsMobile(); const tab = getQueryParamString(router.query.tab); - React.useEffect(() => { - if (tab === 'blob_txs' && !config.features.dataAvailability.isEnabled) { - router.replace({ pathname: '/txs' }, undefined, { shallow: true }); - } - }, [ router, tab ]); - const txsValidatedQuery = useQueryWithPages({ resourceName: 'txs_validated', filters: { filter: 'validated' }, @@ -146,6 +141,7 @@ const Transactions = () => { return ( <> + ({ - context: contextWithEnvs(configs.featureEnvs.validators), -}); +const chainType = 'stability'; -test('base view +@mobile', async({ render, mockApiResponse }) => { - await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType: 'stability' } }); - await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType: 'stability' } }); +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ], + ]); + await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } }); + await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } }); const component = await render(); diff --git a/ui/pages/VerifiedAddresses.pw.tsx b/ui/pages/VerifiedAddresses.pw.tsx index 4b64d5e23b..6a147c6cc0 100644 --- a/ui/pages/VerifiedAddresses.pw.tsx +++ b/ui/pages/VerifiedAddresses.pw.tsx @@ -1,17 +1,15 @@ +import type { BrowserContext } from '@playwright/test'; import React from 'react'; import * as mocks from 'mocks/account/verifiedAddresses'; import * as profileMock from 'mocks/user/profile'; -import authFixture from 'playwright/fixtures/auth'; +import { contextWithAuth } from 'playwright/fixtures/auth'; import { test as base, expect } from 'playwright/lib'; import VerifiedAddresses from './VerifiedAddresses'; -const test = base.extend({ - context: ({ context }, use) => { - authFixture(context); - use(context); - }, +const test = base.extend<{ context: BrowserContext }>({ + context: contextWithAuth, }); test.beforeEach(async({ mockAssetResponse }) => { diff --git a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png index e2ed79ae58..894038a81c 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png index 7fb3bdf096..8356835e2e 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png index d5c4e1f321..655a4ad4a5 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 442d62ab16..e25ba73da9 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png deleted file mode 100644 index 124c2b86d6..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png new file mode 100644 index 0000000000..fceedc3185 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png new file mode 100644 index 0000000000..5ae62491ab Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png new file mode 100644 index 0000000000..cda2818ef2 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-mobile-dark-mode-1.png deleted file mode 100644 index 21e51e79fd..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png index 575ce38e53..17c54d8959 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-mobile-dark-mode-1.png deleted file mode 100644 index ab79db5a10..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..fd0ab1cb5f Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png new file mode 100644 index 0000000000..fbb38d03af Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png new file mode 100644 index 0000000000..35245e7d9f Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png new file mode 100644 index 0000000000..6aa6e1999e Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png new file mode 100644 index 0000000000..d1b2888b75 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png new file mode 100644 index 0000000000..c6f93f138e Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png new file mode 100644 index 0000000000..aedf4aa291 Binary files /dev/null and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-mobile-dark-mode-1.png deleted file mode 100644 index 85c7dea12e..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_base-view-mobile-dark-mode-1.png deleted file mode 100644 index 82585aee4f..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_with-scores-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_with-scores-mobile-dark-mode-1.png deleted file mode 100644 index 5d8c323fe8..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_mobile_with-scores-mobile-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 8f62ca6d83..3ebdbba8ef 100644 Binary files a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png index ce2c911b14..4bb6f375b8 100644 Binary files a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png index f03dd57c1e..de5e5f6786 100644 Binary files a/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/MarketplaceApp.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png index 216d8daf6b..d135f542ad 100644 Binary files a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png and b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png index bfeb78ed07..d37225d13f 100644 Binary files a/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png and b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png index 17cc006cc0..2c4926aec8 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-block-number-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..4a83be499a Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-domain-name-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png index 95c8ef3f0a..ab3db505a4 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-block-number-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png new file mode 100644 index 0000000000..044408cac3 Binary files /dev/null and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-domain-name-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png index 7cab3d682e..a7b79f5741 100644 Binary files a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png index 0335fb3110..7ac99bfd32 100644 Binary files a/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumDeposits.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png index 47e093f593..457bdbb859 100644 Binary files a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png index 9f9063b226..a56071f28e 100644 Binary files a/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ShibariumWithdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png index e8e194d881..3275b0e094 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png index 5873ca0484..f04e9f2426 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png index bf6ba7e403..4c799af178 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png index d27cd36fc0..0ca82751f1 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_mobile-with-verified-info-1.png differ diff --git a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png index 94ca737987..83e9b9e25c 100644 Binary files a/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png and b/ui/pages/__screenshots__/Token.pw.tsx_default_with-verified-info-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index 9ca4d23064..1e3f0939a6 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png index 06a1553f0d..d2915eb093 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png index c7e5cb7e25..3e5ce0641c 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png index 2624b74d24..b7a34b56f7 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png index 0dad636021..4109b922c1 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png index f2f79d6bcb..96e6b133eb 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_default_bridged-tokens-base-view-2.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index a5d0f10c5b..8d91310520 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png index 5dee58eb04..9c2ea5a88a 100644 Binary files a/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png and b/ui/pages/__screenshots__/Tokens.pw.tsx_mobile_base-view-mobile-dark-mode-2.png differ diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png b/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png index eb4f533595..876c1c9b52 100644 Binary files a/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png and b/ui/pages/__screenshots__/UserOp.pw.tsx_default_base-view-1.png differ diff --git a/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png index fbe5973dc2..172f7d7acc 100644 Binary files a/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/UserOp.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png index b17c113b1b..50ac818186 100644 Binary files a/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/UserOps.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png index 3f7ecc7900..1c58075243 100644 Binary files a/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/UserOps.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png index 23bfad3f62..4ac12cde51 100644 Binary files a/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png index 7b0bf0c8b9..554bdc3c23 100644 Binary files a/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png index 6d51e2adf0..3766679027 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png index 1d1ba2a370..0e038dec5d 100644 Binary files a/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkEvmL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png index c61872abb9..6d14c9d8d0 100644 Binary files a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png index a43ee77aaf..a12fa458f1 100644 Binary files a/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/ZkSyncL2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index 28b1dc0eac..d09cb77983 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -14,6 +14,7 @@ import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; @@ -243,6 +244,30 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'ens_domain': { + return ( + + + + + + + ); + } } })(); @@ -334,6 +359,21 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ) : null; } + case 'ens_domain': { + const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; + return ( + + + + + { + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + } + + ); + } default: return null; diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index fde08af038..d9a1cf6d74 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -14,6 +14,7 @@ import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import * as EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; @@ -337,6 +338,48 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ); } + + case 'ens_domain': { + const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; + return ( + <> + + + + + + + + + + + + + + { data.is_smart_contract_verified && } + + + + { data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } } + + + ); + } } })(); diff --git a/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx b/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx index 614764950c..814b380a81 100644 --- a/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx +++ b/ui/shared/AccountActionsMenu/AccountActionsMenu.pw.tsx @@ -1,20 +1,14 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; +import type { BrowserContext } from '@playwright/test'; import React from 'react'; import * as profileMock from 'mocks/user/profile'; -import authFixture from 'playwright/fixtures/auth'; -import TestApp from 'playwright/TestApp'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test as base, expect } from 'playwright/lib'; import AccountActionsMenu from './AccountActionsMenu'; -const USER_INFO_URL = buildApiUrl('user_info'); - -const test = base.extend({ - context: ({ context }, use) => { - authFixture(context); - use(context); - }, +const test = base.extend<{ context: BrowserContext }>({ + context: contextWithAuth, }); test.use({ viewport: { width: 200, height: 200 } }); @@ -28,56 +22,32 @@ test.describe('with multiple items', async() => { }, }; - test.beforeEach(async({ page }) => { - await page.route(USER_INFO_URL, (route) => route.fulfill({ - body: JSON.stringify(profileMock.base), - })); + test.beforeEach(async({ mockApiResponse }) => { + mockApiResponse('user_info', profileMock.base); }); - test('base view', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').click(); await expect(page).toHaveScreenshot(); }); - test('base view with styles', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view with styles', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').click(); await expect(page).toHaveScreenshot(); }); - test('loading', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); - test('loading with styles', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading with styles', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); @@ -92,39 +62,22 @@ test.describe('with one item', async() => { }, }; - test('base view', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').hover(); await expect(page).toHaveScreenshot(); }); - test('base view with styles', async({ mount, page }) => { - const component = await mount( - - - , - { hooksConfig }, - ); - + test('base view with styles', async({ render, page }) => { + const component = await render(, { hooksConfig }); await component.getByRole('button').hover(); await expect(page).toHaveScreenshot(); }); - test('loading', async({ mount }) => { - const component = await mount( - - - , - { hooksConfig }, - ); + test('loading', async({ render }) => { + const component = await render(, { hooksConfig }); await expect(component).toHaveScreenshot(); }); diff --git a/ui/shared/NetworkExplorers.tsx b/ui/shared/NetworkExplorers.tsx index 6671a07274..200341f87e 100644 --- a/ui/shared/NetworkExplorers.tsx +++ b/ui/shared/NetworkExplorers.tsx @@ -20,6 +20,7 @@ import config from 'configs/app'; import stripTrailingSlash from 'lib/stripTrailingSlash'; import IconSvg from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; +import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip'; interface Props { className?: string; @@ -55,26 +56,27 @@ const NetworkExplorers = ({ className, type, pathParam }: Props) => { return ( - + + + diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png index d633cbb80a..479736d81a 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_default-view-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png index 8d02d7da7c..1284ee5f7b 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-long-name-and-many-tags-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png index 9a03708d13..d55d170ab8 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_default_with-text-ad-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png index efabd99eed..39ea00f5a9 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_default-view-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png index f2b21cc103..d7fd28ebf2 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-long-name-and-many-tags-mobile-1.png differ diff --git a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png index 01d7d12f52..389d966a56 100644 Binary files a/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png and b/ui/shared/Page/__screenshots__/PageTitle.pw.tsx_mobile_with-text-ad-mobile-1.png differ diff --git a/ui/shared/PopoverTriggerTooltip.tsx b/ui/shared/PopoverTriggerTooltip.tsx new file mode 100644 index 0000000000..b0ed7ab91e --- /dev/null +++ b/ui/shared/PopoverTriggerTooltip.tsx @@ -0,0 +1,30 @@ +import { Skeleton, Tooltip, chakra } from '@chakra-ui/react'; +import React from 'react'; + +import useIsMobile from 'lib/hooks/useIsMobile'; + +type Props = { + label: string; + isLoading?: boolean; + className?: string; + children: React.ReactNode; +} + +const PopoverTriggerTooltip = ({ label, isLoading, className, children }: Props, ref: React.ForwardedRef) => { + const isMobile = useIsMobile(); + return ( + // tooltip need to be wrapped in div for proper popover positioning + + + { children } + + + ); +}; + +export default chakra(React.forwardRef(PopoverTriggerTooltip)); diff --git a/ui/shared/Tabs/AdaptiveTabsList.tsx b/ui/shared/Tabs/AdaptiveTabsList.tsx index 5f5ff81f1f..de94bcc8d6 100644 --- a/ui/shared/Tabs/AdaptiveTabsList.tsx +++ b/ui/shared/Tabs/AdaptiveTabsList.tsx @@ -1,5 +1,5 @@ import type { StyleProps, ThemingProps } from '@chakra-ui/react'; -import { Box, Tab, TabList, useColorModeValue } from '@chakra-ui/react'; +import { Box, Skeleton, Tab, TabList, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; import { useScrollDirection } from 'lib/contexts/scrollDirection'; @@ -24,6 +24,7 @@ interface Props extends TabsProps { activeTabIndex: number; onItemClick: (index: number) => void; themeProps: ThemingProps<'Tabs'>; + isLoading?: boolean; } const AdaptiveTabsList = (props: Props) => { @@ -113,8 +114,10 @@ const AdaptiveTabsList = (props: Props) => { }, }} > - { typeof tab.title === 'function' ? tab.title() : tab.title } - + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + ); }) } diff --git a/ui/shared/Tabs/RoutedTabs.tsx b/ui/shared/Tabs/RoutedTabs.tsx index a57b68f222..cfc7036bd3 100644 --- a/ui/shared/Tabs/RoutedTabs.tsx +++ b/ui/shared/Tabs/RoutedTabs.tsx @@ -17,9 +17,10 @@ interface Props extends ThemingProps<'Tabs'> { stickyEnabled?: boolean; className?: string; onTabChange?: (index: number) => void; + isLoading?: boolean; } -const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, ...themeProps }: Props) => { +const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabled, className, onTabChange, isLoading, ...themeProps }: Props) => { const router = useRouter(); const tabIndex = useTabIndexFromQuery(tabs); const tabsRef = useRef(null); @@ -63,6 +64,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, rightSlotProps, stickyEnabl stickyEnabled={ stickyEnabled } onTabChange={ handleTabChange } defaultTabIndex={ tabIndex } + isLoading={ isLoading } { ...themeProps } /> ); diff --git a/ui/shared/Tabs/TabsWithScroll.tsx b/ui/shared/Tabs/TabsWithScroll.tsx index f09644b675..5c2f2b39ce 100644 --- a/ui/shared/Tabs/TabsWithScroll.tsx +++ b/ui/shared/Tabs/TabsWithScroll.tsx @@ -25,6 +25,7 @@ export interface Props extends ThemingProps<'Tabs'> { stickyEnabled?: boolean; onTabChange?: (index: number) => void; defaultTabIndex?: number; + isLoading?: boolean; className?: string; } @@ -37,6 +38,7 @@ const TabsWithScroll = ({ stickyEnabled, onTabChange, defaultTabIndex, + isLoading, className, ...themeProps }: Props) => { @@ -101,6 +103,7 @@ const TabsWithScroll = ({ activeTabIndex={ activeTabIndex } onItemClick={ handleTabChange } themeProps={ themeProps } + isLoading={ isLoading } /> { tabsList.map((tab) => { tab.component }) } diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png index 523dbce1e9..c031b777c1 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png index ccd697846d..3c2a6ee5a2 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png index c372d67923..725d066269 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png index 4a5f12f4e8..f55a41ca6d 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/UserAvatar.tsx b/ui/shared/UserAvatar.tsx index 818eaceda7..3a75d56105 100644 --- a/ui/shared/UserAvatar.tsx +++ b/ui/shared/UserAvatar.tsx @@ -8,9 +8,10 @@ import IconSvg from 'ui/shared/IconSvg'; interface Props { size: number; + fallbackIconSize?: number; } -const UserAvatar = ({ size }: Props) => { +const UserAvatar = ({ size, fallbackIconSize = 20 }: Props) => { const appProps = useAppContext(); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const [ isImageLoadError, setImageLoadError ] = React.useState(false); @@ -34,7 +35,7 @@ const UserAvatar = ({ size }: Props) => { boxSize={ `${ size }px` } borderRadius="full" overflow="hidden" - fallback={ isImageLoadError || !data?.avatar ? : undefined } + fallback={ isImageLoadError || !data?.avatar ? : undefined } onError={ handleImageLoadError } /> ); diff --git a/ui/shared/address/AddressFromTo.tsx b/ui/shared/address/AddressFromTo.tsx index 5024c657da..0a5d0efbd3 100644 --- a/ui/shared/address/AddressFromTo.tsx +++ b/ui/shared/address/AddressFromTo.tsx @@ -80,7 +80,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading const iconSize = 20; return ( - + { +const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText }: Props) => { const ref = useRef(null); const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); @@ -134,7 +135,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, if (!hasItems) { return (
- No data + { emptyText || 'No data' }
); } diff --git a/ui/shared/gas/GasInfoTooltipRow.tsx b/ui/shared/gas/GasInfoTooltipRow.tsx index 4df03189ac..3aae5220fb 100644 --- a/ui/shared/gas/GasInfoTooltipRow.tsx +++ b/ui/shared/gas/GasInfoTooltipRow.tsx @@ -16,7 +16,7 @@ const GasInfoTooltipRow = ({ name, info }: Props) => { <> { name } - { info && info.time && ( + { info && typeof info.time === 'number' && info.time > 0 && ( { space }{ (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx index 8eaebf342a..515c79bbcb 100644 --- a/ui/shared/layout/LayoutApp.tsx +++ b/ui/shared/layout/LayoutApp.tsx @@ -3,30 +3,33 @@ import React from 'react'; import type { Props } from './types'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; -import HeaderDesktop from 'ui/snippets/header/HeaderDesktop'; import HeaderMobile from 'ui/snippets/header/HeaderMobile'; import * as Layout from './components'; const LayoutDefault = ({ children }: Props) => { return ( - + - - + + - - + { children } - ); }; diff --git a/ui/shared/layout/LayoutHome.tsx b/ui/shared/layout/LayoutHome.tsx index 6274afeeea..b6786f2339 100644 --- a/ui/shared/layout/LayoutHome.tsx +++ b/ui/shared/layout/LayoutHome.tsx @@ -12,7 +12,7 @@ const LayoutHome = ({ children }: Props) => { return ( - + { +const Container = ({ children, className }: Props) => { return ( - + { children } ); }; -export default React.memo(Container); +export default React.memo(chakra(Container)); diff --git a/ui/shared/layout/components/MainArea.tsx b/ui/shared/layout/components/MainArea.tsx index 804577425f..0c92777246 100644 --- a/ui/shared/layout/components/MainArea.tsx +++ b/ui/shared/layout/components/MainArea.tsx @@ -1,16 +1,17 @@ -import { Flex } from '@chakra-ui/react'; +import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; interface Props { children: React.ReactNode; + className?: string; } -const MainArea = ({ children }: Props) => { +const MainArea = ({ children, className }: Props) => { return ( - + { children } ); }; -export default React.memo(MainArea); +export default React.memo(chakra(MainArea)); diff --git a/ui/shared/search/utils.ts b/ui/shared/search/utils.ts index c24acc63b9..eff4ac90b0 100644 --- a/ui/shared/search/utils.ts +++ b/ui/shared/search/utils.ts @@ -3,7 +3,7 @@ import type { MarketplaceAppOverview } from 'types/client/marketplace'; import config from 'configs/app'; -export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block' | 'user_operation' | 'blob'; +export type ApiCategory = 'token' | 'nft' | 'address' | 'public_tag' | 'transaction' | 'block' | 'user_operation' | 'blob' | 'domain'; export type Category = ApiCategory | 'app'; export type ItemsCategoriesMap = @@ -17,6 +17,7 @@ export type SearchResultAppItem = { export const searchCategories: Array<{id: Category; title: string }> = [ { id: 'app', title: 'DApps' }, + { id: 'domain', title: 'Names' }, { id: 'token', title: 'Tokens (ERC-20)' }, { id: 'nft', title: 'NFTs (ERC-721 & 1155)' }, { id: 'address', title: 'Addresses' }, @@ -32,6 +33,7 @@ if (config.features.userOps.isEnabled) { export const searchItemTitles: Record = { app: { itemTitle: 'DApp', itemTitleShort: 'App' }, + domain: { itemTitle: 'Name', itemTitleShort: 'Name' }, token: { itemTitle: 'Token', itemTitleShort: 'Token' }, nft: { itemTitle: 'NFT', itemTitleShort: 'NFT' }, address: { itemTitle: 'Address', itemTitleShort: 'Address' }, @@ -72,5 +74,8 @@ export function getItemCategory(item: SearchResultItem | SearchResultAppItem): C case 'blob': { return 'blob'; } + case 'ens_domain': { + return 'domain'; + } } } diff --git a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx index d5c43f107f..d278c38ec8 100644 --- a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx +++ b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx @@ -1,8 +1,9 @@ -import { Button, Skeleton } from '@chakra-ui/react'; +import { Button } from '@chakra-ui/react'; import React from 'react'; import IconSvg from 'ui/shared/IconSvg'; +import PopoverTriggerTooltip from '../PopoverTriggerTooltip'; import useScoreLevelAndColor from './useScoreLevelAndColor'; interface Props { @@ -12,19 +13,19 @@ interface Props { height?: string; onlyIcon?: boolean; onClick?: () => void; + label?: string; } const SolidityscanReportButton = ( - { className, score, isLoading, height = '32px', onlyIcon, onClick }: Props, + { className, score, isLoading, height = '32px', onlyIcon, onClick, label = 'Security score' }: Props, ref: React.ForwardedRef, ) => { const { scoreColor } = useScoreLevelAndColor(score); return ( - + - + ); }; diff --git a/ui/shared/stats/StatsWidget.pw.tsx b/ui/shared/stats/StatsWidget.pw.tsx new file mode 100644 index 0000000000..cc9fad8da4 --- /dev/null +++ b/ui/shared/stats/StatsWidget.pw.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +import StatsWidget from './StatsWidget'; + +test.use({ viewport: { width: 300, height: 100 } }); + +test('with positive diff +@dark-mode', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +// according to current logic we don't show diff if it's negative +test('with negative diff', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('loading state', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('with period only', async({ render }) => { + const component = await render( + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/stats/StatsWidget.tsx b/ui/shared/stats/StatsWidget.tsx index 53dcbbd312..077dd1c4e4 100644 --- a/ui/shared/stats/StatsWidget.tsx +++ b/ui/shared/stats/StatsWidget.tsx @@ -1,66 +1,94 @@ -import { Box, Flex, Text, Skeleton, useColorModeValue } from '@chakra-ui/react'; +import { Box, Flex, Text, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react'; +import NextLink from 'next/link'; import React from 'react'; +import type { Route } from 'nextjs-routes'; + import Hint from 'ui/shared/Hint'; +import TruncatedValue from 'ui/shared/TruncatedValue'; type Props = { label: string; value: string; + valuePrefix?: string; + valuePostfix?: string; hint?: string; isLoading?: boolean; diff?: string | number; diffFormatted?: string; diffPeriod?: '24h'; + period?: '1h' | '24h'; + href?: Route; } -const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', diffFormatted }: Props) => { +const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => { + if (href) { + return ( + + { children } + + ); + } + + return children; +}; + +const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, diff, diffPeriod = '24h', diffFormatted, period, href }: Props) => { const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100'); const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const hintColor = useColorModeValue('gray.600', 'gray.400'); return ( - - - - { label } - - - { value } - { diff && Number(diff) > 0 && ( - <> - - +{ diffFormatted || Number(diff).toLocaleString() } - - ({ diffPeriod }) - - ) } - - - { hint && ( - - - - ) } - + + + + + { label } + + + { valuePrefix && { valuePrefix } } + + { valuePostfix && { valuePostfix } } + { diff && Number(diff) > 0 && ( + <> + + +{ diffFormatted || Number(diff).toLocaleString() } + + ({ diffPeriod }) + + ) } + { period && ({ period }) } + + + { hint && ( + + + + ) } + + ); }; diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png new file mode 100644 index 0000000000..72405549ae Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_dark-color-mode_with-positive-diff-dark-mode-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png new file mode 100644 index 0000000000..2e0a87888e Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_loading-state-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png new file mode 100644 index 0000000000..ce20bc6d5c Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-negative-diff-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png new file mode 100644 index 0000000000..0aedb396c2 Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-period-only-1.png differ diff --git a/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png new file mode 100644 index 0000000000..11389c31ff Binary files /dev/null and b/ui/shared/stats/__screenshots__/StatsWidget.pw.tsx_default_with-positive-diff-dark-mode-1.png differ diff --git a/ui/shared/statusTag/StatusTag.tsx b/ui/shared/statusTag/StatusTag.tsx index fb9d59a97d..dc2feebeba 100644 --- a/ui/shared/statusTag/StatusTag.tsx +++ b/ui/shared/statusTag/StatusTag.tsx @@ -38,7 +38,7 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => { return ( - + { text } diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png index a5ab415a85..42b5ac17f2 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_error-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png index 3224ee3df2..24dd6061e8 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_ok-status-1.png differ diff --git a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png index 1e4454c4ea..b9add0a99c 100644 Binary files a/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png and b/ui/shared/statusTag/__screenshots__/StatusTag.pw.tsx_default_pending-status-1.png differ diff --git a/ui/snippets/footer/Footer.pw.tsx b/ui/snippets/footer/Footer.pw.tsx index a3dc45f5c4..3b8498e3b2 100644 --- a/ui/snippets/footer/Footer.pw.tsx +++ b/ui/snippets/footer/Footer.pw.tsx @@ -1,55 +1,28 @@ -import { test as base, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import type { WalletProvider } from 'types/web3'; - -import { buildExternalAssetFilePath } from 'configs/app/utils'; import { FOOTER_LINKS } from 'mocks/config/footerLinks'; -import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; -import TestApp from 'playwright/TestApp'; -import * as app from 'playwright/utils/app'; -import buildApiUrl from 'playwright/utils/buildApiUrl'; +import { test, expect } from 'playwright/lib'; import * as configs from 'playwright/utils/configs'; import Footer from './Footer'; -const FOOTER_LINKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS', 'https://localhost:3000/footer-links.json') || ''; - -const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version'); -const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status'); - -base.describe('with custom links, max cols', () => { - const test = base.extend({ - context: contextWithEnvs([ - { name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ]) as any, - }); - - test.beforeEach(async({ page, mount }) => { - await page.route(FOOTER_LINKS_URL, (route) => { - return route.fulfill({ - body: JSON.stringify(FOOTER_LINKS), - }); +const FOOTER_LINKS_URL = 'https://localhost:3000/footer-links.json'; + +test.describe('with custom links, max cols', () => { + test.beforeEach(async({ render, mockApiResponse, mockConfigResponse, injectMetaMaskProvider, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL ], + ]); + await mockConfigResponse('NEXT_PUBLIC_FOOTER_LINKS', FOOTER_LINKS_URL, JSON.stringify(FOOTER_LINKS)); + await injectMetaMaskProvider(); + await mockApiResponse('homepage_indexing_status', { + finished_indexing: false, + finished_indexing_blocks: false, + indexed_internal_transactions_ratio: '0.1', + indexed_blocks_ratio: '0.1', }); - await page.evaluate(() => { - window.ethereum = { - isMetaMask: true, - _events: {}, - } as WalletProvider; - }); - - await page.route(INDEXING_ALERT_API_URL, (route) => route.fulfill({ - status: 200, - body: JSON.stringify({ finished_indexing: false, indexed_internal_transactions_ratio: 0.1 }), - })); - - await mount( - -