Commit 9c4c16ac authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #2772 from blockscout/release/v2-1-0

Fixes for release v2.1.0
parents 02393619 7d9f94e4
...@@ -7,12 +7,12 @@ const title = 'Ton Application Chain (TAC)'; ...@@ -7,12 +7,12 @@ const title = 'Ton Application Chain (TAC)';
const tonExplorerUrl = getEnvValue('NEXT_PUBLIC_TAC_TON_EXPLORER_URL'); const tonExplorerUrl = getEnvValue('NEXT_PUBLIC_TAC_TON_EXPLORER_URL');
const config: Feature<{ explorerUrl: string }> = (() => { const config: Feature<{ tonExplorerUrl: string }> = (() => {
if (apis.tac && tonExplorerUrl) { if (apis.tac && tonExplorerUrl) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
explorerUrl: tonExplorerUrl, tonExplorerUrl,
}); });
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=tac_turin" # This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=tac_turin"
NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=https://tac-operation-lifecycle.k8s-dev.blockscout.com NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=https://tac-operation-lifecycle.k8s-dev.blockscout.com
NEXT_PUBLIC_TAC_TON_EXPLORER_URL=https://testnet.tonscan.org NEXT_PUBLIC_TAC_TON_EXPLORER_URL=https://testnet.tonviewer.com
# Local ENVs # Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http NEXT_PUBLIC_APP_PROTOCOL=http
......
...@@ -16,67 +16,67 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d ...@@ -16,67 +16,67 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
&nbsp; &nbsp;
## Table of contents ## Table of contents
- [App configuration](ENVS.md#app-configuration) - [App configuration](#app-configuration)
- [Blockchain parameters](ENVS.md#blockchain-parameters) - [Blockchain parameters](#blockchain-parameters)
- [API configuration](ENVS.md#api-configuration) - [API configuration](#api-configuration)
- [UI configuration](ENVS.md#ui-configuration) - [UI configuration](#ui-configuration)
- [Homepage](ENVS.md#homepage) - [Homepage](#homepage)
- [Navigation](ENVS.md#navigation) - [Navigation](#navigation)
- [Footer](ENVS.md#footer) - [Footer](#footer)
- [Favicon](ENVS.md#favicon) - [Favicon](#favicon)
- [Meta](ENVS.md#meta) - [Meta](#meta)
- [Views](ENVS.md#views) - [Views](#views)
- [Block](ENVS.md#block-views) - [Block](#block-views)
- [Address](ENVS.md#address-views) - [Address](#address-views)
- [Transaction](ENVS.md#transaction-views) - [Transaction](#transaction-views)
- [NFT](ENVS.md#nft-views) - [NFT](#nft-views)
- [Misc](ENVS.md#misc) - [Misc](#misc)
- [App features](ENVS.md#app-features) - [App features](#app-features)
- [My account](ENVS.md#my-account) - [My account](#my-account)
- [Gas tracker](ENVS.md#gas-tracker) - [Gas tracker](#gas-tracker)
- [Advanced filter](ENVS.md#advanced-filter) - [Advanced filter](#advanced-filter)
- [Address verification](ENVS.md#address-verification-in-my-account) in "My account" - [Address verification](#address-verification-in-my-account) in "My account"
- [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.) - [Blockchain interaction](#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.)
- [Banner ads](ENVS.md#banner-ads) - [Banner ads](#banner-ads)
- [Text ads](ENVS.md#text-ads) - [Text ads](#text-ads)
- [Beacon chain](ENVS.md#beacon-chain) - [Beacon chain](#beacon-chain)
- [User operations](ENVS.md#user-operations-erc-4337) - [User operations](#user-operations-erc-4337)
- [Rollup chain](ENVS.md#rollup-chain) - [Rollup chain](#rollup-chain)
- [Export data to CSV file](ENVS.md#export-data-to-csv-file) - [Export data to CSV file](#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics) - [Google analytics](#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics) - [Mixpanel analytics](#mixpanel-analytics)
- [GrowthBook feature flagging and A/B testing](ENVS.md#growthbook-feature-flagging-and-ab-testing) - [GrowthBook feature flagging and A/B testing](#growthbook-feature-flagging-and-ab-testing)
- [GraphQL API documentation](ENVS.md#graphql-api-documentation) - [GraphQL API documentation](#graphql-api-documentation)
- [REST API documentation](ENVS.md#rest-api-documentation) - [REST API documentation](#rest-api-documentation)
- [Marketplace](ENVS.md#marketplace) - [Marketplace](#marketplace)
- [Solidity to UML diagrams](ENVS.md#solidity-to-uml-diagrams) - [Solidity to UML diagrams](#solidity-to-uml-diagrams)
- [Blockchain statistics](ENVS.md#blockchain-statistics) - [Blockchain statistics](#blockchain-statistics)
- [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) - [Web3 wallet integration](#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet)
- [Transaction interpretation](ENVS.md#transaction-interpretation) - [Transaction interpretation](#transaction-interpretation)
- [Verified tokens info](ENVS.md#verified-tokens-info) - [Verified tokens info](#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration) - [Name service integration](#name-service-integration)
- [Metadata service integration](ENVS.md#metadata-service-integration) - [Metadata service integration](#metadata-service-integration)
- [Public tag submission](ENVS.md#public-tag-submission) - [Public tag submission](#public-tag-submission)
- [Data availability](ENVS.md#data-availability) - [Data availability](#data-availability)
- [Bridged tokens](ENVS.md#bridged-tokens) - [Bridged tokens](#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](#safecore-address-tags)
- [Address profile API](ENVS.md#address-profile-api) - [Address profile API](#address-profile-api)
- [Address XStar XHS score](ENVS.md#address-xstar-xhs-score) - [Address XStar XHS score](#address-xstar-xhs-score)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](#suave-chain)
- [Celo chain](ENVS.md#celo-chain) - [Celo chain](#celo-chain)
- [Ton Application Chain (TAC)](ENVS.md#ton-application-chain-tac) - [Ton Application Chain (TAC)](#ton-application-chain-tac)
- [MetaSuites extension](ENVS.md#metasuites-extension) - [MetaSuites extension](#metasuites-extension)
- [Validators list](ENVS.md#validators-list) - [Validators list](#validators-list)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [Sentry error monitoring](#sentry-error-monitoring)
- [Rollbar error monitoring](ENVS.md#rollbar-error-monitoring) - [Rollbar error monitoring](#rollbar-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry) - [OpenTelemetry](#opentelemetry)
- [DeFi dropdown](ENVS.md#defi-dropdown) - [DeFi dropdown](#defi-dropdown)
- [Multichain balance button](ENVS.md#multichain-balance-button) - [Multichain balance button](#multichain-balance-button)
- [Get gas button](ENVS.md#get-gas-button) - [Get gas button](#get-gas-button)
- [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk) - [Save on gas with GasHawk](#save-on-gas-with-gashawk)
- [Rewards service API](ENVS.md#rewards-service-api) - [Rewards service API](#rewards-service-api)
- [DEX pools](ENVS.md#dex-pools) - [DEX pools](#dex-pools)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](#external-services-configuration)
&nbsp; &nbsp;
...@@ -369,7 +369,7 @@ Settings for meta tags, OG tags and SEO ...@@ -369,7 +369,7 @@ Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ | | NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ | | NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ |
| NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ | | NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ |
...@@ -399,7 +399,7 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_ADVAN ...@@ -399,7 +399,7 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_ADVAN
### Address verification in "My account" ### Address verification in "My account"
*Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones: *Note* all ENV variables required for [My account](#my-account) feature should be passed alongside the following ones:
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
...@@ -413,12 +413,12 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_ADVAN ...@@ -413,12 +413,12 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_ADVAN
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://cloud.walletconnect.com/) integration | Required | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://cloud.walletconnect.com/) integration | Required | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Gnosis Chain` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_NAME | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `Gnosis Chain` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_ID | `number` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `99` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_ID | `number` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `99` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Ether` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `Ether` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `ETH` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `ETH` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | - | `18` | `6` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | See in [Blockchain parameters](#blockchain-parameters) section | - | `18` | `6` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -498,7 +498,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -498,7 +498,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -555,7 +555,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val ...@@ -555,7 +555,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | v1.0.x+ | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | v1.0.x+ |
| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | v1.24.0+ | | NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | v1.24.0+ |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ |
| 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` | v1.23.0+ | | 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` | v1.23.0+ |
| 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` | v1.28.0+ | | 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` | v1.28.0+ |
| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ | | NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ |
...@@ -668,7 +668,7 @@ This feature allows you to submit an application with a public address tag. ...@@ -668,7 +668,7 @@ This feature allows you to submit an application with a public address tag.
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ | | NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ |
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -900,7 +900,7 @@ The feature enables a "Save with GasHawk" button next to the "Gas used" value on ...@@ -900,7 +900,7 @@ The feature enables a "Save with GasHawk" button next to the "Gas used" value on
### Rewards service API ### Rewards service API
This feature enables Blockscout Merits program. It requires that the [My account](ENVS.md#my-account) and [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) features are also enabled. This feature enables Blockscout Merits program. It requires that the [My account](#my-account) and [Blockchain interaction](#blockchain-interaction-writing-to-contract-etc) features are also enabled.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.05.194a10 10 0 1 1 3.903 19.614A10 10 0 0 1 8.049.194ZM10 1.539a8.462 8.462 0 1 0 0 16.925 8.462 8.462 0 0 0 0-16.925Zm0 1.538a.77.77 0 0 1 .77.77v5.838l4.007 4a.771.771 0 0 1 .169.841.772.772 0 0 1-1.01.417.77.77 0 0 1-.251-.165l-4.23-4.232A.77.77 0 0 1 9.23 10V3.847a.77.77 0 0 1 .77-.77Z" fill="currentColor"/> <path d="M8.164.859a9.3 9.3 0 0 1 8.3 2.457l.047.046c.136.134.268.272.395.415l.03.033c.207.234.403.478.587.733l.039.055.15.214c.087.131.17.266.25.401.15.252.29.51.415.773.012.024.022.049.033.073.084.18.161.362.233.546l.03.08a9.3 9.3 0 0 1 .607 3.297l-.012.46a9.301 9.301 0 0 1-2.395 5.781l-.317.334a9.3 9.3 0 0 1-6.114 2.712l-.461.012A9.3 9.3 0 0 1 5.105 17.9l-.29-.187a9.303 9.303 0 0 1-.819-.615l-.055-.049a9.33 9.33 0 0 1-.438-.397l-.098-.098a8.814 8.814 0 0 1-.351-.371l-.078-.088a9.32 9.32 0 0 1-.16-.188l-.062-.075a9.397 9.397 0 0 1-.294-.383A9.297 9.297 0 0 1 8.164.859Zm1.817 1.509a7.617 7.617 0 0 0-3.994 1.13l-.237.153A7.616 7.616 0 0 0 3.06 6.81l-.113.259a7.615 7.615 0 0 0-.433 4.398l.06.277a7.613 7.613 0 0 0 2.024 3.622l.203.196a7.615 7.615 0 0 0 3.695 1.888l.278.05a7.614 7.614 0 0 0 4.12-.484l.26-.112a7.615 7.615 0 0 0 3.159-2.692l.152-.237a7.616 7.616 0 0 0 1.13-3.993l-.008-.378a7.614 7.614 0 0 0-1.962-4.732l-.26-.274a7.614 7.614 0 0 0-5.005-2.22l-.378-.01Zm.083 1.138c.192.02.374.104.512.242l.056.062c.123.15.191.339.191.534v5.29l3.631 3.626c.079.078.142.17.185.274l.027.078c.024.08.037.162.037.245l-.005.084a.84.84 0 0 1-.032.162l-.027.078a.852.852 0 0 1-.129.213l-.057.062a.837.837 0 0 1-.199.146l-.075.034a.841.841 0 0 1-.566.029l-.079-.029a.842.842 0 0 1-.274-.18l-3.877-3.877a.853.853 0 0 1-.146-.2l-.036-.075a.846.846 0 0 1-.063-.323V4.344c0-.223.09-.438.247-.596l.061-.055a.844.844 0 0 1 .535-.192l.083.005Z" fill="currentColor"/>
<path d="M19.697 9.64a9.704 9.704 0 0 0-1.434-4.73l-.194-.303a9.705 9.705 0 0 0-4.026-3.43l-.33-.143A9.703 9.703 0 0 0 8.461.419l-.354.064a9.705 9.705 0 0 0-4.71 2.405l-.259.25a9.705 9.705 0 0 0-2.58 4.618l-.075.35a9.705 9.705 0 0 0 .552 5.607l.143.33a9.704 9.704 0 0 0 3.43 4.025l.303.194a9.706 9.706 0 0 0 5.09 1.442l.48-.012a9.704 9.704 0 0 0 6.38-2.83l.332-.349A9.706 9.706 0 0 0 19.703 10l-.006-.359Zm-1.518-.047a8.189 8.189 0 0 0-2.11-5.09l-.28-.293a8.188 8.188 0 0 0-5.383-2.389L10 1.811a8.19 8.19 0 0 0-4.294 1.217l-.256.163A8.19 8.19 0 0 0 2.556 6.59l-.121.278a8.19 8.19 0 0 0-.466 4.73l.065.297A8.188 8.188 0 0 0 4.21 15.79l.218.21a8.19 8.19 0 0 0 3.974 2.03l.298.054a8.19 8.19 0 0 0 4.433-.52l.278-.12a8.19 8.19 0 0 0 3.397-2.895l.164-.255a8.19 8.19 0 0 0 1.216-4.295l-.01-.406ZM10.61 3.937a.61.61 0 0 0-.102-.339l-.077-.092a.61.61 0 0 0-.311-.168L10 3.327a.61.61 0 0 0-.338.102l-.093.077a.61.61 0 0 0-.179.43V10c0 .08.015.16.045.235l.056.105a.618.618 0 0 0 .075.092l4.17 4.17c.056.055.123.1.198.13l.115.035c.039.008.079.01.118.01h.002l.119-.01a.615.615 0 0 0 .115-.035l.107-.056a.601.601 0 0 0 .092-.075l.077-.093a.609.609 0 0 0 .057-.106l.035-.114.011-.12a.67.67 0 0 0-.011-.12l-.035-.115a.61.61 0 0 0-.134-.198L10.61 9.75V3.937Zm7.869 6.376a8.486 8.486 0 0 1-1.255 4.136l-.17.264a8.486 8.486 0 0 1-3.52 2.999l-.287.126a8.486 8.486 0 0 1-4.594.538l-.309-.055a8.484 8.484 0 0 1-4.117-2.104L4 15.999a8.485 8.485 0 0 1-2.254-4.037l-.068-.307a8.486 8.486 0 0 1 .484-4.902l.126-.288a8.486 8.486 0 0 1 2.999-3.52l.263-.17A8.486 8.486 0 0 1 10 1.516l.42.01A8.485 8.485 0 0 1 16 4l.289.305A8.484 8.484 0 0 1 18.485 10l-.006.314Zm1.509.182a10.002 10.002 0 0 1-2.575 6.216l-.342.36a10.001 10.001 0 0 1-6.575 2.916L10 20a10 10 0 0 1-5.244-1.486l-.311-.2A10.002 10.002 0 0 1 .91 14.166l-.148-.34a10.001 10.001 0 0 1-.57-5.777l.079-.362A10.002 10.002 0 0 1 2.928 2.93l.268-.257A10.001 10.001 0 0 1 8.049.192l.365-.065a10 10 0 0 1 5.413.634l.34.148a10.003 10.003 0 0 1 4.148 3.535l.2.312A10.002 10.002 0 0 1 20 9.999l-.012.496Zm-9.081-.869 3.904 3.899a.907.907 0 0 1 .198.294l.03.085c.026.085.04.174.04.263l-.005.09a.903.903 0 0 1-.035.174l-.03.085a.908.908 0 0 1-.138.229l-.06.066a.896.896 0 0 1-.216.157l-.08.037a.907.907 0 0 1-.61.03l-.083-.03a.904.904 0 0 1-.295-.194l-4.17-4.169a.914.914 0 0 1-.157-.214l-.038-.081a.91.91 0 0 1-.068-.349V3.937c0-.24.096-.471.265-.64l.067-.06A.906.906 0 0 1 10 3.03l.09.005a.914.914 0 0 1 .551.261l.06.066a.906.906 0 0 1 .206.575v5.69Z" fill="currentColor"/>
</svg> </svg>
import getPageType from './getPageType'; import getPageType, { PAGE_TYPE_DICT } from './getPageType';
import logEvent from './logEvent'; import logEvent from './logEvent';
import reset from './reset'; import reset from './reset';
import useInit from './useInit'; import useInit from './useInit';
...@@ -13,4 +13,5 @@ export { ...@@ -13,4 +13,5 @@ export {
getPageType, getPageType,
userProfile, userProfile,
reset, reset,
PAGE_TYPE_DICT,
}; };
...@@ -22,11 +22,11 @@ export function getTacOperationStatus(type: tac.OperationType) { ...@@ -22,11 +22,11 @@ export function getTacOperationStatus(type: tac.OperationType) {
} }
export function getTacOperationStage(data: tac.OperationDetails, txHash: string) { export function getTacOperationStage(data: tac.OperationDetails, txHash: string) {
const currentStep = data.status_history.find((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase())); const currentStep = data.status_history.filter((step) => step.transactions.some((tx) => tx.hash.toLowerCase() === txHash.toLowerCase()));
if (!currentStep) { if (currentStep.length === 0) {
return null; return;
} }
return STATUS_LABELS[currentStep.type]; return currentStep.map((step) => STATUS_LABELS[step.type]);
} }
export const STATUS_SEQUENCE: Array<tac.OperationStage_StageType> = [ export const STATUS_SEQUENCE: Array<tac.OperationStage_StageType> = [
......
...@@ -107,6 +107,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -107,6 +107,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
], ],
tac: [ tac: [
[ 'NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ], [ 'NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST', 'http://localhost:3100' ],
[ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonscan.org' ], [ 'NEXT_PUBLIC_TAC_TON_EXPLORER_URL', 'https://testnet.tonviewer.com' ],
], ],
}; };
...@@ -4,7 +4,7 @@ import { ADDRESS_HASH } from './addressParams'; ...@@ -4,7 +4,7 @@ import { ADDRESS_HASH } from './addressParams';
export const TAC_OPERATION: tac.OperationBriefDetails = { export const TAC_OPERATION: tac.OperationBriefDetails = {
operation_id: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6', operation_id: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
type: tac.OperationType.PENDING, type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z', timestamp: '2025-05-05T12:32:22.000Z',
sender: { sender: {
address: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6', address: '0x4d3d36b7fcab0a2f93f24bf313ebfe9cc0b2c7157d2aef7e7f7d5835528428c6',
...@@ -14,7 +14,7 @@ export const TAC_OPERATION: tac.OperationBriefDetails = { ...@@ -14,7 +14,7 @@ export const TAC_OPERATION: tac.OperationBriefDetails = {
export const TAC_OPERATION_DETAILS: tac.OperationDetails = { export const TAC_OPERATION_DETAILS: tac.OperationDetails = {
operation_id: '0x6e7cdeea3f39e7664597a44ddb33ce47ba061cbee2992e2c7b0e3f9294ff8b30', operation_id: '0x6e7cdeea3f39e7664597a44ddb33ce47ba061cbee2992e2c7b0e3f9294ff8b30',
type: tac.OperationType.PENDING, type: tac.OperationType.TAC_TON,
timestamp: '2025-05-05T12:32:22.000Z', timestamp: '2025-05-05T12:32:22.000Z',
sender: { sender: {
address: ADDRESS_HASH, address: ADDRESS_HASH,
......
...@@ -32,6 +32,10 @@ type Props = { ...@@ -32,6 +32,10 @@ type Props = {
const DateInput = ({ value, onChange, placeholder, max }: { value: string; onChange: (value: string) => void; placeholder: string; max: string }) => { const DateInput = ({ value, onChange, placeholder, max }: { value: string; onChange: (value: string) => void; placeholder: string; max: string }) => {
const [ tempValue, setTempValue ] = React.useState(value ? dayjs(value).format('YYYY-MM-DD') : ''); const [ tempValue, setTempValue ] = React.useState(value ? dayjs(value).format('YYYY-MM-DD') : '');
React.useEffect(() => {
setTempValue(value ? dayjs(value).format('YYYY-MM-DD') : '');
}, [ value ]);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setTempValue(event.target.value); setTempValue(event.target.value);
onChange(event.target.value); onChange(event.target.value);
...@@ -96,7 +100,7 @@ const AgeFilter = ({ value = defaultValue, handleFilterChange, onClose }: Props) ...@@ -96,7 +100,7 @@ const AgeFilter = ({ value = defaultValue, handleFilterChange, onClose }: Props)
<TableColumnFilter <TableColumnFilter
title="Set last duration" title="Set last duration"
isFilled={ Boolean(currentValue.from || currentValue.to || currentValue.age) } isFilled={ Boolean(currentValue.from || currentValue.to || currentValue.age) }
isTouched={ currentValue.age ? value.age !== currentValue.age : Boolean(currentValue.from && currentValue.to && !isEqual(currentValue, value)) } isTouched={ currentValue.age ? value.age !== currentValue.age : !isEqual(currentValue, value) }
onFilter={ onFilter } onFilter={ onFilter }
onReset={ onReset } onReset={ onReset }
hasReset hasReset
......
...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes'; ...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import { BackToButton } from 'toolkit/components/buttons/BackToButton'; import { BackToButton } from 'toolkit/components/buttons/BackToButton';
import { makePrettyLink } from 'toolkit/utils/url'; import { makePrettyLink } from 'toolkit/utils/url';
...@@ -46,6 +47,10 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) ...@@ -46,6 +47,10 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []); const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []);
const hideContractList = React.useCallback(() => setContractListType(undefined), []); const hideContractList = React.useCallback(() => setContractListType(undefined), []);
const handleBackToClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Back to', Source: mixpanel.PAGE_TYPE_DICT['/apps/[id]'] });
}, []);
return ( return (
<> <>
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }> <Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
...@@ -54,6 +59,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) ...@@ -54,6 +59,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
href={ goBackUrl } href={ goBackUrl }
hint="Back to dApps list" hint="Back to dApps list"
loading={ isLoading } loading={ isLoading }
onClick={ handleBackToClick }
/> />
<Link <Link
external external
......
...@@ -7,6 +7,7 @@ import { sortStatusHistory } from 'lib/operations/tac'; ...@@ -7,6 +7,7 @@ import { sortStatusHistory } from 'lib/operations/tac';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp'; import DetailedInfoTimestamp from 'ui/shared/DetailedInfo/DetailedInfoTimestamp';
import AddressEntityTacTon from 'ui/shared/entities/address/AddressEntityTacTon'; import AddressEntityTacTon from 'ui/shared/entities/address/AddressEntityTacTon';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import TacOperationLifecycleAccordion from './TacOperationLifecycleAccordion'; import TacOperationLifecycleAccordion from './TacOperationLifecycleAccordion';
...@@ -43,6 +44,16 @@ const TacOperationDetails = ({ isLoading, data }: Props) => { ...@@ -43,6 +44,16 @@ const TacOperationDetails = ({ isLoading, data }: Props) => {
</> </>
) } ) }
<DetailedInfo.ItemLabel
hint="The status of the operation"
isLoading={ isLoading }
>
Status
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<TacOperationStatus status={ data.type } isLoading={ isLoading }/>
</DetailedInfo.ItemValue>
{ data.timestamp && ( { data.timestamp && (
<> <>
<DetailedInfo.ItemLabel <DetailedInfo.ItemLabel
...@@ -66,7 +77,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => { ...@@ -66,7 +77,7 @@ const TacOperationDetails = ({ isLoading, data }: Props) => {
Lifecycle Lifecycle
</DetailedInfo.ItemLabel> </DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue> <DetailedInfo.ItemValue>
<TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading }/> <TacOperationLifecycleAccordion data={ statusHistory } isLoading={ isLoading } type={ data.type }/>
</DetailedInfo.ItemValue> </DetailedInfo.ItemValue>
</> </>
) } ) }
......
import React from 'react'; import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types'; import * as tac from '@blockscout/tac-operation-lifecycle-types';
import { AccordionItem, AccordionRoot } from 'toolkit/chakra/accordion'; import { AccordionItem, AccordionRoot } from 'toolkit/chakra/accordion';
...@@ -10,13 +10,16 @@ import TacOperationLifecycleAccordionItemTrigger from './TacOperationLifecycleAc ...@@ -10,13 +10,16 @@ import TacOperationLifecycleAccordionItemTrigger from './TacOperationLifecycleAc
interface Props { interface Props {
data: tac.OperationDetails['status_history']; data: tac.OperationDetails['status_history'];
isLoading?: boolean; isLoading?: boolean;
type: tac.OperationType;
} }
const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => { const TacOperationLifecycleAccordion = ({ data, isLoading, type }: Props) => {
const isPending = type === tac.OperationType.PENDING && !isLoading;
return ( return (
<AccordionRoot maxW="800px" display="flex" flexDirection="column" rowGap={ 6 } lazyMount> <AccordionRoot maxW="800px" display="flex" flexDirection="column" rowGap={ 6 } lazyMount>
{ data.map((item, index) => { { data.map((item, index) => {
const isLast = index === data.length - 1; const isLast = index === data.length - 1 && !isPending;
return ( return (
<AccordionItem key={ index } value={ item.type } borderBottomWidth="0px"> <AccordionItem key={ index } value={ item.type } borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger <TacOperationLifecycleAccordionItemTrigger
...@@ -33,6 +36,17 @@ const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => { ...@@ -33,6 +36,17 @@ const TacOperationLifecycleAccordion = ({ data, isLoading }: Props) => {
</AccordionItem> </AccordionItem>
); );
}) } }) }
{ isPending && (
<AccordionItem value="pending" borderBottomWidth="0px">
<TacOperationLifecycleAccordionItemTrigger
status="pending"
isFirst={ false }
isLast={ true }
isLoading={ isLoading }
isSuccess={ false }
/>
</AccordionItem>
) }
</AccordionRoot> </AccordionRoot>
); );
}; };
......
import { HStack } from '@chakra-ui/react'; import { Box, HStack, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types'; import type * as tac from '@blockscout/tac-operation-lifecycle-types';
...@@ -9,7 +9,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton'; ...@@ -9,7 +9,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
status: tac.OperationStage_StageType; status: tac.OperationStage_StageType | 'pending';
isFirst: boolean; isFirst: boolean;
isLast: boolean; isLast: boolean;
isLoading?: boolean; isLoading?: boolean;
...@@ -17,6 +17,32 @@ interface Props { ...@@ -17,6 +17,32 @@ interface Props {
} }
const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, isSuccess, isLoading }: Props) => { const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, isSuccess, isLoading }: Props) => {
const content = (() => {
switch (status) {
case 'pending': {
return (
<HStack gap={ 2 }>
<Spinner size="md"/>
<Box color="text.secondary">
Pending
</Box>
</HStack>
);
}
default: {
return (
<HStack gap={ 2 } color={ isSuccess ? 'green.500' : 'red.600' }>
<IconSvg name={ isSuccess ? 'verification-steps/finalized' : 'verification-steps/error' } boxSize={ 5 } isLoading={ isLoading }/>
<Skeleton loading={ isLoading }>
{ STATUS_LABELS[status] }
</Skeleton>
</HStack>
);
}
}
})();
return ( return (
<AccordionItemTrigger <AccordionItemTrigger
position="relative" position="relative"
...@@ -47,15 +73,14 @@ const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, is ...@@ -47,15 +73,14 @@ const TacOperationLifecycleAccordionItemTrigger = ({ status, isFirst, isLast, is
height: { base: '14px', lg: '6px' }, height: { base: '14px', lg: '6px' },
}, },
}} }}
disabled={ isLoading } disabled={ isLoading || status === 'pending' }
noIndicator={ isLoading } noIndicator={ isLoading || status === 'pending' }
cursor={ status === 'pending' ? 'default' : 'pointer' }
_disabled={{
opacity: status === 'pending' ? 1 : 'control.disabled',
}}
> >
<HStack gap={ 2 } color={ isSuccess ? 'green.500' : 'red.600' }> { content }
<IconSvg name={ isSuccess ? 'verification-steps/finalized' : 'verification-steps/error' } boxSize={ 5 } isLoading={ isLoading }/>
<Skeleton loading={ isLoading }>
{ STATUS_LABELS[status] }
</Skeleton>
</HStack>
</AccordionItemTrigger> </AccordionItemTrigger>
); );
}; };
......
...@@ -18,6 +18,7 @@ const TacOperationsListItem = ({ item, isLoading }: Props) => { ...@@ -18,6 +18,7 @@ const TacOperationsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<OperationEntity <OperationEntity
id={ item.operation_id } id={ item.operation_id }
type={ item.type }
isLoading={ isLoading } isLoading={ isLoading }
/> />
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
......
...@@ -19,12 +19,12 @@ const TacOperationsTable = ({ items, isLoading }: Props) => { ...@@ -19,12 +19,12 @@ const TacOperationsTable = ({ items, isLoading }: Props) => {
<TableRoot minW="950px"> <TableRoot minW="950px">
<TableHeaderSticky top={ 68 }> <TableHeaderSticky top={ 68 }>
<TableRow> <TableRow>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="100%">Operation</TableColumnHeader> <TableColumnHeader w="100%">Operation</TableColumnHeader>
<TableColumnHeader w="200px"> <TableColumnHeader w="200px">
Timestamp Timestamp
<TimeFormatToggle/> <TimeFormatToggle/>
</TableColumnHeader> </TableColumnHeader>
<TableColumnHeader w="200px">Status</TableColumnHeader>
<TableColumnHeader w="250px">Sender</TableColumnHeader> <TableColumnHeader w="250px">Sender</TableColumnHeader>
</TableRow> </TableRow>
</TableHeaderSticky> </TableHeaderSticky>
......
...@@ -15,10 +15,15 @@ interface Props { ...@@ -15,10 +15,15 @@ interface Props {
const TacOperationsTableItem = ({ item, isLoading }: Props) => { const TacOperationsTableItem = ({ item, isLoading }: Props) => {
return ( return (
<TableRow> <TableRow>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle"> <TableCell verticalAlign="middle">
<OperationEntity <OperationEntity
id={ item.operation_id } id={ item.operation_id }
type={ item.type }
isLoading={ isLoading } isLoading={ isLoading }
truncation="constant_long"
/> />
</TableCell> </TableCell>
<TableCell verticalAlign="middle"> <TableCell verticalAlign="middle">
...@@ -28,9 +33,6 @@ const TacOperationsTableItem = ({ item, isLoading }: Props) => { ...@@ -28,9 +33,6 @@ const TacOperationsTableItem = ({ item, isLoading }: Props) => {
color="text.secondary" color="text.secondary"
/> />
</TableCell> </TableCell>
<TableCell verticalAlign="middle">
<TacOperationStatus status={ item.type } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle" pr={ 12 }> <TableCell verticalAlign="middle" pr={ 12 }>
{ item.sender ? ( { item.sender ? (
<AddressEntityTacTon <AddressEntityTacTon
......
...@@ -47,7 +47,7 @@ const RewardsDashboard = () => { ...@@ -47,7 +47,7 @@ const RewardsDashboard = () => {
return ( return (
<> <>
<Flex gap={ 3 } justifyContent="space-between"> <Flex gap={ 3 } justifyContent="space-between" mb={ 6 }>
<PageTitle <PageTitle
title="Dashboard" title="Dashboard"
secondRow={ ( secondRow={ (
...@@ -58,6 +58,7 @@ const RewardsDashboard = () => { ...@@ -58,6 +58,7 @@ const RewardsDashboard = () => {
to earn, spend, and learn more about the program. to earn, spend, and learn more about the program.
</span> </span>
) } ) }
mb={ 0 }
/> />
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/> <AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex> </Flex>
......
import React from 'react'; import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import * as tacOperationMock from 'mocks/operations/tac'; import * as tacOperationMock from 'mocks/operations/tac';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
...@@ -27,3 +29,25 @@ test('base view +@dark-mode +@mobile', async({ render, mockTextAd, mockApiRespon ...@@ -27,3 +29,25 @@ test('base view +@dark-mode +@mobile', async({ render, mockTextAd, mockApiRespon
await component.getByRole('button', { name: 'Executed in TON' }).click(); await component.getByRole('button', { name: 'Executed in TON' }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('pending operation', async({ render, mockTextAd, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.tac);
await mockTextAd();
await mockApiResponse('tac:operation', {
... tacOperationMock.tacOperation,
type: tac.OperationType.PENDING,
}, {
pathParams: { id: tacOperationMock.tacOperation.operation_id },
});
const component = await render(
<TacOperation/>,
{ hooksConfig: {
router: {
query: { id: tacOperationMock.tacOperation.operation_id },
isReady: true,
},
} },
);
await expect(component).toHaveScreenshot();
});
...@@ -44,7 +44,7 @@ const TacOperation = () => { ...@@ -44,7 +44,7 @@ const TacOperation = () => {
) : null; ) : null;
const titleSecondRow = ( const titleSecondRow = (
<OperationEntity id={ id } noLink variant="subheading"/> <OperationEntity id={ id } noLink variant="subheading" type={ query.data?.type }/>
); );
return ( return (
......
...@@ -32,7 +32,7 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -32,7 +32,7 @@ import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import type { SearchResultAppItem } from 'ui/shared/search/utils'; import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TacOperationTag from 'ui/shared/TacOperationTag'; import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import SearchResultEntityTag from './SearchResultEntityTag'; import SearchResultEntityTag from './SearchResultEntityTag';
...@@ -219,7 +219,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -219,7 +219,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
case 'tac_operation': { case 'tac_operation': {
return ( return (
<OperationEntity.Container> <OperationEntity.Container>
<OperationEntity.Icon/> <OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link <OperationEntity.Link
isLoading={ isLoading } isLoading={ isLoading }
id={ data.tac_operation.operation_id } id={ data.tac_operation.operation_id }
...@@ -233,7 +233,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr ...@@ -233,7 +233,7 @@ const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Pr
mr={ 2 } mr={ 2 }
/> />
</OperationEntity.Link> </OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/> <TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container> </OperationEntity.Container>
); );
} }
......
...@@ -32,7 +32,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -32,7 +32,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { SearchResultAppItem } from 'ui/shared/search/utils'; import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
import TacOperationTag from 'ui/shared/TacOperationTag'; import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
import SearchResultEntityTag from './SearchResultEntityTag'; import SearchResultEntityTag from './SearchResultEntityTag';
...@@ -331,7 +331,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -331,7 +331,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
<> <>
<TableCell colSpan={ 2 } fontSize="sm"> <TableCell colSpan={ 2 } fontSize="sm">
<OperationEntity.Container> <OperationEntity.Container>
<OperationEntity.Icon/> <OperationEntity.Icon type={ data.tac_operation.type }/>
<OperationEntity.Link <OperationEntity.Link
isLoading={ isLoading } isLoading={ isLoading }
id={ data.tac_operation.operation_id } id={ data.tac_operation.operation_id }
...@@ -345,7 +345,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P ...@@ -345,7 +345,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: P
mr={ 2 } mr={ 2 }
/> />
</OperationEntity.Link> </OperationEntity.Link>
<TacOperationTag type={ data.tac_operation.type }/> <TacOperationStatus status={ data.tac_operation.type }/>
</OperationEntity.Container> </OperationEntity.Container>
</TableCell> </TableCell>
<TableCell fontSize="sm" verticalAlign="middle" isNumeric> <TableCell fontSize="sm" verticalAlign="middle" isNumeric>
......
import { Flex, chakra } from '@chakra-ui/react'; import { Flex, chakra } from '@chakra-ui/react';
import { debounce } from 'es-toolkit'; import { debounce } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { Heading } from 'toolkit/chakra/heading'; import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
...@@ -29,10 +31,12 @@ const TEXT_MAX_LINES = 1; ...@@ -29,10 +31,12 @@ const TEXT_MAX_LINES = 1;
const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading = false, afterTitle, beforeTitle, secondRow }: Props) => { const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading = false, afterTitle, beforeTitle, secondRow }: Props) => {
const tooltip = useDisclosure(); const tooltip = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter();
const [ isTextTruncated, setIsTextTruncated ] = React.useState(false); const [ isTextTruncated, setIsTextTruncated ] = React.useState(false);
const headingRef = React.useRef<HTMLHeadingElement>(null); const headingRef = React.useRef<HTMLHeadingElement>(null);
const textRef = React.useRef<HTMLSpanElement>(null); const textRef = React.useRef<HTMLSpanElement>(null);
const pageType = mixpanel.getPageType(router.pathname);
const updatedTruncateState = React.useCallback(() => { const updatedTruncateState = React.useCallback(() => {
if (!headingRef.current || !textRef.current) { if (!headingRef.current || !textRef.current) {
...@@ -71,6 +75,11 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -71,6 +75,11 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
} }
}, [ tooltip ]); }, [ tooltip ]);
const handleBackToClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Back to', Source: pageType });
backLink && 'onClick' in backLink && backLink.onClick();
}, [ backLink, pageType ]);
return ( return (
<Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }> <Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }>
<Flex <Flex
...@@ -85,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -85,7 +94,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
<BackToButton <BackToButton
hint={ backLink.label } hint={ backLink.label }
href={ 'url' in backLink ? backLink.url : undefined } href={ 'url' in backLink ? backLink.url : undefined }
onClick={ 'onClick' in backLink ? backLink.onClick : undefined } onClick={ handleBackToClick }
loadingSkeleton={ isLoading } loadingSkeleton={ isLoading }
mr={ 3 } mr={ 3 }
/> />
......
...@@ -68,7 +68,7 @@ const Icon = (props: IconProps) => { ...@@ -68,7 +68,7 @@ const Icon = (props: IconProps) => {
const isProxy = Boolean(props.address.implementations?.length); const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified; const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular'; const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + props.hintPostfix; const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + (props.hintPostfix ?? '');
return ( return (
<EntityBase.Icon <EntityBase.Icon
...@@ -83,7 +83,7 @@ const Icon = (props: IconProps) => { ...@@ -83,7 +83,7 @@ const Icon = (props: IconProps) => {
const label = (() => { const label = (() => {
if (isDelegatedAddress) { if (isDelegatedAddress) {
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + props.hintPostfix; return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + (props.hintPostfix ?? '');
} }
return props.hint; return props.hint;
......
...@@ -23,7 +23,7 @@ const AddressEntityTacTon = (props: Props) => { ...@@ -23,7 +23,7 @@ const AddressEntityTacTon = (props: Props) => {
const href = (() => { const href = (() => {
switch (props.chainType) { switch (props.chainType) {
case tac.BlockchainType.TON: case tac.BlockchainType.TON:
return tacFeature.explorerUrl + route({ return tacFeature.tonExplorerUrl + route({
pathname: '/address/[hash]', pathname: '/address/[hash]',
query: { query: {
...props.query, ...props.query,
......
import { chakra } from '@chakra-ui/react'; import { Spinner, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as tac from '@blockscout/tac-operation-lifecycle-types';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components'; import * as EntityBase from 'ui/shared/entities/base/components';
...@@ -22,7 +24,14 @@ const Link = chakra((props: LinkProps) => { ...@@ -22,7 +24,14 @@ const Link = chakra((props: LinkProps) => {
); );
}); });
const Icon = (props: EntityBase.IconBaseProps) => { type IconProps = EntityBase.IconBaseProps & Pick<EntityProps, 'type'>;
const Icon = (props: IconProps) => {
switch (props.type) {
case tac.OperationType.PENDING: {
return <Spinner size="md" marginRight={ props.marginRight ?? '8px' }/>;
}
default: {
return ( return (
<EntityBase.Icon <EntityBase.Icon
{ ...props } { ...props }
...@@ -30,6 +39,8 @@ const Icon = (props: EntityBase.IconBaseProps) => { ...@@ -30,6 +39,8 @@ const Icon = (props: EntityBase.IconBaseProps) => {
borderRadius="none" borderRadius="none"
/> />
); );
}
}
}; };
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>; type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>;
...@@ -58,6 +69,7 @@ const Container = EntityBase.Container; ...@@ -58,6 +69,7 @@ const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps { export interface EntityProps extends EntityBase.EntityBaseProps {
id: string; id: string;
type: tac.OperationType | undefined;
} }
const OperationEntity = (props: EntityProps) => { const OperationEntity = (props: EntityProps) => {
......
...@@ -14,7 +14,7 @@ const TxEntityTon = (props: TxEntity.EntityProps) => { ...@@ -14,7 +14,7 @@ const TxEntityTon = (props: TxEntity.EntityProps) => {
} }
const formattedHash = props.hash.replace(/^0x/, ''); const formattedHash = props.hash.replace(/^0x/, '');
const defaultHref = `${ stripTrailingSlash(tacFeature.explorerUrl) }/tx/${ formattedHash }`; const defaultHref = `${ stripTrailingSlash(tacFeature.tonExplorerUrl) }/transaction/${ formattedHash }`;
return <TxEntity.default { ...props } hash={ formattedHash } href={ props.href ?? defaultHref } icon={{ name: 'brands/ton' }} isExternal/>; return <TxEntity.default { ...props } hash={ formattedHash } href={ props.href ?? defaultHref } icon={{ name: 'brands/ton' }} isExternal/>;
}; };
......
import React from 'react'; import React from 'react';
import { useSettingsContext } from 'lib/contexts/settings'; import { useSettingsContext } from 'lib/contexts/settings';
import * as mixpanel from 'lib/mixpanel/index';
import { IconButton } from 'toolkit/chakra/icon-button'; import { IconButton } from 'toolkit/chakra/icon-button';
import type { IconButtonProps } from 'toolkit/chakra/icon-button'; import type { IconButtonProps } from 'toolkit/chakra/icon-button';
import { Tooltip } from 'toolkit/chakra/tooltip'; import { Tooltip } from 'toolkit/chakra/tooltip';
...@@ -12,12 +13,17 @@ const TimeFormatToggle = (props: Props) => { ...@@ -12,12 +13,17 @@ const TimeFormatToggle = (props: Props) => {
const settings = useSettingsContext(); const settings = useSettingsContext();
const timeFormat = settings?.timeFormat || 'relative'; const timeFormat = settings?.timeFormat || 'relative';
const handleClick = React.useCallback(() => {
settings?.toggleTimeFormat();
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Switch time format', Source: 'Table header' });
}, [ settings ]);
return ( return (
<Tooltip content="Toggle time format"> <Tooltip content="Toggle time format">
<IconButton <IconButton
aria-label="Toggle time format" aria-label="Toggle time format"
variant="icon_secondary" variant="icon_secondary"
onClick={ settings?.toggleTimeFormat } onClick={ handleClick }
boxSize={ 5 } boxSize={ 5 }
selected={ timeFormat === 'absolute' } selected={ timeFormat === 'absolute' }
borderRadius="sm" borderRadius="sm"
......
...@@ -7,6 +7,7 @@ import { NETWORK_GROUPS } from 'types/networks'; ...@@ -7,6 +7,7 @@ import { NETWORK_GROUPS } from 'types/networks';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import * as mixpanel from 'lib/mixpanel/index';
import { useDisclosure } from 'toolkit/hooks/useDisclosure'; import { useDisclosure } from 'toolkit/hooks/useDisclosure';
export default function useNetworkMenu() { export default function useNetworkMenu() {
...@@ -20,14 +21,21 @@ export default function useNetworkMenu() { ...@@ -20,14 +21,21 @@ export default function useNetworkMenu() {
staleTime: Infinity, staleTime: Infinity,
}); });
const handleOpenChange = React.useCallback((details: { open: boolean }) => {
if (details.open) {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Network menu', Source: 'Header' });
}
onOpenChange(details);
}, [ onOpenChange ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
open, open,
onClose, onClose,
onOpen, onOpen,
onToggle, onToggle,
onOpenChange, onOpenChange: handleOpenChange,
isPending, isPending,
data, data,
availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)), availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)),
}), [ open, onClose, onOpen, onToggle, onOpenChange, data, isPending ]); }), [ open, onClose, onOpen, onToggle, handleOpenChange, data, isPending ]);
} }
...@@ -7,16 +7,16 @@ import type { SearchResultTacOperation } from 'types/api/search'; ...@@ -7,16 +7,16 @@ import type { SearchResultTacOperation } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import * as OperationEntity from 'ui/shared/entities/operation/OperationEntity'; import * as OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TacOperationTag from 'ui/shared/TacOperationTag'; import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
const SearchBarSuggestTacOperation = ({ data, isMobile }: ItemsProps<SearchResultTacOperation>) => { const SearchBarSuggestTacOperation = ({ data, isMobile }: ItemsProps<SearchResultTacOperation>) => {
const icon = <OperationEntity.Icon/>; const icon = <OperationEntity.Icon type={ data.tac_operation.type }/>;
const hash = ( const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 } mr={ 2 }> <chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 } mr={ 2 }>
<HashStringShortenDynamic hash={ data.tac_operation.operation_id } noTooltip/> <HashStringShortenDynamic hash={ data.tac_operation.operation_id } noTooltip/>
</chakra.mark> </chakra.mark>
); );
const status = <TacOperationTag type={ data.tac_operation.type }/>; const status = <TacOperationStatus status={ data.tac_operation.type }/>;
const date = dayjs(data.tac_operation.timestamp).format('llll'); const date = dayjs(data.tac_operation.timestamp).format('llll');
if (isMobile) { if (isMobile) {
......
...@@ -3,10 +3,11 @@ import React from 'react'; ...@@ -3,10 +3,11 @@ import React from 'react';
import type * as tac from '@blockscout/tac-operation-lifecycle-types'; import type * as tac from '@blockscout/tac-operation-lifecycle-types';
import { getTacOperationStatus, getTacOperationStage } from 'lib/operations/tac'; import { getTacOperationStage } from 'lib/operations/tac';
import { Tag } from 'toolkit/chakra/tag'; import { Tag } from 'toolkit/chakra/tag';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import OperationEntity from 'ui/shared/entities/operation/OperationEntity'; import OperationEntity from 'ui/shared/entities/operation/OperationEntity';
import TacOperationStatus from 'ui/shared/statusTag/TacOperationStatus';
interface Props { interface Props {
tacOperations: Array<tac.OperationDetails>; tacOperations: Array<tac.OperationDetails>;
...@@ -35,19 +36,20 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => { ...@@ -35,19 +36,20 @@ const TxDetailsTacOperation = ({ tacOperations, isLoading, txHash }: Props) => {
> >
{ tacOperations.map((tacOperation) => { { tacOperations.map((tacOperation) => {
const tags = [ const tags = [
getTacOperationStage(tacOperation, txHash), ...(getTacOperationStage(tacOperation, txHash) || []),
getTacOperationStatus(tacOperation.type),
]; ];
return ( return (
<HStack key={ tacOperation.operation_id } gap={ 3 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}> <HStack key={ tacOperation.operation_id } gap={ 3 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<OperationEntity <OperationEntity
id={ tacOperation.operation_id } id={ tacOperation.operation_id }
type={ tacOperation.type }
isLoading={ isLoading } isLoading={ isLoading }
/> />
{ tags.length > 0 && ( { tags.length > 0 && (
<HStack flexShrink={ 0 }> <HStack flexShrink={ 0 } flexWrap="wrap">
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading }>{ tag }</Tag>) } <TacOperationStatus status={ tacOperation.type } isLoading={ isLoading }/>
{ tags.map((tag) => <Tag key={ tag } loading={ isLoading } flexShrink={ 0 }>{ tag }</Tag>) }
</HStack> </HStack>
) } ) }
</HStack> </HStack>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment