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

ReCaptcha: migrate back to v2 solution (#2446)

* show recaptcha in the site footer

* replace badge in footer with text

* add invisible reCaptcha v2

* handle unsolved reCaptcha case

* migrate the rest of components to invisible reCaptcha v2

* remove unused code

* refactoring

* add env for demo

* fix tests

* update values for demo

* update link to secret in vault

* mock recaptcha for tests

* ohhh victor victor

* fix token metadata update test

* deprecate variable for ReCaptcha v3
parent cd27e8fc
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=xxx NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_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
......
...@@ -6,11 +6,11 @@ import { getEnvValue } from '../utils'; ...@@ -6,11 +6,11 @@ import { getEnvValue } from '../utils';
const title = 'My account'; const title = 'My account';
const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => { const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV3.siteKey) { if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV2.siteKey) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
recaptchaSiteKey: services.reCaptchaV3.siteKey, recaptchaSiteKey: services.reCaptchaV2.siteKey,
}); });
} }
......
...@@ -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.reCaptchaV3.siteKey) { if (services.reCaptchaV2.siteKey) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
reCaptcha: { reCaptcha: {
siteKey: services.reCaptchaV3.siteKey, siteKey: services.reCaptchaV2.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.reCaptchaV3.siteKey && addressMetadata.isEnabled && apiHost) { if (services.reCaptchaV2.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({
reCaptchaV3: { reCaptchaV2: {
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY'), siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_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_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
...@@ -52,7 +52,7 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 ...@@ -52,7 +52,7 @@ 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_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom
......
...@@ -143,7 +143,7 @@ function printDeprecationWarning(envsMap: Record<string, string>) { ...@@ -143,7 +143,7 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗'); console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable.'); console.warn('The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n'); console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
} }
...@@ -182,9 +182,9 @@ function printDeprecationWarning(envsMap: Record<string, string>) { ...@@ -182,9 +182,9 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
function checkDeprecatedEnvs(envsMap: Record<string, string>) { function checkDeprecatedEnvs(envsMap: Record<string, string>) {
!silent && console.log(`🌀 Checking deprecated environment variables...`); !silent && console.log(`🌀 Checking deprecated environment variables...`);
if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && !envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { if (!envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
console.log('🚨 The NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variable is no longer supported. Please pass NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY or remove it completely.'); console.log('🚨 The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is no longer supported. Please pass NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY or remove it completely.');
throw new Error(); throw new Error();
} }
......
...@@ -889,8 +889,8 @@ const schema = yup ...@@ -889,8 +889,8 @@ 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(), // DEPRECATED NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(), NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(), // DEPRECATED
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(),
......
...@@ -4,5 +4,5 @@ NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none ...@@ -4,5 +4,5 @@ NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[] NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=deprecated NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
\ No newline at end of file \ No newline at end of file
...@@ -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_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_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
......
...@@ -76,4 +76,4 @@ frontend: ...@@ -76,4 +76,4 @@ frontend:
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_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
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 NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY
...@@ -77,11 +77,12 @@ frontend: ...@@ -77,11 +77,12 @@ frontend:
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']" NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
PROMETHEUS_METRICS_ENABLED: true PROMETHEUS_METRICS_ENABLED: true
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true
envFromSecret: envFromSecret:
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_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
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 NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN
...@@ -12,4 +12,3 @@ ...@@ -12,4 +12,3 @@
| 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 | Replaced 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 |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.0.x+ | v1.36.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY |
...@@ -349,7 +349,7 @@ Settings for meta tags, OG tags and SEO ...@@ -349,7 +349,7 @@ Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ | | NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.36.0+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ | | NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ |
| NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ | | NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ |
...@@ -452,7 +452,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -452,7 +452,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_V3_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.36.0+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -614,6 +614,7 @@ This feature allows you to submit an application with a public address tag. ...@@ -614,6 +614,7 @@ This feature allows you to submit an application with a public address tag.
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ | | NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ |
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ |
&nbsp; &nbsp;
...@@ -848,5 +849,5 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt ...@@ -848,5 +849,5 @@ 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_V3_APP_SITE_KEY | `string` | Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.36.0+ | | NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.36.0+ |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.0.x+ | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.0.x+ |
\ No newline at end of file
...@@ -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.reCaptchaV3.siteKey) { if (!config.services.reCaptchaV2.siteKey) {
return {}; return {};
} }
......
...@@ -78,6 +78,9 @@ const config: PlaywrightTestConfig = defineConfig({ ...@@ -78,6 +78,9 @@ const config: PlaywrightTestConfig = defineConfig({
// Mock for growthbook to test feature flags // Mock for growthbook to test feature flags
{ find: 'lib/growthbook/useFeatureValue', replacement: './playwright/mocks/lib/growthbook/useFeatureValue.js' }, { find: 'lib/growthbook/useFeatureValue', replacement: './playwright/mocks/lib/growthbook/useFeatureValue.js' },
// Mock for reCaptcha hook
{ find: 'ui/shared/reCaptcha/useReCaptcha', replacement: './playwright/mocks/ui/shared/recaptcha/useReCaptcha.js' },
// The createWeb3Modal() function from web3modal/wagmi/react somehow pollutes the global styles which causes the tests to fail // The createWeb3Modal() function from web3modal/wagmi/react somehow pollutes the global styles which causes the tests to fail
// We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module // We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module
// Otherwise it will complain that createWeb3Modal() is no called before the hooks are used // Otherwise it will complain that createWeb3Modal() is no called before the hooks are used
......
const useReCaptcha = () => {
return {
ref: { current: null },
executeAsync: () => Promise.resolve('recaptcha_token'),
};
};
export default useReCaptcha;
const styles = () => { const styles = () => {
return { return {
'.grecaptcha-badge': { '.grecaptcha-badge': {
zIndex: 'toast', visibility: 'hidden',
},
'div:has(div):has(iframe[title="recaptcha challenge expires in two minutes"])': {
'&::after': {
content: `" "`,
display: 'block',
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 100000,
bgColor: 'blackAlpha.300',
},
}, },
}; };
}; };
......
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';
...@@ -13,7 +12,8 @@ import type { ResourceName } from 'lib/api/resources'; ...@@ -13,7 +12,8 @@ import type { ResourceName } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import downloadBlob from 'lib/downloadBlob'; import downloadBlob from 'lib/downloadBlob';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha'; import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import CsvExportFormField from './CsvExportFormField'; import CsvExportFormField from './CsvExportFormField';
...@@ -36,16 +36,23 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -36,16 +36,23 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}); });
const { handleSubmit, formState } = formApi; const { handleSubmit, formState } = formApi;
const toast = useToast(); const toast = useToast();
const recaptcha = useReCaptcha();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
try { try {
const token = await recaptcha.executeAsync();
if (!token) {
throw new Error('ReCaptcha is not solved');
}
const url = buildUrl(resource, { hash } as never, { const url = buildUrl(resource, { hash } as never, {
address_id: hash, address_id: hash,
from_period: exportType !== 'holders' ? data.from : null, from_period: exportType !== 'holders' ? data.from : null,
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_v3_response: data.reCaptcha, recaptcha_response: token,
}); });
const response = await fetch(url, { const response = await fetch(url, {
...@@ -76,9 +83,9 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -76,9 +83,9 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}); });
} }
}, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); }, [ recaptcha, resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]);
if (!config.services.reCaptchaV3.siteKey) { if (!config.services.reCaptchaV2.siteKey) {
return ( 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.
...@@ -88,7 +95,6 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -88,7 +95,6 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
} }
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<chakra.form <chakra.form
noValidate noValidate
...@@ -97,8 +103,8 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -97,8 +103,8 @@ 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/>
</Flex> </Flex>
<ReCaptcha ref={ recaptcha.ref }/>
<Button <Button
variant="solid" variant="solid"
size="lg" size="lg"
...@@ -112,7 +118,6 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -112,7 +118,6 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
</Button> </Button>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
</GoogleReCaptchaProvider>
); );
}; };
......
export interface FormFields { export interface FormFields {
from: string; from: string;
to: string; to: string;
reCaptcha: string;
} }
import { Button, chakra, Heading, useDisclosure } from '@chakra-ui/react'; import { Button, chakra, Heading, useDisclosure } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
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 { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
...@@ -14,8 +13,9 @@ import getErrorMessage from 'lib/errors/getErrorMessage'; ...@@ -14,8 +13,9 @@ import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import AuthModal from 'ui/snippets/auth/AuthModal'; import AuthModal from 'ui/snippets/auth/AuthModal';
import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail'; import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail';
...@@ -34,6 +34,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -34,6 +34,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
const authModal = useDisclosure(); const authModal = useDisclosure();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const recaptcha = useReCaptcha();
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
...@@ -45,12 +46,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -45,12 +46,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(formData) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(formData) => {
try { try {
const token = await recaptcha.executeAsync();
await apiFetch('auth_send_otp', { await apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { body: {
email: formData.email, email: formData.email,
recaptcha_v3_response: formData.reCaptcha, recaptcha_response: token,
}, },
}, },
}); });
...@@ -68,7 +71,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -68,7 +71,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
description: apiError?.message || getErrorMessage(error) || 'Something went wrong', description: apiError?.message || getErrorMessage(error) || 'Something went wrong',
}); });
} }
}, [ apiFetch, authModal, toast ]); }, [ apiFetch, authModal, toast, recaptcha ]);
const hasDirtyFields = Object.keys(formApi.formState.dirtyFields).length > 0; const hasDirtyFields = Object.keys(formApi.formState.dirtyFields).length > 0;
...@@ -82,15 +85,11 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -82,15 +85,11 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
> >
<FormFieldText<FormFields> name="name" placeholder="Name" isReadOnly mb={ 3 }/> <FormFieldText<FormFields> name="name" placeholder="Name" isReadOnly mb={ 3 }/>
<MyProfileFieldsEmail <MyProfileFieldsEmail
isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) } isReadOnly={ !config.services.reCaptchaV2.siteKey || Boolean(profileQuery.data?.email) }
defaultValue={ profileQuery.data?.email || undefined } defaultValue={ profileQuery.data?.email || undefined }
/> />
{ config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && ( { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && <ReCaptcha ref={ recaptcha.ref }/> }
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }> { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && (
<FormFieldReCaptcha/>
</GoogleReCaptchaProvider>
) }
{ config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && (
<Button <Button
mt={ 6 } mt={ 6 }
size="sm" size="sm"
......
export interface FormFields { export interface FormFields {
email: string; email: string;
name: string; name: string;
reCaptcha: string;
} }
...@@ -62,12 +62,6 @@ test('metadata update', async({ render, page, createSocket, mockApiResponse, moc ...@@ -62,12 +62,6 @@ test('metadata update', async({ render, page, createSocket, mockApiResponse, moc
// open the menu, click the button and submit form // open the menu, click the button and submit form
await page.getByLabel('Address menu').click(); await page.getByLabel('Address menu').click();
await page.getByRole('menuitem', { name: 'Refresh metadata' }).click(); await page.getByRole('menuitem', { name: 'Refresh metadata' }).click();
await page.evaluate(() => {
const form = document.querySelector('form');
form && (form.style.display = 'block');
});
await page.getByPlaceholder('reCaptcha token').fill('xxx');
await page.getByRole('button', { name: 'Submit' }).click();
// join socket channel // join socket channel
const channel = await socketServer.joinChannel(socket, `token_instances:${ hash.toLowerCase() }`); const channel = await socketServer.joinChannel(socket, `token_instances:${ hash.toLowerCase() }`);
...@@ -116,12 +110,6 @@ test('metadata update failed', async({ render, page }) => { ...@@ -116,12 +110,6 @@ test('metadata update failed', async({ render, page }) => {
// open the menu, click the button and submit form // open the menu, click the button and submit form
await page.getByLabel('Address menu').click(); await page.getByLabel('Address menu').click();
await page.getByRole('menuitem', { name: 'Refresh metadata' }).click(); await page.getByRole('menuitem', { name: 'Refresh metadata' }).click();
await page.evaluate(() => {
const form = document.querySelector('form');
form && (form.style.display = 'block');
});
await page.getByPlaceholder('reCaptcha token').fill('xxx');
await page.getByRole('button', { name: 'Submit' }).click();
// check that button is not disabled // check that button is not disabled
await page.getByLabel('Address menu').click(); await page.getByLabel('Address menu').click();
......
import { Button, chakra, Grid, GridItem } from '@chakra-ui/react'; import { Button, chakra, Grid, GridItem } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
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';
...@@ -15,10 +14,11 @@ import getErrorObj from 'lib/errors/getErrorObj'; ...@@ -15,10 +14,11 @@ import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl'; import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import PublicTagsSubmitFieldAddresses from './fields/PublicTagsSubmitFieldAddresses'; import PublicTagsSubmitFieldAddresses from './fields/PublicTagsSubmitFieldAddresses';
import PublicTagsSubmitFieldTags from './fields/PublicTagsSubmitFieldTags'; import PublicTagsSubmitFieldTags from './fields/PublicTagsSubmitFieldTags';
...@@ -34,6 +34,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -34,6 +34,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const recaptcha = useReCaptcha();
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
...@@ -56,12 +57,15 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -56,12 +57,15 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
const requestsBody = convertFormDataToRequestsBody(data); const requestsBody = convertFormDataToRequestsBody(data);
const result = await Promise.all(requestsBody.map(async(body) => { const result = await Promise.all(requestsBody.map(async(body) => {
return recaptcha.executeAsync()
.then(() => {
return apiFetch<'public_tag_application', unknown, { message: string }>('public_tag_application', { return apiFetch<'public_tag_application', unknown, { message: string }>('public_tag_application', {
pathParams: { chainId: appConfig.chain.id }, pathParams: { chainId: appConfig.chain.id },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { submission: body }, body: { submission: body },
}, },
});
}) })
.then(() => ({ error: null, payload: body })) .then(() => ({ error: null, payload: body }))
.catch((error: unknown) => { .catch((error: unknown) => {
...@@ -74,9 +78,9 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -74,9 +78,9 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
})); }));
onSubmitResult(result); onSubmitResult(result);
}, [ apiFetch, onSubmitResult ]); }, [ apiFetch, onSubmitResult, recaptcha ]);
if (!appConfig.services.reCaptchaV3.siteKey) { if (!appConfig.services.reCaptchaV2.siteKey) {
return null; return null;
} }
...@@ -85,7 +89,6 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -85,7 +89,6 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
}; };
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<chakra.form <chakra.form
noValidate noValidate
...@@ -130,7 +133,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -130,7 +133,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 3 }}> <GridItem colSpan={{ base: 1, lg: 3 }}>
<FormFieldReCaptcha/> <ReCaptcha ref={ recaptcha.ref }/>
</GridItem> </GridItem>
<Button <Button
...@@ -147,7 +150,6 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -147,7 +150,6 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
</Grid> </Grid>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
</GoogleReCaptchaProvider>
); );
}; };
......
...@@ -9,7 +9,6 @@ export interface FormFields { ...@@ -9,7 +9,6 @@ export interface FormFields {
addresses: Array<{ hash: string }>; addresses: Array<{ hash: string }>;
tags: Array<FormFieldTag>; tags: Array<FormFieldTag>;
description: string | undefined; description: string | undefined;
reCaptcha: string;
} }
export interface FormFieldTag { export interface FormFieldTag {
......
...@@ -5,7 +5,6 @@ describe('function convertFormDataToRequestsBody()', () => { ...@@ -5,7 +5,6 @@ describe('function convertFormDataToRequestsBody()', () => {
it('should convert form data to requests body', () => { it('should convert form data to requests body', () => {
const formData = { const formData = {
...mocks.baseFields, ...mocks.baseFields,
reCaptcha: 'xxx',
addresses: [ { hash: mocks.address1 }, { hash: mocks.address2 } ], addresses: [ { hash: mocks.address1 }, { hash: mocks.address2 } ],
tags: [ convertTagApiFieldsToFormFields(mocks.tag1), convertTagApiFieldsToFormFields(mocks.tag2) ], tags: [ convertTagApiFieldsToFormFields(mocks.tag1), convertTagApiFieldsToFormFields(mocks.tag2) ],
}; };
......
import { Button, Text } from '@chakra-ui/react'; import { Button, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import config from 'configs/app'; import config from 'configs/app';
import buildUrl from 'lib/api/buildUrl'; import buildUrl from 'lib/api/buildUrl';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import AppErrorIcon from '../AppErrorIcon'; import AppErrorIcon from '../AppErrorIcon';
import AppErrorTitle from '../AppErrorTitle'; import AppErrorTitle from '../AppErrorTitle';
...@@ -13,19 +14,16 @@ import AppErrorTitle from '../AppErrorTitle'; ...@@ -13,19 +14,16 @@ import AppErrorTitle from '../AppErrorTitle';
const AppErrorTooManyRequests = () => { const AppErrorTooManyRequests = () => {
const toast = useToast(); const toast = useToast();
const fetch = useFetch(); const fetch = useFetch();
const [ token, setToken ] = React.useState<string | undefined>(undefined); const recaptcha = useReCaptcha();
const handleReCaptchaChange = React.useCallback(async(token: string) => {
setToken(token);
}, [ ]);
const handleSubmit = React.useCallback(async() => { const handleSubmit = React.useCallback(async() => {
try { try {
const token = await recaptcha.executeAsync();
const url = buildUrl('api_v2_key'); const url = buildUrl('api_v2_key');
await fetch(url, { await fetch(url, {
method: 'POST', method: 'POST',
body: { recaptcha_v3_response: token }, body: { recaptcha_response: token },
credentials: 'include', credentials: 'include',
}, { }, {
resource: 'api_v2_key', resource: 'api_v2_key',
...@@ -43,25 +41,22 @@ const AppErrorTooManyRequests = () => { ...@@ -43,25 +41,22 @@ const AppErrorTooManyRequests = () => {
isClosable: true, isClosable: true,
}); });
} }
}, [ token, toast, fetch ]); }, [ recaptcha, toast, fetch ]);
if (!config.services.reCaptchaV3.siteKey) { if (!config.services.reCaptchaV2.siteKey) {
throw new Error('reCAPTCHA V3 site key is not set'); throw new Error('reCAPTCHA V2 site key is not set');
} }
return ( return (
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }> <>
<AppErrorIcon statusCode={ 429 }/> <AppErrorIcon statusCode={ 429 }/>
<AppErrorTitle title="Too many requests"/> <AppErrorTitle title="Too many requests"/>
<Text variant="secondary" mt={ 3 }> <Text variant="secondary" mt={ 3 }>
You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon. You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon.
</Text> </Text>
<GoogleReCaptcha <ReCaptcha ref={ recaptcha.ref }/>
onVerify={ handleReCaptchaChange }
refreshReCaptcha
/>
<Button onClick={ handleSubmit } mt={ 8 }>Try again</Button> <Button onClick={ handleSubmit } mt={ 8 }>Try again</Button>
</GoogleReCaptchaProvider> </>
); );
}; };
......
import React from 'react';
import { GoogleReCaptcha } from 'react-google-recaptcha-v3';
import { useFormContext } from 'react-hook-form';
const FormFieldReCaptcha = () => {
const { register, unregister, clearErrors, setValue, formState } = useFormContext();
React.useEffect(() => {
register('reCaptcha', { required: true, shouldUnregister: true });
return () => {
unregister('reCaptcha');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleReCaptchaChange = React.useCallback((token: string) => {
clearErrors('reCaptcha');
setValue('reCaptcha', token, { shouldValidate: true });
}, [ clearErrors, setValue ]);
return (
<GoogleReCaptcha
onVerify={ handleReCaptchaChange }
refreshReCaptcha={ formState.submitCount ?? -1 }
/>
);
};
export default FormFieldReCaptcha;
import React from 'react';
import ReCaptcha from 'react-google-recaptcha';
import config from 'configs/app';
interface Props {
disabledFeatureMessage?: React.ReactNode;
}
const ReCaptchaInvisible = ({ disabledFeatureMessage }: Props, ref: React.Ref<ReCaptcha>) => {
if (!config.services.reCaptchaV2.siteKey) {
return disabledFeatureMessage ?? null;
}
return (
<ReCaptcha
ref={ ref }
sitekey={ config.services.reCaptchaV2.siteKey }
size="invisible"
/>
);
};
export default React.forwardRef(ReCaptchaInvisible);
import React from 'react';
import type ReCAPTCHA from 'react-google-recaptcha';
export default function useReCaptcha() {
const ref = React.useRef<ReCAPTCHA>(null);
const rejectCb = React.useRef<((error: Error) => void) | null>(null);
const [ isOpen, setIsOpen ] = React.useState(false);
const executeAsync = React.useCallback(async() => {
setIsOpen(true);
const tokenPromise = ref.current?.executeAsync() || Promise.reject(new Error('Unable to execute ReCaptcha'));
const modalOpenPromise = new Promise<void>((resolve, reject) => {
rejectCb.current = reject;
});
return Promise.race([ tokenPromise, modalOpenPromise ]);
}, [ ref ]);
const handleContainerClick = React.useCallback(() => {
setIsOpen(false);
rejectCb.current?.(new Error('ReCaptcha is not solved'));
}, []);
React.useEffect(() => {
if (!isOpen) {
return;
}
const container = window.document.querySelector('div:has(div):has(iframe[title="recaptcha challenge expires in two minutes"])');
container?.addEventListener('click', handleContainerClick);
return () => {
container?.removeEventListener('click', handleContainerClick);
};
}, [ isOpen, handleContainerClick ]);
return React.useMemo(() => ({ ref, executeAsync }), [ ref, executeAsync ]);
}
...@@ -2,7 +2,6 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve ...@@ -2,7 +2,6 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { Screen, ScreenSuccess } from './types'; import type { Screen, ScreenSuccess } from './types';
...@@ -183,9 +182,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro ...@@ -183,9 +182,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
</ModalHeader> </ModalHeader>
<ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/> <ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
<GoogleReCaptchaProvider reCaptchaKey={ feature.recaptchaSiteKey }>
{ content } { content }
</GoogleReCaptchaProvider>
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
......
import { chakra, Button, Text } from '@chakra-ui/react'; import { chakra, Button, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
...@@ -12,6 +11,8 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; ...@@ -12,6 +11,8 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
interface Props { interface Props {
onSubmit: (screen: Screen) => void; onSubmit: (screen: Screen) => void;
...@@ -27,7 +28,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -27,7 +28,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { executeRecaptcha } = useGoogleReCaptcha(); const recaptcha = useReCaptcha();
const formApi = useForm<EmailFormFields>({ const formApi = useForm<EmailFormFields>({
mode: 'onBlur', mode: 'onBlur',
...@@ -38,13 +39,14 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -38,13 +39,14 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => { const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => {
try { try {
const token = await executeRecaptcha?.(); const token = await recaptcha.executeAsync();
await apiFetch('auth_send_otp', { await apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { body: {
email: formData.email, email: formData.email,
recaptcha_v3_response: token, recaptcha_response: token,
}, },
}, },
}); });
...@@ -68,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -68,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
description: getErrorObjPayload<{ message: string }>(error)?.message || getErrorMessage(error) || 'Something went wrong', description: getErrorObjPayload<{ message: string }>(error)?.message || getErrorMessage(error) || 'Something went wrong',
}); });
} }
}, [ executeRecaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]); }, [ recaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -93,6 +95,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -93,6 +95,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
> >
Send a code Send a code
</Button> </Button>
<ReCaptcha ref={ recaptcha.ref }/>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
); );
......
import { chakra, Box, Text, Button } from '@chakra-ui/react'; import { chakra, Box, Text, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
...@@ -12,6 +11,8 @@ import getErrorMessage from 'lib/errors/getErrorMessage'; ...@@ -12,6 +11,8 @@ import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import AuthModalFieldOtpCode from '../fields/AuthModalFieldOtpCode'; import AuthModalFieldOtpCode from '../fields/AuthModalFieldOtpCode';
...@@ -25,7 +26,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -25,7 +26,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { executeRecaptcha } = useGoogleReCaptcha(); const recaptcha = useReCaptcha();
const [ isCodeSending, setIsCodeSending ] = React.useState(false); const [ isCodeSending, setIsCodeSending ] = React.useState(false);
const formApi = useForm<OtpCodeFormFields>({ const formApi = useForm<OtpCodeFormFields>({
...@@ -72,11 +73,11 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -72,11 +73,11 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
try { try {
formApi.clearErrors('code'); formApi.clearErrors('code');
setIsCodeSending(true); setIsCodeSending(true);
const token = await executeRecaptcha?.(); const token = await recaptcha.executeAsync();
await apiFetch('auth_send_otp', { await apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { email, recaptcha_v3_response: token }, body: { email, recaptcha_response: token },
}, },
}); });
...@@ -96,7 +97,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -96,7 +97,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
} finally { } finally {
setIsCodeSending(false); setIsCodeSending(false);
} }
}, [ apiFetch, email, executeRecaptcha, formApi, toast ]); }, [ apiFetch, email, formApi, toast, recaptcha ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -110,6 +111,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -110,6 +111,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
and enter your code below. and enter your code below.
</Text> </Text>
<AuthModalFieldOtpCode isDisabled={ isCodeSending }/> <AuthModalFieldOtpCode isDisabled={ isCodeSending }/>
<ReCaptcha ref={ recaptcha.ref }/>
<Button <Button
variant="link" variant="link"
display="flex" display="flex"
......
...@@ -164,6 +164,22 @@ const Footer = () => { ...@@ -164,6 +164,22 @@ const Footer = () => {
m: '0 auto', m: '0 auto',
}; };
const renderRecaptcha = (gridArea?: GridProps['gridArea']) => {
if (!config.services.reCaptchaV2.siteKey) {
return <Box gridArea={ gridArea }/>;
}
return (
<Box gridArea={ gridArea } fontSize="xs" lineHeight={ 5 } mt={ 6 } color="text">
<span>This site is protected by reCAPTCHA and the Google </span>
<Link href="https://policies.google.com/privacy" isExternal>Privacy Policy</Link>
<span> and </span>
<Link href="https://policies.google.com/terms" isExternal>Terms of Service</Link>
<span> apply.</span>
</Box>
);
};
if (config.UI.footer.links) { if (config.UI.footer.links) {
return ( return (
<Box { ...containerProps }> <Box { ...containerProps }>
...@@ -171,6 +187,7 @@ const Footer = () => { ...@@ -171,6 +187,7 @@ const Footer = () => {
<div> <div>
{ renderNetworkInfo() } { renderNetworkInfo() }
{ renderProjectInfo() } { renderProjectInfo() }
{ renderRecaptcha() }
</div> </div>
<Grid <Grid
...@@ -212,12 +229,14 @@ const Footer = () => { ...@@ -212,12 +229,14 @@ const Footer = () => {
lg: ` lg: `
"network links-top" "network links-top"
"info links-bottom" "info links-bottom"
"recaptcha links-bottom"
`, `,
}} }}
> >
{ renderNetworkInfo({ lg: 'network' }) } { renderNetworkInfo({ lg: 'network' }) }
{ renderProjectInfo({ lg: 'info' }) } { renderProjectInfo({ lg: 'info' }) }
{ renderRecaptcha({ lg: 'recaptcha' }) }
<Grid <Grid
gridArea={{ lg: 'links-bottom' }} gridArea={{ lg: 'links-bottom' }}
......
import type { ToastId } from '@chakra-ui/react'; import type { ToastId } from '@chakra-ui/react';
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } from '@chakra-ui/react'; import { Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } 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 { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
...@@ -11,9 +10,12 @@ import config from 'configs/app'; ...@@ -11,9 +10,12 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import { MINUTE, SECOND } from 'lib/consts'; import { MINUTE, SECOND } from 'lib/consts';
import getErrorMessage from 'lib/errors/getErrorMessage';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
import { useMetadataUpdateContext } from './contexts/metadataUpdate'; import { useMetadataUpdateContext } from './contexts/metadataUpdate';
...@@ -30,6 +32,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -30,6 +32,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const recaptcha = useReCaptcha();
const handleRefreshError = React.useCallback(() => { const handleRefreshError = React.useCallback(() => {
setStatus?.('ERROR'); setStatus?.('ERROR');
...@@ -42,15 +45,16 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -42,15 +45,16 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
}); });
}, [ setStatus, toast ]); }, [ setStatus, toast ]);
const initializeUpdate = React.useCallback((reCaptchaToken: string) => { const initializeUpdate = React.useCallback(async(tokenProp?: string) => {
apiFetch<'token_instance_refresh_metadata', unknown, unknown>('token_instance_refresh_metadata', { try {
const token = tokenProp || await recaptcha.executeAsync();
await apiFetch<'token_instance_refresh_metadata', unknown, unknown>('token_instance_refresh_metadata', {
pathParams: { hash, id }, pathParams: { hash, id },
fetchParams: { fetchParams: {
method: 'PATCH', method: 'PATCH',
body: { recaptcha_v3_response: reCaptchaToken }, body: { recaptcha_response: token },
}, },
}) });
.then(() => {
setStatus?.('WAITING_FOR_RESPONSE'); setStatus?.('WAITING_FOR_RESPONSE');
toastId.current = toast({ toastId.current = toast({
title: 'Please wait', title: 'Please wait',
...@@ -61,34 +65,21 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -61,34 +65,21 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
isClosable: false, isClosable: false,
}); });
timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE); timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE);
}) } catch (error) {
.catch(() => {
toast({ toast({
title: 'Error', title: 'Error',
description: 'Unable to initialize metadata update', description: getErrorMessage(error) || 'Unable to initialize metadata update',
status: 'warning', status: 'warning',
}); });
setStatus?.('ERROR'); setStatus?.('ERROR');
}); }
}, [ apiFetch, handleRefreshError, hash, id, setStatus, toast ]);
}, [ apiFetch, handleRefreshError, hash, id, recaptcha, setStatus, toast ]);
const handleModalClose = React.useCallback(() => { const handleModalClose = React.useCallback(() => {
setStatus?.('INITIAL'); setStatus?.('INITIAL');
}, [ setStatus ]); }, [ setStatus ]);
const handleReCaptchaChange = React.useCallback((token: string | null) => {
if (token) {
initializeUpdate(token);
}
}, [ initializeUpdate ]);
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = React.useCallback((event) => {
event.preventDefault();
const data = new FormData(event.target as HTMLFormElement);
const token = data.get('recaptcha_token');
typeof token === 'string' && initializeUpdate(token);
}, [ initializeUpdate ]);
const handleSocketMessage: SocketMessage.TokenInstanceMetadataFetched['handler'] = React.useCallback((payload) => { const handleSocketMessage: SocketMessage.TokenInstanceMetadataFetched['handler'] = React.useCallback((payload) => {
if (String(payload.token_id) !== id) { if (String(payload.token_id) !== id) {
return; return;
...@@ -141,6 +132,15 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -141,6 +132,15 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
handler: handleSocketMessage, handler: handleSocketMessage,
}); });
React.useEffect(() => {
if (status !== 'MODAL_OPENED') {
return;
}
const timeoutId = window.setTimeout(initializeUpdate, 100);
return () => window.clearTimeout(timeoutId);
}, [ status, initializeUpdate ]);
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
timeoutId.current && window.clearTimeout(timeoutId.current); timeoutId.current && window.clearTimeout(timeoutId.current);
...@@ -161,25 +161,12 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -161,25 +161,12 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</ModalHeader>
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody mb={ 0 } minH="78px"> <ModalBody mb={ 0 } minH="78px">
{ config.services.reCaptchaV3.siteKey ? ( { config.services.reCaptchaV2.siteKey ? (
<> <>
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
<Center h="80px"> <Center h="80px">
<Spinner size="lg"/> <Spinner size="lg"/>
</Center> </Center>
<GoogleReCaptcha <ReCaptcha ref={ recaptcha.ref }/>
onVerify={ handleReCaptchaChange }
refreshReCaptcha
/>
</GoogleReCaptchaProvider>
{ /* ONLY FOR TEST PURPOSES */ }
<chakra.form noValidate onSubmit={ handleFormSubmit } display="none">
<chakra.input
name="recaptcha_token"
placeholder="reCaptcha token"
/>
<chakra.button type="submit">Submit</chakra.button>
</chakra.form>
</> </>
) : ( ) : (
<Alert status="error"> <Alert status="error">
......
...@@ -15010,7 +15010,7 @@ prompts@^2.0.1: ...@@ -15010,7 +15010,7 @@ prompts@^2.0.1:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.5.0, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
...@@ -15252,6 +15252,14 @@ rc@^1.2.7: ...@@ -15252,6 +15252,14 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-async-script@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"
react-clientside-effect@^1.2.6: react-clientside-effect@^1.2.6:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
...@@ -15307,12 +15315,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4: ...@@ -15307,12 +15315,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4:
use-callback-ref "^1.3.0" use-callback-ref "^1.3.0"
use-sidecar "^1.1.2" use-sidecar "^1.1.2"
react-google-recaptcha-v3@1.10.1: react-google-recaptcha@3.1.0:
version "1.10.1" version "3.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz#5b125bc0dec123206431860e8800e188fc735aff" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
integrity sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ== integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
dependencies: dependencies:
hoist-non-react-statics "^3.3.2" prop-types "^15.5.0"
react-async-script "^1.2.0"
react-hook-form@7.52.1: react-hook-form@7.52.1:
version "7.52.1" version "7.52.1"
......
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