Commit d5863de0 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Account v2 (#2262)

* simple profile button and auth modal layout

* connect email and code screens to API

* add screens to modal for wallet authentication

* migrate to pin input

* user profile menu

* refactor otp field

* fix passing set-cookie from api response

* add wallet info into profile menu

* add mobile menu

* show connected wallet address in button

* my profile page re-design

* custom behaviour of connect button on dapp page

* style pin input

* add logout

* handle case when account is disabled

* handle case when wc is disabled

* remove old components

* refactoring

* workflow to link wallet or email to account

* link wallet from profile

* show better OTP code errors

* add email alert on watchlist and verified addresses pages

* deprecate env and remove old code

* remove code for unverified email page

* add auth guard to address action items

* move useRedirectForInvalidAuthToken hook

* add mixpanel events

* refetch csrf token after login and fix connect wallet from contract page

* Add NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY env

* migrate to reCAPTCHA v3

* resend code and change email from profile page

* better wallet sign-in message error

* fix demo envs

* update some screenshots

* profile button design fixes

* fix behaviour "connect wallet" button on contract page

* fix linking email and wallet to existing account

* bug fixes

* restore the login page

* update screenshots

* tests for auth modal and user profile

* add name field to profile page and write some tests

* [skip ci] clean up and more tests

* update texts

* change text once more

* fix verified email checkmark behaviour

* pass api error to the toast while signing in with wallet

* [skip ci] disable email field on profile page

* bug fixes

* update screenshot

* Blockscout account V2

Fixes #2029

* fix texts and button paddings

* Form fields refactoring (#2320)

* text and address fields for watchlist form

* checkbox field component

* refactor private tags form

* refactor api keys and custom abi

* refactor verifiy address and token info forms (pt. 1)

* refactor token info forms (pt. 2)

* refactor token info forms (pt. 3)

* refactor public tags form

* refactor contract verification form

* refactor contract audit form

* refactor auth, profile and csv export forms

* renaming and moving

* more refactoring and test fixes

---------
Co-authored-by: default avataraagaev <alik@agaev.me>
parent 8c4a4ad8
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
...@@ -7,7 +7,7 @@ const RESTRICTED_MODULES = { ...@@ -7,7 +7,7 @@ const RESTRICTED_MODULES = {
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' }, { name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{ {
name: '@chakra-ui/react', name: '@chakra-ui/react',
importNames: [ 'Popover', 'Menu', 'useToast' ], importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast' ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead', message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
}, },
{ {
......
import type { Feature } from './types'; import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash'; import services from '../services';
import app from '../app';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const authUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_AUTH_URL') || app.baseUrl);
const logoutUrl = (() => {
try {
const envUrl = getEnvValue('NEXT_PUBLIC_LOGOUT_URL');
const auth0ClientId = getEnvValue('NEXT_PUBLIC_AUTH0_CLIENT_ID');
const returnUrl = authUrl + '/auth/logout';
if (!envUrl || !auth0ClientId) {
throw Error();
}
const url = new URL(envUrl);
url.searchParams.set('client_id', auth0ClientId);
url.searchParams.set('returnTo', returnUrl);
return url.toString();
} catch (error) {
return;
}
})();
const title = 'My account'; const title = 'My account';
const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => { const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
if ( if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV3.siteKey) {
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
authUrl &&
logoutUrl
) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
authUrl, recaptchaSiteKey: services.reCaptchaV3.siteKey,
logoutUrl,
}); });
} }
......
...@@ -5,12 +5,12 @@ import services from '../services'; ...@@ -5,12 +5,12 @@ import services from '../services';
const title = 'Export data to CSV file'; const title = 'Export data to CSV file';
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => { const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) { if (services.reCaptchaV3.siteKey) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
reCaptcha: { reCaptcha: {
siteKey: services.reCaptcha.siteKey, siteKey: services.reCaptchaV3.siteKey,
}, },
}); });
} }
......
...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); ...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Public tag submission'; const title = 'Public tag submission';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (services.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) { if (services.reCaptchaV3.siteKey && addressMetadata.isEnabled && apiHost) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
......
import { getEnvValue } from './utils'; import { getEnvValue } from './utils';
export default Object.freeze({ export default Object.freeze({
reCaptcha: { reCaptchaV3: {
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY'),
}, },
}); });
...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx ...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
...@@ -52,5 +52,5 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 ...@@ -52,5 +52,5 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
...@@ -148,4 +148,15 @@ function printDeprecationWarning(envsMap: Record<string, string>) { ...@@ -148,4 +148,15 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.'); console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n'); console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
} }
if (
envsMap.NEXT_PUBLIC_AUTH0_CLIENT_ID ||
envsMap.NEXT_PUBLIC_AUTH_URL ||
envsMap.NEXT_PUBLIC_LOGOUT_URL
) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_AUTH0_CLIENT_ID, NEXT_PUBLIC_AUTH_URL and NEXT_PUBLIC_LOGOUT_URL variables are now deprecated and will be removed in the next release.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
} }
...@@ -839,7 +839,7 @@ const schema = yup ...@@ -839,7 +839,7 @@ const schema = yup
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
......
...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com ...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://example.com NEXT_PUBLIC_LOGOUT_URL=https://example.com
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
...@@ -77,7 +77,7 @@ frontend: ...@@ -77,7 +77,7 @@ frontend:
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY
...@@ -83,8 +83,8 @@ frontend: ...@@ -83,8 +83,8 @@ frontend:
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 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
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
| 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_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.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | | NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | | NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaces by NEXT_PUBLIC_HOMEPAGE_STATS | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS |
\ No newline at end of file | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.36.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY |
...@@ -342,9 +342,10 @@ Settings for meta tags, OG tags and SEO ...@@ -342,9 +342,10 @@ 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_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | Required | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.36.0+ |
| NEXT_PUBLIC_AUTH_URL | `string` | 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 | Required | - | `https://blockscout.com` | 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_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | Required | - | `https://blockscoutcom.us.auth0.com/v2/logout` | 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+ |
&nbsp; &nbsp;
...@@ -442,7 +443,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -442,7 +443,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_V3_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.36.0+ |
&nbsp; &nbsp;
...@@ -801,4 +802,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt ...@@ -801,4 +802,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt
| 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` | Site key | - | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.36.0+ |
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3 3.7a1.7 1.7 0 0 1 3.4 0v.903a1 1 0 1 0 2 0V3.7a3.7 3.7 0 0 0-7.4 0v6.272a1 1 0 0 0 2 0V3.7Zm5.4 6.302a1 1 0 0 0-2 0V16.3a1.7 1.7 0 0 1-3.4 0v-.914a1 1 0 1 0-2 0v.914a3.7 3.7 0 1 0 7.4 0v-6.298ZM3.692 8.3C2.76 8.3 2 9.059 2 10c0 .94.76 1.7 1.693 1.7H10a1 1 0 1 1 0 2H3.693A3.696 3.696 0 0 1 0 10C0 7.96 1.65 6.3 3.693 6.3h.902a1 1 0 0 1 0 2h-.902ZM10 6.3a1 1 0 0 0 0 2h6.294C17.238 8.3 18 9.064 18 10c0 .937-.761 1.7-1.705 1.7h-.865a1 1 0 1 0 0 2h.865A3.702 3.702 0 0 0 20 10c0-2.046-1.66-3.7-3.705-3.7H10Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 177 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.961 7.644a2 2 0 0 0-2.251-2.812l-76.313 17.5a2 2 0 0 0-.727 3.569l17.58 12.744v17.636a2.001 2.001 0 0 0 3.321 1.502l10.716-9.426 21.384 9.744a2 2 0 0 0 2.634-.957l23.656-49.5ZM108.308 35.92 93.583 25.247l62.923-14.43-48.198 25.104Zm.942 1.764v14.173l8.367-7.36a.385.385 0 0 1 .022-.018l.016-.014a.998.998 0 0 1 .214-.242l37.608-30.616-46.227 24.077Zm31.293 15.962-19.927-9.08 39.719-32.335-19.792 41.415ZM93.278 65.729a1.5 1.5 0 0 0 1.93 2.296 93.435 93.435 0 0 0 2.449-2.13 57.65 57.65 0 0 0 .819-.753l.044-.042.012-.011.004-.004.001-.001L97.5 64l1.038 1.083a1.5 1.5 0 0 0-2.075-2.167l-.002.002-.008.008-.037.035a19.011 19.011 0 0 1-.154.145 90.663 90.663 0 0 1-2.984 2.623Zm-5.037 7.714a1.5 1.5 0 0 0-1.751-2.436 105.47 105.47 0 0 1-7.163 4.73 1.5 1.5 0 1 0 1.547 2.57c2.69-1.618 5.17-3.284 7.367-4.864Zm-15.172 9.06a1.5 1.5 0 0 0-1.28-2.714c-2.556 1.205-5.207 2.277-7.906 3.13a1.5 1.5 0 0 0 .905 2.86c2.847-.9 5.624-2.024 8.28-3.276Zm-17.046 5.22a1.5 1.5 0 0 0-.358-2.98A34.938 34.938 0 0 1 51.5 85c-1.392 0-2.728-.09-4.012-.26a1.5 1.5 0 1 0-.394 2.973A33.44 33.44 0 0 0 51.5 88c1.51 0 3.02-.097 4.523-.278Zm-17.422-2.388a1.5 1.5 0 1 0 1.212-2.745c-2.517-1.11-4.783-2.55-6.816-4.194a1.5 1.5 0 1 0-1.887 2.332c2.216 1.792 4.705 3.377 7.491 4.607ZM24.974 74.523a1.5 1.5 0 1 0 2.338-1.881C25.51 70.403 24 68.076 22.756 65.86a1.5 1.5 0 0 0-2.616 1.47c1.311 2.333 2.911 4.803 4.834 7.193Zm-8.515-15a1.5 1.5 0 0 0 2.796-1.086 49.986 49.986 0 0 1-1-2.813 32.043 32.043 0 0 1-.29-.956l-.013-.045-.002-.01a1.5 1.5 0 0 0-2.899.775l1.45-.388-1.45.388.001.003.002.005.004.018.018.061.064.227c.057.196.143.478.258.837.23.718.579 1.741 1.06 2.984Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.605 21a2.26 2.26 0 0 1-1.614-.665l-6.314-6.318a2.266 2.266 0 0 1 0-3.23l8.45-8.457C10.888 1.57 12.265 1 13.31 1h5.412C19.956 1 21 2.045 21 3.28v5.416c0 .403-.085.856-.233 1.304H19.18c.208-.443.348-.944.348-1.352V3.233a.851.851 0 0 0-.854-.855h-5.365v.047c-.665 0-1.71.428-2.184.903l-8.451 8.456a.832.832 0 0 0 0 1.188l6.314 6.318c.332.332.902.332 1.187 0l1.818-1.82v2.09l-.773.775A2.26 2.26 0 0 1 9.605 21Z" fill="currentColor"/>
<path d="m7.991 20.335-.177.177.177-.177Zm-6.314-6.318.176-.177-.176.177Zm0-3.23.176.176-.176-.177Zm8.45-8.457-.176-.177.177.177ZM20.768 10v.25h.181l.057-.172-.238-.078Zm-1.587 0-.226-.106-.168.356h.394V10Zm-5.871-7.622v-.25h-.25v.25h.25Zm0 .047v.25h.25v-.25h-.25Zm-2.184.903-.177-.177.177.177Zm-8.451 8.456.176.177-.176-.177Zm0 1.188-.177.176.177-.176Zm6.314 6.318-.177.177.177-.177Zm1.187 0-.177-.177-.007.007-.006.007.19.163Zm1.818-1.82h.25v-.604l-.426.428.176.176Zm0 2.09.177.177.073-.073v-.103h-.25Zm-.773.775.176.177-.176-.177Zm-3.406.177a2.51 2.51 0 0 0 1.791.738v-.5a2.01 2.01 0 0 1-1.437-.592l-.354.354ZM1.5 14.193l6.314 6.319.354-.354-6.315-6.318-.353.353Zm0-3.583c-1 1-1 2.583 0 3.583l.353-.353a2.016 2.016 0 0 1 0-2.877L1.5 10.61Zm8.45-8.457L1.5 10.61l.353.353 8.451-8.456-.353-.354ZM13.31.75c-.564 0-1.202.153-1.794.4-.592.246-1.156.595-1.564 1.003l.353.354c.352-.352.856-.668 1.403-.896.548-.229 1.12-.361 1.602-.361v-.5Zm5.412 0H13.31v.5h5.412v-.5Zm2.529 2.53c0-1.373-1.156-2.53-2.529-2.53v.5c1.096 0 2.029.933 2.029 2.03h.5Zm0 5.416V3.28h-.5v5.416h.5Zm-.245 1.382c.154-.466.245-.946.245-1.382h-.5c0 .37-.078.797-.22 1.226l.475.156Zm-1.825.172h1.587v-.5H19.18v.5Zm.098-1.602c0 .36-.126.823-.324 1.246l.452.213c.218-.464.372-1.002.372-1.459h-.5Zm0-5.415v5.415h.5V3.233h-.5Zm-.604-.605c.336 0 .604.268.604.605h.5c0-.613-.491-1.105-1.104-1.105v.5Zm-5.365 0h5.365v-.5h-5.365v.5Zm.25-.203v-.047h-.5v.047h.5Zm-2.257 1.08c.205-.206.552-.416.939-.576.386-.159.78-.254 1.068-.254v-.5c-.377 0-.839.119-1.259.292-.42.173-.833.414-1.102.684l.354.354ZM2.85 11.96l8.452-8.456-.354-.354-8.451 8.456.353.354Zm0 .834a.582.582 0 0 1 0-.834l-.353-.354c-.43.43-.43 1.111 0 1.541l.353-.353Zm6.315 6.318L2.85 12.795l-.353.353 6.314 6.319.354-.354Zm.82.014c-.178.208-.577.23-.82-.014l-.354.354c.422.422 1.162.443 1.554-.015l-.38-.325Zm1.832-1.833-1.819 1.82.354.352 1.818-1.819-.353-.353Zm.426 2.267v-2.09h-.5v2.09h.5Zm-.847.95.774-.774-.353-.353-.774.774.353.354Zm-1.79.739a2.51 2.51 0 0 0 1.79-.738l-.353-.354a2.01 2.01 0 0 1-1.438.592v.5ZM20.988 20v-5c0-.55-.45-1-1-1h-5.996c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h5.996c.55 0 1-.45 1-1Zm-2.998-2.5c0 .55-.45 1-1 1s-1-.45-1-1 .45-1 1-1 1 .45 1 1Z" fill="currentColor"/>
<path d="M19.489 16v-2.5c0-1.4-1.1-2.5-2.499-2.5s-2.498 1.1-2.498 2.5V16" stroke="currentColor" stroke-opacity=".8" stroke-miterlimit="10"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.916 9.556c-.111-.111-.111-.223-.222-.334L16.356 5.89a1.077 1.077 0 0 0-1.558 0 1.073 1.073 0 0 0 0 1.556l1.446 1.444h-5.118c-.667 0-1.112.444-1.112 1.111s.445 1.111 1.112 1.111h5.118l-1.446 1.445a1.073 1.073 0 0 0 0 1.555c.223.222.556.334.779.334.223 0 .556-.112.779-.334l3.338-3.333c.111-.111.222-.222.222-.333a1.225 1.225 0 0 0 0-.89Z" fill="currentColor"/>
<path d="M13.908 16.778c-1.224.666-2.559 1-3.894 1-4.34 0-7.789-3.445-7.789-7.778s3.45-7.778 7.789-7.778c1.335 0 2.67.334 3.894 1 .556.334 1.224.111 1.558-.444.334-.556.111-1.222-.445-1.556C13.463.444 11.794 0 10.014 0A9.965 9.965 0 0 0 0 10c0 5.556 4.45 10 10.014 10a9.94 9.94 0 0 0 5.007-1.333c.556-.334.667-1 .445-1.556-.334-.444-1.002-.667-1.558-.333Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4 11a8.4 8.4 0 1 1-16.8 0 8.4 8.4 0 0 1 16.8 0Zm1.6 0c0 5.523-4.477 10-10 10S1 16.523 1 11 5.477 1 11 1s10 4.477 10 10Zm-5.895-3.706A.916.916 0 1 1 16.4 8.589l-6.022 6.022a1.05 1.05 0 0 1-1.485 0l-3.2-3.199a.915.915 0 0 1 1.295-1.295l2.258 2.258a.55.55 0 0 0 .778 0l5.081-5.081Z" fill="currentColor"/>
<path d="m16.4 7.293-.141.142.141-.142Zm-1.295 0 .142.142-.142-.141ZM16.4 8.59l-.141-.142.141.142Zm-6.022 6.022.141.141-.141-.141Zm-4.684-3.199.141-.141-.141.141Zm0-1.295-.142-.141.142.141Zm1.294 0-.141.142.141-.142Zm2.258 2.258-.14.142.14-.142Zm.778 0-.141-.141.141.141ZM11 19.6a8.6 8.6 0 0 0 8.6-8.6h-.4a8.2 8.2 0 0 1-8.2 8.2v.4ZM2.4 11a8.6 8.6 0 0 0 8.6 8.6v-.4A8.2 8.2 0 0 1 2.8 11h-.4ZM11 2.4A8.6 8.6 0 0 0 2.4 11h.4A8.2 8.2 0 0 1 11 2.8v-.4Zm8.6 8.6A8.6 8.6 0 0 0 11 2.4v.4a8.2 8.2 0 0 1 8.2 8.2h.4ZM11 21.2c5.633 0 10.2-4.567 10.2-10.2h-.4c0 5.412-4.388 9.8-9.8 9.8v.4ZM.8 11c0 5.633 4.567 10.2 10.2 10.2v-.4c-5.412 0-9.8-4.388-9.8-9.8H.8ZM11 .8C5.367.8.8 5.367.8 11h.4c0-5.412 4.388-9.8 9.8-9.8V.8ZM21.2 11C21.2 5.367 16.633.8 11 .8v.4c5.412 0 9.8 4.388 9.8 9.8h.4Zm-4.659-3.848a1.116 1.116 0 0 0-1.577 0l.283.283a.716.716 0 0 1 1.012 0l.282-.283Zm0 1.578a1.116 1.116 0 0 0 0-1.578l-.282.283c.28.28.28.733 0 1.012l.283.283Zm-6.022 6.022 6.023-6.022-.283-.283-6.023 6.023.283.282Zm-1.767 0a1.25 1.25 0 0 0 1.767 0l-.283-.282a.85.85 0 0 1-1.202 0l-.282.282Zm-3.2-3.199 3.2 3.2.282-.283-3.199-3.2-.283.283Zm0-1.577a1.115 1.115 0 0 0 0 1.577l.283-.282a.715.715 0 0 1 0-1.012l-.283-.283Zm1.578 0a1.115 1.115 0 0 0-1.578 0l.283.283a.715.715 0 0 1 1.012 0l.283-.283Zm2.258 2.258L7.13 9.976l-.283.283 2.258 2.258.283-.283Zm.495 0a.35.35 0 0 1-.495 0l-.283.283a.75.75 0 0 0 1.06 0l-.282-.283Zm5.081-5.082-5.081 5.082.283.283 5.08-5.082-.282-.283Z" fill="currentColor"/>
</svg>
...@@ -168,9 +168,6 @@ export const RESOURCES = { ...@@ -168,9 +168,6 @@ export const RESOURCES = {
user_info: { user_info: {
path: '/api/account/v2/user/info', path: '/api/account/v2/user/info',
}, },
email_resend: {
path: '/api/account/v2/email/resend',
},
custom_abi: { custom_abi: {
path: '/api/account/v2/user/custom_abis{/:id}', path: '/api/account/v2/user/custom_abis{/:id}',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
...@@ -228,6 +225,26 @@ export const RESOURCES = { ...@@ -228,6 +225,26 @@ export const RESOURCES = {
needAuth: true, needAuth: true,
}, },
// AUTH
auth_send_otp: {
path: '/api/account/v2/send_otp',
},
auth_confirm_otp: {
path: '/api/account/v2/confirm_otp',
},
auth_siwe_message: {
path: '/api/account/v2/siwe_message',
},
auth_siwe_verify: {
path: '/api/account/v2/authenticate_via_wallet',
},
auth_link_email: {
path: '/api/account/v2/email/link',
},
auth_link_address: {
path: '/api/account/v2/address/link',
},
// STATS MICROSERVICE API // STATS MICROSERVICE API
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
......
...@@ -10,7 +10,7 @@ type TMarketplaceContext = { ...@@ -10,7 +10,7 @@ type TMarketplaceContext = {
setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void; setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
} }
const MarketplaceContext = createContext<TMarketplaceContext>({ export const MarketplaceContext = createContext<TMarketplaceContext>({
isAutoConnectDisabled: false, isAutoConnectDisabled: false,
setIsAutoConnectDisabled: () => {}, setIsAutoConnectDisabled: () => {},
}); });
......
...@@ -5,8 +5,6 @@ import isBrowser from './isBrowser'; ...@@ -5,8 +5,6 @@ import isBrowser from './isBrowser';
export enum NAMES { export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
INVALID_SESSION='invalid_session',
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex', COLOR_MODE_HEX='chakra-ui-color-mode-hex',
...@@ -28,12 +26,16 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) { ...@@ -28,12 +26,16 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) {
} }
} }
export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) { export function set(name: NAMES, value: string, attributes: Cookies.CookieAttributes = {}) {
attributes.path = '/'; attributes.path = '/';
return Cookies.set(name, value, attributes); return Cookies.set(name, value, attributes);
} }
export function remove(name: NAMES, attributes: Cookies.CookieAttributes = {}) {
return Cookies.remove(name, attributes);
}
export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) { export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) {
return cookieString.split(`${ name }=`)[1]?.split(';')[0]; return cookieString.split(`${ name }=`)[1]?.split(';')[0];
} }
import getErrorObj from './getErrorObj';
export default function getErrorMessage(error: unknown): string | undefined {
const errorObj = getErrorObj(error);
return errorObj && 'message' in errorObj && typeof errorObj.message === 'string' ? errorObj.message : undefined;
}
...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch';
export default function useGetCsrfToken() { export default function useGetCsrfToken() {
const nodeApiFetch = useFetch(); const nodeApiFetch = useFetch();
useQuery({ return useQuery({
queryKey: getResourceKey('csrf'), queryKey: getResourceKey('csrf'),
queryFn: async() => { queryFn: async() => {
if (!isNeedProxy()) { if (!isNeedProxy()) {
......
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useLoginUrl from 'lib/hooks/useLoginUrl';
export default function useIsAccountActionAllowed() {
const queryClient = useQueryClient();
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl();
return React.useCallback(() => {
if (!loginUrl) {
return false;
}
if (!isAuth) {
window.location.assign(loginUrl);
return false;
}
return true;
}, [ isAuth, loginUrl ]);
}
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import config from 'configs/app';
const feature = config.features.account;
export default function useLoginUrl() {
const router = useRouter();
return feature.isEnabled ?
feature.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }) :
undefined;
}
...@@ -5,12 +5,10 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga ...@@ -5,12 +5,10 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga
import config from 'configs/app'; import config from 'configs/app';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
interface ReturnType { interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>; mainNavItems: Array<NavItem | NavGroupItem>;
accountNavItems: Array<NavItem>; accountNavItems: Array<NavItem>;
profileItem: NavItem;
} }
export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem { export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem {
...@@ -312,13 +310,6 @@ export default function useNavItems(): ReturnType { ...@@ -312,13 +310,6 @@ export default function useNavItems(): ReturnType {
}, },
].filter(Boolean); ].filter(Boolean);
const profileItem = { return { mainNavItems, accountNavItems };
text: 'My profile',
nextRoute: { pathname: '/auth/profile' as const },
iconComponent: UserAvatar,
isActive: pathname === '/auth/profile',
};
return { mainNavItems, accountNavItems, profileItem };
}, [ pathname ]); }, [ pathname ]);
} }
...@@ -65,8 +65,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -65,8 +65,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/api/healthz': 'Regular page', '/api/healthz': 'Regular page',
'/api/config': 'Regular page', '/api/config': 'Regular page',
'/api/sprite': 'Regular page', '/api/sprite': 'Regular page',
'/auth/auth0': 'Regular page',
'/auth/unverified-email': 'Regular page',
}; };
export default function getPageOgType(pathname: Route['pathname']) { export default function getPageOgType(pathname: Route['pathname']) {
......
...@@ -69,8 +69,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -69,8 +69,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/healthz': DEFAULT_TEMPLATE, '/api/healthz': DEFAULT_TEMPLATE,
'/api/config': DEFAULT_TEMPLATE, '/api/config': DEFAULT_TEMPLATE,
'/api/sprite': DEFAULT_TEMPLATE, '/api/sprite': DEFAULT_TEMPLATE,
'/auth/auth0': DEFAULT_TEMPLATE,
'/auth/unverified-email': DEFAULT_TEMPLATE,
}; };
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = { const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
...@@ -65,8 +65,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -65,8 +65,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/healthz': '%network_name% node API health check', '/api/healthz': '%network_name% node API health check',
'/api/config': '%network_name% node API app config', '/api/config': '%network_name% node API app config',
'/api/sprite': '%network_name% node API SVG sprite content', '/api/sprite': '%network_name% node API SVG sprite content',
'/auth/auth0': '%network_name% authentication',
'/auth/unverified-email': '%network_name% unverified email',
}; };
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = { const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
...@@ -63,8 +63,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -63,8 +63,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/api/healthz': 'Node API: Health check', '/api/healthz': 'Node API: Health check',
'/api/config': 'Node API: App config', '/api/config': 'Node API: App config',
'/api/sprite': 'Node API: SVG sprite content', '/api/sprite': 'Node API: SVG sprite content',
'/auth/auth0': 'Auth',
'/auth/unverified-email': 'Unverified email',
}; };
export default function getPageType(pathname: Route['pathname']) { export default function getPageType(pathname: Route['pathname']) {
......
...@@ -7,6 +7,8 @@ export enum EventTypes { ...@@ -7,6 +7,8 @@ export enum EventTypes {
LOCAL_SEARCH = 'Local search', LOCAL_SEARCH = 'Local search',
ADD_TO_WALLET = 'Add to wallet', ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access', ACCOUNT_ACCESS = 'Account access',
LOGIN = 'Login',
ACCOUNT_LINK_INFO = 'Account link info',
PRIVATE_TAG = 'Private tag', PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address', VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token', VERIFY_TOKEN = 'Verify token',
...@@ -54,7 +56,27 @@ Type extends EventTypes.ADD_TO_WALLET ? ( ...@@ -54,7 +56,27 @@ Type extends EventTypes.ADD_TO_WALLET ? (
} }
) : ) :
Type extends EventTypes.ACCOUNT_ACCESS ? { Type extends EventTypes.ACCOUNT_ACCESS ? {
'Action': 'Auth0 init' | 'Verification email resent' | 'Logged out'; 'Action': 'Dropdown open' | 'Logged out';
} :
Type extends EventTypes.LOGIN ? (
{
'Action': 'Started';
'Source': string;
} | {
'Action': 'Wallet' | 'Email';
'Source': 'Options selector';
} | {
'Action': 'OTP sent';
'Source': 'Email';
} | {
'Action': 'Success';
'Source': 'Email' | 'Wallet';
}
) :
Type extends EventTypes.ACCOUNT_LINK_INFO ? {
'Source': 'Profile' | 'Login modal' | 'Profile dropdown';
'Status': 'Started' | 'OTP sent' | 'Finished';
'Type': 'Email' | 'Wallet';
} : } :
Type extends EventTypes.PRIVATE_TAG ? { Type extends EventTypes.PRIVATE_TAG ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
...@@ -75,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? { ...@@ -75,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
} : } :
Type extends EventTypes.WALLET_CONNECT ? { Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts' | 'Swap button'; 'Source': 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.WALLET_ACTION ? ( Type extends EventTypes.WALLET_ACTION ? (
......
export const validator = (value: string | undefined) => {
if (!value) {
return true;
}
try {
new URL(value);
return true;
} catch (error) {
return 'Incorrect URL';
}
};
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import useAccount from './useAccount';
export default function useAccountWithDomain(isEnabled: boolean) {
const { address } = useAccount();
const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled);
const domainQuery = useApiQuery('address_domain', {
pathParams: {
chainId: config.chain.id,
address,
},
queryOptions: {
enabled: isQueryEnabled,
refetchOnMount: false,
},
});
return React.useMemo(() => {
return {
address: isEnabled ? address : undefined,
domain: domainQuery.data?.domain?.name,
isLoading: isQueryEnabled && domainQuery.isLoading,
};
}, [ address, domainQuery.data?.domain?.name, domainQuery.isLoading, isEnabled, isQueryEnabled ]);
}
...@@ -8,11 +8,11 @@ interface Params { ...@@ -8,11 +8,11 @@ interface Params {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
} }
export default function useWallet({ source }: Params) { export default function useWeb3Wallet({ source }: Params) {
const { open } = useWeb3Modal(); const { open: openModal } = useWeb3Modal();
const { open: isOpen } = useWeb3ModalState(); const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect(); const { disconnect } = useDisconnect();
const [ isModalOpening, setIsModalOpening ] = React.useState(false); const [ isOpening, setIsOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false); const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const isConnectionStarted = React.useRef(false); const isConnectionStarted = React.useRef(false);
...@@ -21,12 +21,12 @@ export default function useWallet({ source }: Params) { ...@@ -21,12 +21,12 @@ export default function useWallet({ source }: Params) {
}, []); }, []);
const handleConnect = React.useCallback(async() => { const handleConnect = React.useCallback(async() => {
setIsModalOpening(true); setIsOpening(true);
await open(); await openModal();
setIsModalOpening(false); setIsOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
isConnectionStarted.current = true; isConnectionStarted.current = true;
}, [ open, source ]); }, [ openModal, source ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => { const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
if (!isReconnected && isConnectionStarted.current) { if (!isReconnected && isConnectionStarted.current) {
...@@ -46,15 +46,14 @@ export default function useWallet({ source }: Params) { ...@@ -46,15 +46,14 @@ export default function useWallet({ source }: Params) {
const { address, isDisconnected } = useAccount(); const { address, isDisconnected } = useAccount();
const isWalletConnected = isClientLoaded && !isDisconnected && address !== undefined; const isConnected = isClientLoaded && !isDisconnected && address !== undefined;
return { return React.useMemo(() => ({
openModal: open,
isWalletConnected,
address: address || '',
connect: handleConnect, connect: handleConnect,
disconnect: handleDisconnect, disconnect: handleDisconnect,
isModalOpening, isOpen: isOpening || isOpen,
isModalOpen: isOpen, isConnected,
}; address,
openModal,
}), [ handleConnect, handleDisconnect, isOpen, isOpening, isConnected, address, openModal ]);
} }
export const base = { import type { UserInfo } from 'types/api/account';
export const base: UserInfo = {
avatar: 'https://avatars.githubusercontent.com/u/22130104', avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: 'tom@ohhhh.me', email: 'tom@ohhhh.me',
name: 'tom goriunov', name: 'tom goriunov',
nickname: 'tom2drum', nickname: 'tom2drum',
address_hash: null,
}; };
export const withoutEmail = { export const withoutEmail: UserInfo = {
avatar: 'https://avatars.githubusercontent.com/u/22130104', avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: null, email: null,
name: 'tom goriunov', name: 'tom goriunov',
nickname: 'tom2drum', nickname: 'tom2drum',
address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
}; };
...@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev'; ...@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev';
import config from 'configs/app'; import config from 'configs/app';
export function googleReCaptcha(): CspDev.DirectiveDescriptor { export function googleReCaptcha(): CspDev.DirectiveDescriptor {
if (!config.services.reCaptcha.siteKey) { if (!config.services.reCaptchaV3.siteKey) {
return {}; return {};
} }
......
...@@ -6,9 +6,9 @@ import type { RollupType } from 'types/client/rollup'; ...@@ -6,9 +6,9 @@ import type { RollupType } from 'types/client/rollup';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import isNeedProxy from 'lib/api/isNeedProxy';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const adBannerFeature = config.features.adsBanner; const adBannerFeature = config.features.adsBanner;
import isNeedProxy from 'lib/api/isNeedProxy';
import type * as metadata from 'lib/metadata'; import type * as metadata from 'lib/metadata';
export interface Props<Pathname extends Route['pathname'] = never> { export interface Props<Pathname extends Route['pathname'] = never> {
......
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { DAY } from 'lib/consts';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
export function account(req: NextRequest) { export function account(req: NextRequest) {
...@@ -25,37 +22,7 @@ export function account(req: NextRequest) { ...@@ -25,37 +22,7 @@ export function account(req: NextRequest) {
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
if ((isAccountRoute || isProfileRoute)) { if ((isAccountRoute || isProfileRoute)) {
const authUrl = feature.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } }); return NextResponse.redirect(config.app.baseUrl);
return NextResponse.redirect(authUrl);
}
}
// if user hasn't confirmed email yet
if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) {
// if user has both cookies, make redirect to logout
if (apiTokenCookie) {
// yes, we could have checked that the current URL is not the logout URL, but we hadn't
// logout URL is always external URL in auth0.com sub-domain
// at least we hope so
const res = NextResponse.redirect(feature.logoutUrl);
res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again
return res;
}
// if user hasn't seen email verification page, make redirect to it
if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) {
if (!req.nextUrl.pathname.includes('/auth/unverified-email')) {
const url = config.app.baseUrl + route({ pathname: '/auth/unverified-email' });
const res = NextResponse.redirect(url);
res.cookies.set({
name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED,
value: 'true',
expires: Date.now() + 7 * DAY,
});
return res;
}
} }
} }
} }
...@@ -28,9 +28,7 @@ declare module "nextjs-routes" { ...@@ -28,9 +28,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/api-docs"> | StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }> | DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps"> | StaticRoute<"/apps">
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile"> | StaticRoute<"/auth/profile">
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/batches/[number]", { "number": string }> | DynamicRoute<"/batches/[number]", { "number": string }>
| StaticRoute<"/batches"> | StaticRoute<"/batches">
| DynamicRoute<"/blobs/[hash]", { "hash": string }> | DynamicRoute<"/blobs/[hash]", { "hash": string }>
......
...@@ -33,6 +33,7 @@ export default function fetchFactory( ...@@ -33,6 +33,7 @@ export default function fetchFactory(
message: 'API fetch via Next.js proxy', message: 'API fetch via Next.js proxy',
url, url,
// headers, // headers,
// init,
}); });
const body = (() => { const body = (() => {
......
...@@ -22,8 +22,13 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { ...@@ -22,8 +22,13 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
); );
// proxy some headers from API // proxy some headers from API
nextRes.setHeader('x-request-id', apiRes.headers.get('x-request-id') || ''); const requestId = apiRes.headers.get('x-request-id');
nextRes.setHeader('set-cookie', apiRes.headers.get('set-cookie') || ''); requestId && nextRes.setHeader('x-request-id', requestId);
const setCookie = apiRes.headers.raw()['set-cookie'];
setCookie?.forEach((value) => {
nextRes.appendHeader('set-cookie', value);
});
nextRes.setHeader('content-type', apiRes.headers.get('content-type') || ''); nextRes.setHeader('content-type', apiRes.headers.get('content-type') || '');
nextRes.status(apiRes.status).send(apiRes.body); nextRes.status(apiRes.status).send(apiRes.body);
......
import type { NextPage } from 'next';
const Page: NextPage = () => {
return null;
};
export default Page;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { NextPage } from 'next';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import UnverifiedEmail from 'ui/pages/UnverifiedEmail';
const Page: NextPage = () => {
return (
<PageNextJs pathname="/auth/unverified-email">
<UnverifiedEmail/>
</PageNextJs>
);
};
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -10,6 +10,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps'; ...@@ -10,6 +10,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app'; import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContext } from 'lib/contexts/marketplace';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain'; import currentChain from 'lib/web3/currentChain';
import theme from 'theme/theme'; import theme from 'theme/theme';
...@@ -23,6 +24,10 @@ export type Props = { ...@@ -23,6 +24,10 @@ export type Props = {
appContext?: { appContext?: {
pageProps: PageProps; pageProps: PageProps;
}; };
marketplaceContext?: {
isAutoConnectDisabled: boolean;
setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
};
} }
const defaultAppContext = { const defaultAppContext = {
...@@ -35,6 +40,11 @@ const defaultAppContext = { ...@@ -35,6 +40,11 @@ const defaultAppContext = {
}, },
}; };
const defaultMarketplaceContext = {
isAutoConnectDisabled: false,
setIsAutoConnectDisabled: () => {},
};
const wagmiConfig = createConfig({ const wagmiConfig = createConfig({
chains: [ currentChain ], chains: [ currentChain ],
connectors: [ connectors: [
...@@ -49,7 +59,7 @@ const wagmiConfig = createConfig({ ...@@ -49,7 +59,7 @@ const wagmiConfig = createConfig({
}, },
}); });
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketplaceContext = defaultMarketplaceContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -64,11 +74,13 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -64,11 +74,13 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<MarketplaceContext.Provider value={ marketplaceContext }>
<GrowthBookProvider> <GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }> <WagmiProvider config={ wagmiConfig }>
{ children } { children }
</WagmiProvider> </WagmiProvider>
</GrowthBookProvider> </GrowthBookProvider>
</MarketplaceContext.Provider>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
export type IconName = export type IconName =
| "ABI_slim" | "ABI_slim"
| "ABI" | "ABI"
| "API_slim"
| "API" | "API"
| "apps_list" | "apps_list"
| "apps_slim" | "apps_slim"
...@@ -48,7 +49,6 @@ ...@@ -48,7 +49,6 @@
| "donate" | "donate"
| "dots" | "dots"
| "edit" | "edit"
| "email-sent"
| "email" | "email"
| "empty_search_result" | "empty_search_result"
| "ENS_slim" | "ENS_slim"
...@@ -104,6 +104,7 @@ ...@@ -104,6 +104,7 @@
| "output_roots" | "output_roots"
| "payment_link" | "payment_link"
| "plus" | "plus"
| "private_tags_slim"
| "privattags" | "privattags"
| "profile" | "profile"
| "publictags_slim" | "publictags_slim"
...@@ -120,6 +121,7 @@ ...@@ -120,6 +121,7 @@
| "score/score-ok" | "score/score-ok"
| "search" | "search"
| "share" | "share"
| "sign_out"
| "social/canny" | "social/canny"
| "social/coingecko" | "social/coingecko"
| "social/coinmarketcap" | "social/coinmarketcap"
...@@ -165,6 +167,7 @@ ...@@ -165,6 +167,7 @@
| "validator" | "validator"
| "verification-steps/finalized" | "verification-steps/finalized"
| "verification-steps/unfinalized" | "verification-steps/unfinalized"
| "verified_slim"
| "verified" | "verified"
| "wallet" | "wallet"
| "wallets/coinbase" | "wallets/coinbase"
......
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const baseStyle = defineStyle({
textAlign: 'center',
bgColor: 'dialog_bg',
});
const sizes = {
md: defineStyle({
fontSize: 'md',
w: 10,
h: 10,
borderRadius: 'md',
}),
};
const variants = {
outline: defineStyle(
(props) => getOutlinedFieldStyles(props),
),
};
const PinInput = defineStyleConfig({
baseStyle,
sizes,
variants,
defaultProps: {
size: 'md',
},
});
export default PinInput;
...@@ -10,6 +10,7 @@ import Input from './Input'; ...@@ -10,6 +10,7 @@ import Input from './Input';
import Link from './Link'; import Link from './Link';
import Menu from './Menu'; import Menu from './Menu';
import Modal from './Modal'; import Modal from './Modal';
import PinInput from './PinInput';
import Popover from './Popover'; import Popover from './Popover';
import Radio from './Radio'; import Radio from './Radio';
import Select from './Select'; import Select from './Select';
...@@ -36,6 +37,7 @@ const components = { ...@@ -36,6 +37,7 @@ const components = {
Link, Link,
Menu, Menu,
Modal, Modal,
PinInput,
Popover, Popover,
Radio, Radio,
Select, Select,
......
...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools'; ...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
import scrollbar from './foundations/scrollbar'; import scrollbar from './foundations/scrollbar';
import addressEntity from './globals/address-entity'; import addressEntity from './globals/address-entity';
import recaptcha from './globals/recaptcha';
import getDefaultTransitionProps from './utils/getDefaultTransitionProps'; import getDefaultTransitionProps from './utils/getDefaultTransitionProps';
const global = (props: StyleFunctionProps) => ({ const global = (props: StyleFunctionProps) => ({
...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({
}, },
...scrollbar(props), ...scrollbar(props),
...addressEntity(props), ...addressEntity(props),
...recaptcha(),
}); });
export default global; export default global;
const styles = () => {
return {
'.grecaptcha-badge': {
zIndex: 'toast',
},
};
};
export default styles;
...@@ -71,6 +71,7 @@ export interface UserInfo { ...@@ -71,6 +71,7 @@ export interface UserInfo {
name?: string; name?: string;
nickname?: string; nickname?: string;
email: string | null; email: string | null;
address_hash: string | null;
avatar?: string; avatar?: string;
} }
......
...@@ -17,3 +17,6 @@ export type PickByType<T, X> = Record< ...@@ -17,3 +17,6 @@ export type PickByType<T, X> = Record<
{[K in keyof T]: T[K] extends X ? K : never}[keyof T], {[K in keyof T]: T[K] extends X ? K : never}[keyof T],
X X
>; >;
// Make some properties of an object optional
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
import { Button, VStack } from '@chakra-ui/react'; import { Button, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import dayjs from 'lib/date/dayjs';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import AuditComment from './fields/AuditComment'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import AuditCompanyName from './fields/AuditCompanyName'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import AuditProjectName from './fields/AuditProjectName'; import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import AuditProjectUrl from './fields/AuditProjectUrl';
import AuditReportDate from './fields/AuditReportDate';
import AuditReportUrl from './fields/AuditReportUrl';
import AuditSubmitterEmail from './fields/AuditSubmitterEmail';
import AuditSubmitterIsOwner from './fields/AuditSubmitterIsOwner';
import AuditSubmitterName from './fields/AuditSubmitterName';
interface Props { interface Props {
address?: string; address?: string;
...@@ -46,10 +41,11 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -46,10 +41,11 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { handleSubmit, formState, control, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { is_project_owner: false }, defaultValues: { is_project_owner: false },
}); });
const { handleSubmit, formState, setError } = formApi;
const onFormSubmit: SubmitHandler<Inputs> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<Inputs> = React.useCallback(async(data) => {
try { try {
...@@ -94,18 +90,33 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -94,18 +90,33 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
}, [ apiFetch, address, toast, setError, onSuccess ]); }, [ apiFetch, address, toast, setError, onSuccess ]);
return ( return (
<FormProvider { ...formApi }>
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }> <form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<VStack gap={ 5 }> <VStack gap={ 5 } alignItems="flex-start">
<AuditSubmitterName control={ control }/> <FormFieldText<Inputs> name="submitter_name" isRequired placeholder="Submitter name"/>
<AuditSubmitterEmail control={ control }/> <FormFieldEmail<Inputs> name="submitter_email" isRequired placeholder="Submitter email"/>
<AuditSubmitterIsOwner control={ control }/> <FormFieldCheckbox<Inputs, 'is_project_owner'>
<AuditProjectName control={ control }/> name="is_project_owner"
<AuditProjectUrl control={ control }/> label="I'm the contract owner"
<AuditCompanyName control={ control }/> />
<AuditReportUrl control={ control }/> <FormFieldText<Inputs> name="project_name" isRequired placeholder="Project name"/>
<AuditReportDate control={ control }/> <FormFieldUrl<Inputs> name="project_url" isRequired placeholder="Project URL"/>
<FormFieldText<Inputs> name="audit_company_name" isRequired placeholder="Audit company name"/>
<AuditComment control={ control }/> <FormFieldUrl<Inputs> name="audit_report_url" isRequired placeholder="Audit report URL"/>
<FormFieldText<Inputs>
name="audit_publish_date"
type="date"
max={ dayjs().format('YYYY-MM-DD') }
isRequired
placeholder="Audit publish date"
/>
<FormFieldText<Inputs>
name="comment"
placeholder="Comment"
maxH="160px"
rules={{ maxLength: 300 }}
asComponent="Textarea"
/>
</VStack> </VStack>
<Button <Button
type="submit" type="submit"
...@@ -118,6 +129,7 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -118,6 +129,7 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
Send request Send request
</Button> </Button>
</form> </form>
</FormProvider>
); );
}; };
......
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditComment = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'comment'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name }>
<Textarea
{ ...field }
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
/>
<InputPlaceholder text="Comment" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="comment"
control={ control }
render={ renderControl }
rules={{ maxLength: 300 }}
/>
);
};
export default React.memo(AuditComment);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditCompanyName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_company_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Audit company name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_company_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditCompanyName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditProjectName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'project_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Project name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="project_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditProjectName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { validator as validateUrl } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditProjectUrl = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'project_url'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Project URL" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="project_url"
control={ control }
render={ renderControl }
rules={{ required: true, validate: { url: validateUrl } }}
/>
);
};
export default React.memo(AuditProjectUrl);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditCompanyName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_publish_date'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
type="date"
max={ dayjs().format('YYYY-MM-DD') }
/>
<InputPlaceholder text="Audit publish date" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_publish_date"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditCompanyName);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { validator as validateUrl } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditReportUrl = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'audit_report_url'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
autoComplete="off"
/>
<InputPlaceholder text="Audit report URL" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="audit_report_url"
control={ control }
render={ renderControl }
rules={{ required: true, validate: { url: validateUrl } }}
/>
);
};
export default React.memo(AuditReportUrl);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterEmail = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'submitter_email'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
/>
<InputPlaceholder text="Submitter email" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="submitter_email"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: EMAIL_REGEXP }}
/>
);
};
export default React.memo(AuditSubmitterEmail);
import { FormControl } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import CheckboxInput from 'ui/shared/CheckboxInput';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterIsOwner = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'is_project_owner'>['render'] = React.useCallback(({ field }) => {
return (
<FormControl id={ field.name }>
<CheckboxInput<Inputs, 'is_project_owner'>
text="I'm the contract owner"
field={ field }
/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="is_project_owner"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(AuditSubmitterIsOwner);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from '../ContractSubmitAuditForm';
interface Props {
control: Control<Inputs>;
}
const AuditSubmitterName = ({ control }: Props) => {
const renderControl: ControllerProps<Inputs, 'submitter_name'>['render'] = React.useCallback(({ field, fieldState }) => {
return (
<FormControl variant="floating" id={ field.name } isRequired>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
/>
<InputPlaceholder text="Submitter name" error={ fieldState.error }/>
</FormControl>
);
}, [ ]);
return (
<Controller
name="submitter_name"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AuditSubmitterName);
...@@ -3,28 +3,28 @@ import React from 'react'; ...@@ -3,28 +3,28 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useWeb3Wallet from 'lib/web3/useWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import useWallet from 'ui/snippets/walletMenu/useWallet';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
const ContractConnectWallet = ({ isLoading }: Props) => { const ContractConnectWallet = ({ isLoading }: Props) => {
const { isModalOpening, isModalOpen, connect, disconnect, address, isWalletConnected } = useWallet({ source: 'Smart contracts' }); const web3Wallet = useWeb3Wallet({ source: 'Smart contracts' });
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const content = (() => { const content = (() => {
if (!isWalletConnected) { if (!web3Wallet.isConnected) {
return ( return (
<> <>
<span>Disconnected</span> <span>Disconnected</span>
<Button <Button
ml={ 3 } ml={ 3 }
onClick={ connect } onClick={ web3Wallet.connect }
size="sm" size="sm"
variant="outline" variant="outline"
isLoading={ isModalOpening || isModalOpen } isLoading={ web3Wallet.isOpen }
loadingText="Connect wallet" loadingText="Connect wallet"
> >
Connect wallet Connect wallet
...@@ -38,20 +38,20 @@ const ContractConnectWallet = ({ isLoading }: Props) => { ...@@ -38,20 +38,20 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
<Flex alignItems="center"> <Flex alignItems="center">
<span>Connected to </span> <span>Connected to </span>
<AddressEntity <AddressEntity
address={{ hash: address }} address={{ hash: web3Wallet.address || '' }}
truncation={ isMobile ? 'constant' : 'dynamic' } truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 } fontWeight={ 600 }
ml={ 2 } ml={ 2 }
/> />
</Flex> </Flex>
<Button onClick={ disconnect } size="sm" variant="outline">Disconnect</Button> <Button onClick={ web3Wallet.disconnect } size="sm" variant="outline">Disconnect</Button>
</Flex> </Flex>
); );
})(); })();
return ( return (
<Skeleton isLoaded={ !isLoading } mb={ 6 }> <Skeleton isLoaded={ !isLoading } mb={ 6 }>
<Alert status={ address ? 'success' : 'warning' }> <Alert status={ web3Wallet.address ? 'success' : 'warning' }>
{ content } { content }
</Alert> </Alert>
</Skeleton> </Skeleton>
......
...@@ -5,10 +5,10 @@ import React from 'react'; ...@@ -5,10 +5,10 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
...@@ -23,16 +23,12 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -23,16 +23,12 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const isAccountActionAllowed = useIsAccountActionAllowed();
const onFocusCapture = usePreventFocusAfterModalClosing(); const onFocusCapture = usePreventFocusAfterModalClosing();
const handleClick = React.useCallback(() => { const handleAddToFavorite = React.useCallback(() => {
if (!isAccountActionAllowed()) {
return;
}
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen(); watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
!watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' }); !watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' });
}, [ isAccountActionAllowed, watchListId, deleteModalProps, addModalProps ]); }, [ watchListId, deleteModalProps, addModalProps ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } }); const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
...@@ -50,7 +46,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -50,7 +46,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const formData = React.useMemo(() => { const formData = React.useMemo(() => {
if (typeof watchListId !== 'number') { if (typeof watchListId !== 'number') {
return; return { address_hash: hash };
} }
return { return {
...@@ -65,6 +61,8 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -65,6 +61,8 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
return ( return (
<> <>
<AuthGuard onAuthSuccess={ handleAddToFavorite }>
{ ({ onClick }) => (
<Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }> <Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<IconButton <IconButton
isActive={ Boolean(watchListId) } isActive={ Boolean(watchListId) }
...@@ -75,11 +73,13 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -75,11 +73,13 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
pl="6px" pl="6px"
pr="6px" pr="6px"
flexShrink={ 0 } flexShrink={ 0 }
onClick={ handleClick } onClick={ onClick }
icon={ <IconSvg name={ watchListId ? 'star_filled' : 'star_outline' } boxSize={ 5 }/> } icon={ <IconSvg name={ watchListId ? 'star_filled' : 'star_outline' } boxSize={ 5 }/> }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
/> />
</Tooltip> </Tooltip>
) }
</AuthGuard>
<WatchlistAddModal <WatchlistAddModal
{ ...addModalProps } { ...addModalProps }
isAdd isAdd
...@@ -87,7 +87,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -87,7 +87,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
onSuccess={ handleAddOrDeleteSuccess } onSuccess={ handleAddOrDeleteSuccess }
data={ formData } data={ formData }
/> />
{ formData && ( { formData.id && (
<DeleteAddressModal <DeleteAddressModal
{ ...deleteModalProps } { ...deleteModalProps }
onClose={ handleDeleteModalClose } onClose={ handleDeleteModalClose }
......
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFirstStepFields, RootFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg" mt={ 8 }>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldAddress);
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Textarea
{ ...field }
required
isInvalid={ Boolean(error) }
isReadOnly
autoComplete="off"
maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/>
<InputPlaceholder text="Message to sign" error={ error }/>
</FormControl>
);
}, [ formState.errors ]);
return (
<Controller
defaultValue="some value"
name="message"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AddressVerificationFieldMessage);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import { SIGNATURE_REGEXP } from 'lib/validations/signature';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" bgColor="dialog_bg">
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Signature hash" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return (
<Controller
name="signature"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: SIGNATURE_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldSignature);
import { Alert, Box, Button, Flex } from '@chakra-ui/react'; import { Alert, Box, Button, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { import type {
AddressVerificationResponseError, AddressVerificationResponseError,
...@@ -16,10 +16,10 @@ import { route } from 'nextjs-routes'; ...@@ -16,10 +16,10 @@ import { route } from 'nextjs-routes';
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 useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields; type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props { interface Props {
...@@ -34,7 +34,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -34,7 +34,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
address: defaultAddress, address: defaultAddress,
}, },
}); });
const { handleSubmit, formState, control, setError, clearErrors, watch } = formApi; const { handleSubmit, formState, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const address = watch('address'); const address = watch('address');
...@@ -100,10 +100,17 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -100,10 +100,17 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
})(); })();
return ( return (
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }> <form noValidate onSubmit={ onSubmit }>
<Box>Enter the contract address you are verifying ownership for.</Box> <Box>Enter the contract address you are verifying ownership for.</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> } { rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/> <FormFieldAddress<Fields>
name="address"
isRequired
bgColor="dialog_bg"
placeholder="Smart contract address (0x...)"
mt={ 8 }
/>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
<Button size="lg" type="submit" isLoading={ formState.isSubmitting } loadingText="Continue" flexShrink={ 0 }> <Button size="lg" type="submit" isLoading={ formState.isSubmitting } loadingText="Continue" flexShrink={ 0 }>
Continue Continue
...@@ -111,6 +118,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -111,6 +118,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
<AdminSupportText/> <AdminSupportText/>
</Flex> </Flex>
</form> </form>
</FormProvider>
); );
}; };
......
...@@ -2,7 +2,7 @@ import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chak ...@@ -2,7 +2,7 @@ import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chak
import { useWeb3Modal } from '@web3modal/wagmi/react'; import { useWeb3Modal } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useSignMessage, useAccount } from 'wagmi'; import { useSignMessage, useAccount } from 'wagmi';
import type { import type {
...@@ -19,11 +19,10 @@ import config from 'configs/app'; ...@@ -19,11 +19,10 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString'; import shortenString from 'lib/shortenString';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { SIGNATURE_REGEXP } from 'ui/shared/forms/validators/signature';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage';
import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
type Fields = RootFields & AddressVerificationFormSecondStepFields; type Fields = RootFields & AddressVerificationFormSecondStepFields;
type SignMethod = 'wallet' | 'manual'; type SignMethod = 'wallet' | 'manual';
...@@ -45,7 +44,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -45,7 +44,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
message: signingMessage, message: signingMessage,
}, },
}); });
const { handleSubmit, formState, control, setValue, getValues, setError, clearErrors, watch } = formApi; const { handleSubmit, formState, setValue, getValues, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
...@@ -184,6 +183,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -184,6 +183,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
})(); })();
return ( return (
<FormProvider { ...formApi }>
<form noValidate onSubmit={ onSubmit }> <form noValidate onSubmit={ onSubmit }>
{ rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> } { rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<Box mb={ 8 }> <Box mb={ 8 }>
...@@ -214,7 +214,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -214,7 +214,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
<Flex rowGap={ 5 } flexDir="column"> <Flex rowGap={ 5 } flexDir="column">
<div> <div>
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/> <CopyToClipboard text={ signingMessage } ml="auto" display="block"/>
<AddressVerificationFieldMessage formState={ formState } control={ control }/> <FormFieldText<Fields>
name="message"
placeholder="Message to sign"
isRequired
asComponent="Textarea"
isReadOnly
maxH={{ base: '140px', lg: '80px' }}
bgColor="dialog_bg"
/>
</div> </div>
{ !noWeb3Provider && ( { !noWeb3Provider && (
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }> <RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
...@@ -222,13 +230,22 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -222,13 +230,22 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
<Radio value="manual">Sign manually</Radio> <Radio value="manual">Sign manually</Radio>
</RadioGroup> </RadioGroup>
) } ) }
{ signMethod === 'manual' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> } { signMethod === 'manual' && (
<FormFieldText<Fields>
name="signature"
placeholder="Signature hash"
isRequired
rules={{ pattern: SIGNATURE_REGEXP }}
bgColor="dialog_bg"
/>
) }
</Flex> </Flex>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button } { button }
<AdminSupportText/> <AdminSupportText/>
</Flex> </Flex>
</form> </form>
</FormProvider>
); );
}; };
......
import { import {
Box, Box,
Button, Button,
FormControl,
FormLabel,
Input,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
...@@ -16,7 +13,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources'; ...@@ -16,7 +13,7 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
type Props = { type Props = {
data?: ApiKey; data?: ApiKey;
...@@ -32,7 +29,7 @@ type Inputs = { ...@@ -32,7 +29,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
defaultValues: { defaultValues: {
token: data?.api_key || '', token: data?.api_key || '',
...@@ -81,80 +78,54 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -81,80 +78,54 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<ApiKeyErrors>) => { onError: (error: ResourceErrorAccount<ApiKeyErrors>) => {
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.name) { if (errorMap?.name) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = useCallback((data) => { const onSubmit: SubmitHandler<Inputs> = useCallback(async(data) => {
setAlertVisible(false); setAlertVisible(false);
mutation.mutate(data); await mutation.mutateAsync(data);
}, [ mutation, setAlertVisible ]); }, [ mutation, setAlertVisible ]);
const renderTokenInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'token'>}) => {
return ( return (
<FormControl variant="floating" id="address"> <FormProvider { ...formApi }>
<Input <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
{ ...field }
bgColor="dialog_bg"
isReadOnly
/>
<FormLabel>Auto-generated API key token</FormLabel>
</FormControl>
);
}, []);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/>
</FormControl>
);
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
{ data && ( { data && (
<Box marginBottom={ 5 }> <FormFieldText<Inputs>
<Controller
name="token" name="token"
control={ control } placeholder="Auto-generated API key token"
render={ renderTokenInput } isReadOnly
bgColor="dialog_bg"
mb={ 5 }
/> />
</Box>
) } ) }
<Box marginBottom={ 8 }> <FormFieldText<Inputs>
<Controller
name="name" name="name"
control={ control } placeholder="Application name for API key (e.g Web3 project)"
isRequired
rules={{ rules={{
maxLength: NAME_MAX_LENGTH, maxLength: NAME_MAX_LENGTH,
required: true,
}} }}
render={ renderNameInput } bgColor="dialog_bg"
mb={ 8 }
/> />
</Box>
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
isDisabled={ !isDirty } isDisabled={ !formApi.formState.isDirty }
isLoading={ mutation.isPending } isLoading={ mutation.isPending }
> >
{ data ? 'Save' : 'Generate API key' } { data ? 'Save' : 'Generate API key' }
</Button> </Button>
</Box> </Box>
</form> </form>
</FormProvider>
); );
}; };
......
...@@ -43,7 +43,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -43,7 +43,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
mode: 'onBlur', mode: 'onBlur',
defaultValues: getDefaultValues(methodFromQuery, config, hash, null), defaultValues: getDefaultValues(methodFromQuery, config, hash, null),
}); });
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi; const { handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>(); const methodNameRef = React.useRef<string>();
...@@ -145,7 +145,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -145,7 +145,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
topic: `addresses:${ address?.toLowerCase() }`, topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: Boolean(address && addressState.error), isDisabled: !address || Boolean(address && addressState.error),
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -191,11 +191,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -191,11 +191,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
<Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
{ !hash && <ContractVerificationFieldAddress/> } { !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldLicenseType/> <ContractVerificationFieldLicenseType/>
<ContractVerificationFieldMethod <ContractVerificationFieldMethod methods={ config.verification_options }/>
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
</Grid> </Grid>
{ content } { content }
{ Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && ( { Boolean(method) && method.value !== 'solidity-hardhat' && method.value !== 'solidity-foundry' && (
......
import { FormControl, Input, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,26 +12,6 @@ interface Props { ...@@ -15,26 +12,6 @@ interface Props {
} }
const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => { const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<> <>
<ContractVerificationFormRow> <ContractVerificationFormRow>
...@@ -43,11 +20,12 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => { ...@@ -43,11 +20,12 @@ const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
</chakra.span> </chakra.span>
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldAddress<FormFields>
name="address" name="address"
control={ control } isRequired
render={ renderControl } placeholder="Smart contract / Address (0x...)"
rules={{ required: true, pattern: ADDRESS_REGEXP }} isReadOnly={ isReadOnly }
size={{ base: 'md', lg: 'lg' }}
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
</> </>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs'; import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldAutodetectArgs = () => { const ContractVerificationFieldAutodetectArgs = () => {
const [ isOn, setIsOn ] = React.useState(true); const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>(); const { resetField } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
!isOn && resetField('constructor_args'); !isOn && resetField('constructor_args');
setIsOn(prev => !prev); setIsOn(prev => !prev);
}, [ isOn, resetField ]); }, [ isOn, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'autodetect_constructor_args'>}) => (
<CheckboxInput<FormFields, 'autodetect_constructor_args'>
text="Try to fetch constructor arguments automatically"
field={ field }
isDisabled={ formState.isSubmitting }
onChange={ handleCheckboxChange }
/>
), [ formState.isSubmitting, handleCheckboxChange ]);
return ( return (
<> <>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldCheckbox<FormFields, 'autodetect_constructor_args'>
name="autodetect_constructor_args" name="autodetect_constructor_args"
control={ control } label="Try to fetch constructor arguments automatically"
render={ renderControl } onChange={ handleCheckboxChange }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> } { !isOn && <ContractVerificationFieldConstructorArgs/> }
......
import { FormControl, Textarea } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,32 +11,14 @@ interface Props { ...@@ -15,32 +11,14 @@ interface Props {
} }
const ContractVerificationFieldCode = ({ isVyper }: Props) => { const ContractVerificationFieldCode = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'code'>}) => {
const error = 'code' in formState.errors ? formState.errors.code : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Textarea
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
required
/>
<InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="code" name="code"
control={ control } isRequired
render={ renderControl } placeholder="Contract code"
rules={{ required: true }} size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/> />
{ isVyper ? null : ( { isVyper ? null : (
<span>If your code utilizes a library or inherits dependencies, we recommend using other verification methods instead.</span> <span>If your code utilizes a library or inherits dependencies, we recommend using other verification methods instead.</span>
......
import { chakra, Checkbox, Code } from '@chakra-ui/react'; import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -22,8 +20,7 @@ interface Props { ...@@ -22,8 +20,7 @@ interface Props {
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false); const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>(); const { formState, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -46,25 +43,6 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -46,25 +43,6 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ isNightly, options ]); }, [ isNightly, options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
return (
<FancySelect
{ ...field }
loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="Compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<> <>
...@@ -78,11 +56,14 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -78,11 +56,14 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
Include nightly builds Include nightly builds
</Checkbox> </Checkbox>
) } ) }
<Controller <FormFieldFancySelect<FormFields, 'compiler'>
name="compiler" name="compiler"
control={ control } placeholder="Compiler (enter version or use the dropdown)"
render={ renderControl } loadOptions={ loadOptions }
rules={{ required: true }} defaultOptions
placeholderIcon={ <IconSvg name="search"/> }
isRequired
isAsync
/> />
</> </>
{ isVyper ? null : ( { isVyper ? null : (
......
import { FormControl, Link, Textarea } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldConstructorArgs = () => { const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea
{ ...field }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/>
<InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="constructor_args" name="constructor_args"
control={ control } isRequired
render={ renderControl } rules={{ maxLength: 255 }}
rules={{ required: true }} placeholder="ABI-encoded Constructor Arguments"
size={{ base: 'md', lg: 'lg' }}
asComponent="Textarea"
/> />
<> <>
<span>Add arguments in </span> <span>Add arguments in </span>
......
import { useUpdateEffect } from '@chakra-ui/react'; import { useUpdateEffect } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -15,8 +13,7 @@ const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/; ...@@ -15,8 +13,7 @@ const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
const ContractVerificationFieldContractIndex = () => { const ContractVerificationFieldContractIndex = () => {
const [ options, setOptions ] = React.useState<Array<Option>>([]); const [ options, setOptions ] = React.useState<Array<Option>>([]);
const { formState, control, watch } = useFormContext<FormFields>(); const { formState, watch } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const sources = watch('sources'); const sources = watch('sources');
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined; const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
...@@ -40,34 +37,18 @@ const ContractVerificationFieldContractIndex = () => { ...@@ -40,34 +37,18 @@ const ContractVerificationFieldContractIndex = () => {
setOptions([]); setOptions([]);
}, [ sources ]); }, [ sources ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'contract_index'>}) => {
const error = 'contract_index' in formState.errors ? formState.errors.contract_index : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract name"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync={ false }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'contract_index'>
name="contract_index" name="contract_index"
control={ control } placeholder="Contract name"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
isAsync={ false }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
import { Link } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -18,8 +15,6 @@ interface Props { ...@@ -18,8 +15,6 @@ interface Props {
} }
const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => { const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -27,29 +22,13 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => { ...@@ -27,29 +22,13 @@ const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
(isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || [] (isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]); ), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => {
const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="EVM Version"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'evm_version'>
name="evm_version" name="evm_version"
control={ control } placeholder="EVM Version"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
/> />
<> <>
<span>The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version. </span> <span>The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version. </span>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldIsYul = () => { const ContractVerificationFieldIsYul = () => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_yul'>}) => (
<CheckboxInput<FormFields, 'is_yul'> text="Is Yul contract" field={ field } isDisabled={ formState.isSubmitting }/>
), [ formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldCheckbox<FormFields, 'is_yul'>
name="is_yul" name="is_yul"
control={ control } label="Is Yul contract"
render={ renderControl }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
...@@ -56,11 +56,9 @@ const ContractVerificationFieldLibraries = () => { ...@@ -56,11 +56,9 @@ const ContractVerificationFieldLibraries = () => {
<ContractVerificationFieldLibraryItem <ContractVerificationFieldLibraryItem
key={ field.id } key={ field.id }
index={ index } index={ index }
control={ control }
fieldsLength={ fields.length } fieldsLength={ fields.length }
onAddFieldClick={ handleAddFieldClick } onAddFieldClick={ handleAddFieldClick }
onRemoveFieldClick={ handleRemoveFieldClick } onRemoveFieldClick={ handleRemoveFieldClick }
error={ 'libraries' in formState.errors ? formState.errors.libraries?.[index] : undefined }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
)) } )) }
......
import { Flex, FormControl, IconButton, Input, Text } from '@chakra-ui/react'; import { Flex, IconButton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Control, ControllerRenderProps, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const LIMIT = 10; const LIMIT = 10;
interface Props { interface Props {
control: Control<FormFields>;
index: number; index: number;
fieldsLength: number; fieldsLength: number;
error?: {
name?: FieldError;
address?: FieldError;
};
onAddFieldClick: (index: number) => void; onAddFieldClick: (index: number) => void;
onRemoveFieldClick: (index: number) => void; onRemoveFieldClick: (index: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
} }
const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, onAddFieldClick, onRemoveFieldClick, error, isDisabled }: Props) => { const ContractVerificationFieldLibraryItem = ({ index, fieldsLength, onAddFieldClick, onRemoveFieldClick, isDisabled }: Props) => {
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLDivElement>(null);
const renderNameControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, `libraries.${ number }.name`>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error?.name) }
isDisabled={ isDisabled }
maxLength={ 255 }
autoComplete="off"
/>
<InputPlaceholder text="Library name (.sol file)" error={ error?.name }/>
</FormControl>
);
}, [ error?.name, isDisabled ]);
const renderAddressControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, `libraries.${ number }.address`>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
isInvalid={ Boolean(error?.address) }
isDisabled={ isDisabled }
required
autoComplete="off"
/>
<InputPlaceholder text="Library address (0x...)" error={ error?.address }/>
</FormControl>
);
}, [ error?.address, isDisabled ]);
const handleAddButtonClick = React.useCallback(() => { const handleAddButtonClick = React.useCallback(() => {
onAddFieldClick(index); onAddFieldClick(index);
}, [ index, onAddFieldClick ]); }, [ index, onAddFieldClick ]);
...@@ -104,11 +66,12 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on ...@@ -104,11 +66,12 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
</Flex> </Flex>
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields, `libraries.${ number }.name`>
name={ `libraries.${ index }.name` } name={ `libraries.${ index }.name` }
control={ control } isRequired
render={ renderNameControl } rules={{ maxLength: 255 }}
rules={{ required: true }} placeholder="Library name (.sol file)"
size={{ base: 'md', lg: 'lg' }}
/> />
{ index === 0 ? ( { index === 0 ? (
<> <>
...@@ -117,11 +80,11 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on ...@@ -117,11 +80,11 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
) : null } ) : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldAddress<FormFields, `libraries.${ number }.address`>
name={ `libraries.${ index }.address` } name={ `libraries.${ index }.address` }
control={ control } isRequired
render={ renderAddressControl } placeholder="Library address (0x...)"
rules={{ required: true, pattern: ADDRESS_REGEXP }} size={{ base: 'md', lg: 'lg' }}
/> />
{ index === 0 ? ( { index === 0 ? (
<> <>
......
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldLicenseType = () => { const options = CONTRACT_LICENSES.map(({ label, title, type }) => ({ label: `${ title } (${ label })`, value: type }));
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'license_type'>}) => {
const error = 'license_type' in formState.errors ? formState.errors.license_type : undefined;
return ( const ContractVerificationFieldLicenseType = () => {
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract license"
isDisabled={ formState.isSubmitting }
error={ error }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldFancySelect<FormFields, 'license_type'>
name="license_type" name="license_type"
control={ control } placeholder="Contract license"
render={ renderControl } options={ options }
/> />
<span> <span>
For best practices, all contract source code holders, publishers and authors are encouraged to also For best practices, all contract source code holders, publishers and authors are encouraged to also
......
...@@ -13,26 +13,22 @@ import { ...@@ -13,26 +13,22 @@ import {
Box, Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/client/contract';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover'; import Popover from 'ui/shared/chakra/Popover';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import { METHOD_LABELS } from '../utils'; import { METHOD_LABELS } from '../utils';
interface Props { interface Props {
control: Control<FormFields>;
isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options']; methods: SmartContractVerificationConfig['verification_options'];
} }
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => { const ContractVerificationFieldMethod = ({ methods }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,21 +37,6 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -41,21 +37,6 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
label: METHOD_LABELS[method], label: METHOD_LABELS[method],
})), [ methods ]); })), [ methods ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Verification method (compiler type)"
isDisabled={ isDisabled }
isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/>
);
}, [ isDisabled, isMobile, options ]);
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => { const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) { switch (method) {
case 'flattened-code': case 'flattened-code':
...@@ -128,11 +109,13 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props ...@@ -128,11 +109,13 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
</Portal> </Portal>
</Popover> </Popover>
</Box> </Box>
<Controller <FormFieldFancySelect<FormFields, 'method'>
name="method" name="method"
control={ control } placeholder="Verification method (compiler type)"
render={ renderControl } options={ options }
rules={{ required: true }} isRequired
isAsync={ false }
isReadOnly={ options.length === 1 }
/> />
</> </>
); );
......
import { chakra, Code, FormControl, Input } from '@chakra-ui/react'; import { chakra, Code } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props { interface Props {
hint?: string; hint?: string;
isReadOnly?: boolean;
} }
const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => { const ContractVerificationFieldName = ({ hint }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
const error = 'name' in formState.errors ? formState.errors.name : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ 255 }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Contract name" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <FormFieldText<FormFields>
name="name" name="name"
control={ control } isRequired
render={ renderControl } placeholder="Contract name"
rules={{ required: true }} size={{ base: 'md', lg: 'lg' }}
rules={{ maxLength: 255 }}
/> />
{ hint ? <span>{ hint }</span> : ( { hint ? <span>{ hint }</span> : (
<> <>
......
import { Flex, Input } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldOptimization = () => { const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(true); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_runs' in formState.errors ? formState.errors.optimization_runs : undefined;
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev); setIsEnabled(prev => !prev);
}, []); }, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled"
field={ field }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
/>
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => {
return (
<Input
{ ...field }
required
isDisabled={ formState.isSubmitting }
autoComplete="off"
type="number"
placeholder="Optimization runs"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/>
);
}, [ error, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}> <Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller <FormFieldCheckbox<FormFields, 'is_optimization_enabled'>
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } label="Optimization enabled"
render={ renderCheckboxControl } onChange={ handleCheckboxChange }
flexShrink={ 0 }
/> />
{ isEnabled && ( { isEnabled && (
<Controller <FormFieldText<FormFields, 'optimization_runs'>
name="optimization_runs" name="optimization_runs"
control={ control } isRequired
render={ renderInputControl } placeholder="Optimization runs"
rules={{ required: true }} type="number"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
/> />
) } ) }
</Flex> </Flex>
......
...@@ -6,10 +6,10 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,10 +6,10 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts'; import { Mb } from 'lib/consts';
import DragAndDropArea from 'ui/shared/forms/DragAndDropArea'; import FieldError from 'ui/shared/forms/components/FieldError';
import FieldError from 'ui/shared/forms/FieldError'; import DragAndDropArea from 'ui/shared/forms/inputs/file/DragAndDropArea';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/inputs/file/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/inputs/file/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
......
import { Box, Link } from '@chakra-ui/react'; import { Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/client/contract'; import type { SmartContractVerificationConfig } from 'types/client/contract';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -17,8 +14,6 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -17,8 +14,6 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
const OPTIONS_LIMIT = 50; const OPTIONS_LIMIT = 50;
const ContractVerificationFieldZkCompiler = () => { const ContractVerificationFieldZkCompiler = () => {
const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
...@@ -32,33 +27,17 @@ const ContractVerificationFieldZkCompiler = () => { ...@@ -32,33 +27,17 @@ const ContractVerificationFieldZkCompiler = () => {
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ options ]); }, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'zk_compiler'>}) => {
const error = 'zk_compiler' in formState.errors ? formState.errors.zk_compiler : undefined;
return ( return (
<FancySelect <ContractVerificationFormRow>
{ ...field } <FormFieldFancySelect<FormFields, 'zk_compiler'>
loadOptions={ loadOptions } name="zk_compiler"
defaultOptions
size={ isMobile ? 'md' : 'lg' }
placeholder="ZK compiler (enter version or use the dropdown)" placeholder="ZK compiler (enter version or use the dropdown)"
placeholderIcon={ <IconSvg name="search"/> } placeholderIcon={ <IconSvg name="search"/> }
isDisabled={ formState.isSubmitting } loadOptions={ loadOptions }
error={ error } defaultOptions
isRequired isRequired
isAsync isAsync
/> />
);
}, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return (
<ContractVerificationFormRow>
<Controller
name="zk_compiler"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
<Box> <Box>
<Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link> <Link isExternal href="https://docs.zksync.io/zk-stack/components/compiler/specification#glossary">zksolc</Link>
<span> compiler version.</span> <span> compiler version.</span>
......
import type { SmartContractLicenseType } from 'types/api/contract'; import type { SmartContractLicenseType } from 'types/api/contract';
import type { SmartContractVerificationMethod } from 'types/client/contract'; import type { SmartContractVerificationMethod } from 'types/client/contract';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/forms/inputs/select/types';
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
......
...@@ -165,7 +165,7 @@ export function getDefaultValues( ...@@ -165,7 +165,7 @@ export function getDefaultValues(
const method = singleMethod || methodParam; const method = singleMethod || methodParam;
if (!method) { if (!method) {
return; return { address: hash || '' };
} }
const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType }; const defaultValues: FormFields = { ...DEFAULT_VALUES[method], address: hash || '', license_type: licenseType };
......
import { Alert, Button, chakra, Flex } from '@chakra-ui/react'; import { Alert, Button, chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import type { CsvExportParams } from 'types/client/address'; import type { CsvExportParams } from 'types/client/address';
import config from 'configs/app';
import buildUrl from 'lib/api/buildUrl'; import buildUrl from 'lib/api/buildUrl';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -43,7 +45,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -43,7 +45,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
to_period: exportType !== 'holders' ? data.to : null, to_period: exportType !== 'holders' ? data.to : null,
filter_type: filterType, filter_type: filterType,
filter_value: filterValue, filter_value: filterValue,
recaptcha_response: data.reCaptcha, recaptcha_v3_response: data.reCaptcha,
}); });
const response = await fetch(url, { const response = await fetch(url, {
...@@ -76,14 +78,17 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -76,14 +78,17 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); }, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]);
const disabledFeatureMessage = ( if (!config.services.reCaptchaV3.siteKey) {
return (
<Alert status="error"> <Alert status="error">
CSV export is not available at the moment since reCaptcha is not configured for this application. CSV export is not available at the moment since reCaptcha is not configured for this application.
Please contact the service maintainer to make necessary changes in the service configuration. Please contact the service maintainer to make necessary changes in the service configuration.
</Alert> </Alert>
); );
}
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<chakra.form <chakra.form
noValidate noValidate
...@@ -92,7 +97,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -92,7 +97,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
<Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap"> <Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
{ exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> } { exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
{ exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> } { exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
<FormFieldReCaptcha disabledFeatureMessage={ disabledFeatureMessage }/> <FormFieldReCaptcha/>
</Flex> </Flex>
<Button <Button
variant="solid" variant="solid"
...@@ -101,12 +106,13 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -101,12 +106,13 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
mt={ 8 } mt={ 8 }
isLoading={ formState.isSubmitting } isLoading={ formState.isSubmitting }
loadingText="Download" loadingText="Download"
isDisabled={ !formState.isValid } isDisabled={ Boolean(formState.errors.from || formState.errors.to) }
> >
Download Download
</Button> </Button>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
</GoogleReCaptchaProvider>
); );
}; };
......
import { FormControl, Input } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize'; import _capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, UseFormReturn } from 'react-hook-form'; import type { UseFormReturn } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
interface Props { interface Props {
formApi: UseFormReturn<FormFields>; formApi: UseFormReturn<FormFields>;
...@@ -15,26 +13,7 @@ interface Props { ...@@ -15,26 +13,7 @@ interface Props {
} }
const CsvExportFormField = ({ formApi, name }: Props) => { const CsvExportFormField = ({ formApi, name }: Props) => {
const { formState, control, getValues, trigger } = formApi; const { formState, getValues, trigger } = formApi;
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'from' | 'to'>}) => {
const error = field.name in formState.errors ? formState.errors[field.name] : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }} maxW={{ base: 'auto', lg: '220px' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
type="date"
isDisabled={ formState.isSubmitting }
autoComplete="off"
max={ dayjs().format('YYYY-MM-DD') }
/>
<InputPlaceholder text={ _capitalize(field.name) } error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
const validate = React.useCallback((newValue: string) => { const validate = React.useCallback((newValue: string) => {
if (name === 'from') { if (name === 'from') {
...@@ -57,11 +36,15 @@ const CsvExportFormField = ({ formApi, name }: Props) => { ...@@ -57,11 +36,15 @@ const CsvExportFormField = ({ formApi, name }: Props) => {
}, [ formState.errors.from, formState.errors.to, getValues, name, trigger ]); }, [ formState.errors.from, formState.errors.to, getValues, name, trigger ]);
return ( return (
<Controller <FormFieldText<FormFields, typeof name>
name={ name } name={ name }
control={ control } type="date"
render={ renderControl } max={ dayjs().format('YYYY-MM-DD') }
rules={{ required: true, validate }} placeholder={ _capitalize(name) }
isRequired
rules={{ validate }}
size={{ base: 'md', lg: 'lg' }}
maxW={{ base: 'auto', lg: '220px' }}
/> />
); );
}; };
......
import { import {
Box, Box,
Button, Button,
FormControl,
Input,
Textarea,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
...@@ -16,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources'; ...@@ -16,9 +13,8 @@ import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
import AddressInput from 'ui/shared/AddressInput'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = { type Props = {
data?: CustomAbi; data?: CustomAbi;
...@@ -35,7 +31,7 @@ type Inputs = { ...@@ -35,7 +31,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isDirty }, handleSubmit, setError } = useForm<Inputs>({ const formApi = useForm<Inputs>({
defaultValues: { defaultValues: {
contract_address_hash: data?.contract_address_hash || '', contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '', name: data?.name || '',
...@@ -85,102 +81,64 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -85,102 +81,64 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onError: (error: ResourceErrorAccount<CustomAbiErrors>) => { onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
const errorMap = error.payload?.errors; const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) { if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) {
errorMap?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') }); errorMap?.address_hash && formApi.setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') }); errorMap?.name && formApi.setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.abi && setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') }); errorMap?.abi && formApi.setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
} else if (errorMap?.identity_id) { } else if (errorMap?.identity_id) {
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') }); formApi.setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
}); });
const onSubmit: SubmitHandler<Inputs> = useCallback((formData) => { const onSubmit: SubmitHandler<Inputs> = useCallback(async(formData) => {
setAlertVisible(false); setAlertVisible(false);
mutation.mutate({ ...formData, id: data?.id ? String(data.id) : undefined }); await mutation.mutateAsync({ ...formData, id: data?.id ? String(data.id) : undefined });
}, [ mutation, data, setAlertVisible ]); }, [ mutation, data, setAlertVisible ]);
const renderContractAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'contract_address_hash'>}) => {
return ( return (
<AddressInput<Inputs, 'contract_address_hash'> <FormProvider { ...formApi }>
field={ field } <form noValidate onSubmit={ formApi.handleSubmit(onSubmit) }>
error={ errors.contract_address_hash } <FormFieldAddress<Inputs>
bgColor="dialog_bg" name="contract_address_hash"
placeholder="Smart contract address (0x...)" placeholder="Smart contract address (0x...)"
/> isRequired
);
}, [ errors ]);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired bgColor="dialog_bg">
<Input
{ ...field }
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
bgColor="dialog_bg" bgColor="dialog_bg"
mb={ 5 }
/> />
<InputPlaceholder text="Project name" error={ errors.name }/> <FormFieldText<Inputs>
</FormControl> name="name"
); placeholder="Project name"
}, [ errors ]); isRequired
const renderAbiInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'abi'>}) => {
return (
<FormControl variant="floating" id="abi" isRequired bgColor="dialog_bg">
<Textarea
{ ...field }
size="lg"
minH="300px"
isInvalid={ Boolean(errors.abi) }
bgColor="dialog_bg"
/>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/>
</FormControl>
);
}, [ errors ]);
return (
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box>
<Controller
name="contract_address_hash"
control={ control }
render={ renderContractAddressInput }
rules={{ rules={{
pattern: ADDRESS_REGEXP, maxLength: NAME_MAX_LENGTH,
required: true,
}} }}
bgColor="dialog_bg"
mb={ 5 }
/> />
</Box> <FormFieldText<Inputs>
<Box marginTop={ 5 }>
<Controller
name="name"
control={ control }
render={ renderNameInput }
rules={{ required: true }}
/>
</Box>
<Box marginTop={ 5 }>
<Controller
name="abi" name="abi"
control={ control } placeholder="Custom ABI [{...}] (JSON format)"
render={ renderAbiInput } isRequired
rules={{ required: true }} asComponent="Textarea"
bgColor="dialog_bg"
size="lg"
minH="300px"
mb={ 8 }
/> />
</Box> <Box>
<Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
isDisabled={ !isDirty } isDisabled={ !formApi.formState.isDirty }
isLoading={ mutation.isPending } isLoading={ mutation.isPending }
> >
{ data ? 'Save' : 'Create custom ABI' } { data ? 'Save' : 'Create custom ABI' }
</Button> </Button>
</Box> </Box>
</form> </form>
</FormProvider>
); );
}; };
......
...@@ -12,7 +12,7 @@ const authTest = test.extend<{ context: BrowserContext }>({ ...@@ -12,7 +12,7 @@ const authTest = test.extend<{ context: BrowserContext }>({
context: contextWithAuth, context: contextWithAuth,
}); });
authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse, mockAssetResponse }) => { authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse }) => {
const IMAGE_URL = 'https://localhost:3000/my-image.png'; const IMAGE_URL = 'https://localhost:3000/my-image.png';
await mockEnvs([ await mockEnvs([
...@@ -28,7 +28,6 @@ authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiRes ...@@ -28,7 +28,6 @@ authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiRes
}); });
await mockApiResponse('user_info', profileMock.base); await mockApiResponse('user_info', profileMock.base);
await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg');
const component = await render(<HeroBanner/>); const component = await render(<HeroBanner/>);
......
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const TEXT_COLOR_DEFAULT = 'white'; const TEXT_COLOR_DEFAULT = 'white';
...@@ -67,9 +67,11 @@ const HeroBanner = () => { ...@@ -67,9 +67,11 @@ const HeroBanner = () => {
} }
</Heading> </Heading>
{ config.UI.navigation.layout === 'vertical' && ( { config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'flex' }}> <Box display={{ base: 'none', lg: 'block' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> } {
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> } (config.features.account.isEnabled && <UserProfileDesktop buttonVariant="hero"/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonVariant="hero"/>)
}
</Box> </Box>
) } ) }
</Flex> </Flex>
......
...@@ -5,9 +5,9 @@ import { route } from 'nextjs-routes'; ...@@ -5,9 +5,9 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { TX } from 'stubs/tx'; import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemMobile from './LatestTxsItemMobile'; import LatestTxsItemMobile from './LatestTxsItemMobile';
......
...@@ -2,11 +2,11 @@ import { Heading } from '@chakra-ui/react'; ...@@ -2,11 +2,11 @@ import { Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useHasAccount from 'lib/hooks/useHasAccount';
import LatestOptimisticDeposits from 'ui/home/latestDeposits/LatestOptimisticDeposits'; import LatestOptimisticDeposits from 'ui/home/latestDeposits/LatestOptimisticDeposits';
import LatestTxs from 'ui/home/LatestTxs'; import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs'; import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useAuth from 'ui/snippets/auth/useIsAuth';
import LatestArbitrumDeposits from './latestDeposits/LatestArbitrumDeposits'; import LatestArbitrumDeposits from './latestDeposits/LatestArbitrumDeposits';
...@@ -17,15 +17,15 @@ const TAB_LIST_PROPS = { ...@@ -17,15 +17,15 @@ const TAB_LIST_PROPS = {
}; };
const TransactionsHome = () => { const TransactionsHome = () => {
const hasAccount = useHasAccount(); const isAuth = useAuth();
if ((rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum')) || hasAccount) { if ((rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum')) || isAuth) {
const tabs = [ const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> }, { id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && rollupFeature.isEnabled && rollupFeature.type === 'optimistic' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestOptimisticDeposits/> }, { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestOptimisticDeposits/> },
rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' &&
{ id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestArbitrumDeposits/> }, { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestArbitrumDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> }, isAuth && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean); ].filter(Boolean);
return ( return (
<> <>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment