Commit 8acf8445 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #2108 from blockscout/get-gas-button

Add a "Get gas" button
parents f06a987a 0dfb1255
import type { Feature } from './types';
import type { GasRefuelProviderConfig } from 'types/client/gasRefuelProviderConfig';
import chain from '../chain';
import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace';
const value = parseEnvJson<GasRefuelProviderConfig>(getEnvValue('NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG'));
const title = 'Get gas button';
const config: Feature<{
name: string;
logoUrl?: string;
url: string;
dappId?: string;
}> = (() => {
if (value) {
return Object.freeze({
title,
isEnabled: true,
name: value.name,
logoUrl: value.logo,
url: value.url_template.replace('{chainId}', chain.id || ''),
dappId: marketplace.isEnabled ? value.dapp_id : undefined,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -11,6 +11,7 @@ export { default as dataAvailability } from './dataAvailability'; ...@@ -11,6 +11,7 @@ export { default as dataAvailability } from './dataAvailability';
export { default as deFiDropdown } from './deFiDropdown'; export { default as deFiDropdown } from './deFiDropdown';
export { default as faultProofSystem } from './faultProofSystem'; export { default as faultProofSystem } from './faultProofSystem';
export { default as gasTracker } from './gasTracker'; export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton';
export { default as googleAnalytics } from './googleAnalytics'; export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook'; export { default as growthBook } from './growthBook';
......
...@@ -40,6 +40,7 @@ NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs ...@@ -40,6 +40,7 @@ NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -13,6 +13,7 @@ import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_A ...@@ -13,6 +13,7 @@ import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_A
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders'; import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract'; import type { ContractCodeIde } from '../../../types/client/contract';
import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown'; import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
import { GAS_UNITS } from '../../../types/client/gasTracker'; import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker'; import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
...@@ -654,6 +655,19 @@ const schema = yup ...@@ -654,6 +655,19 @@ const schema = yup
dapp_id: yup.string(), dapp_id: yup.string(),
}); });
return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<GasRefuelProviderConfig>().transform(replaceQuotes).json().shape({
name: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string(),
dapp_id: yup.string(),
});
return isUndefined || valueSchema.isValidSync(data); return isUndefined || valueSchema.isValidSync(data);
}), }),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE), NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
......
...@@ -78,3 +78,4 @@ NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket'] ...@@ -78,3 +78,4 @@ NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}] NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
...@@ -61,6 +61,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -61,6 +61,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [OpenTelemetry](ENVS.md#opentelemetry) - [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#defi-dropdown) - [Swap button](ENVS.md#defi-dropdown)
- [Multichain balance button](ENVS.md#multichain-balance-button) - [Multichain balance button](ENVS.md#multichain-balance-button)
- [Get gas button](ENVS.md#get-gas-button)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp; &nbsp;
...@@ -708,6 +709,27 @@ If the feature is enabled, a Multichain balance button will be displayed on the ...@@ -708,6 +709,27 @@ If the feature is enabled, a Multichain balance button will be displayed on the
&nbsp; &nbsp;
### Get gas button
If the feature is enabled, a Get gas button will be displayed in the top bar, which will take you to the gas refuel application in the marketplace or to an external site.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Get gas button config. See [below](#get-gas-button-configuration-properties) | - | - | `{ name: 'Need gas?', dapp_id: 'smol-refuel', url_template: 'https://smolrefuel.com/?outboundChain={chainId}', logo: 'https://example.com/icon.png' }` | v1.33.0+ |
&nbsp;
#### Get gas button configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Text on the button | Required | - | `Need gas?` |
| url_template | `string` | Url template, may contain `{chainId}` variable | Required | - | `https://smolrefuel.com/?outboundChain={chainId}` |
| dapp_id | `string` | Set for open a Blockscout dapp page instead of opening external app page | - | - | `smol-refuel` |
| logo | `string` | Gas refuel application logo url | - | - | `https://example.com/icon.png` |
&nbsp;
## External services configuration ## External services configuration
### Google ReCaptcha ### Google ReCaptcha
......
export type GasRefuelProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
};
import { Image, Box } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
const getGasFeature = config.features.getGasButton;
const GetGasButton = () => {
const isMobile = useIsMobile(false);
const onGetGasClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Get gas', Source: 'address' });
}, []);
if (getGasFeature.isEnabled && !isMobile) {
try {
const dappId = getGasFeature.dappId;
const urlObj = new URL(getGasFeature.url);
urlObj.searchParams.append('utm_source', 'blockscout');
urlObj.searchParams.append('utm_medium', 'address');
const url = urlObj.toString();
const isInternal = typeof dappId === 'string';
const Link = isInternal ? LinkInternal : LinkExternal;
const href = isInternal ? route({ pathname: '/apps/[id]', query: { id: dappId, url } }) : url;
return (
<>
<Box h="1px" w="8px" bg="divider" mx={ 1 }/>
<Link
href={ href }
display="flex"
alignItems="center"
fontSize="xs"
lineHeight={ 5 }
onClick={ onGetGasClick }
>
{ getGasFeature.logoUrl && (
<Image
src={ getGasFeature.logoUrl }
alt={ getGasFeature.name }
boxSize="14px"
mr={ 1 }
/>
) }
{ getGasFeature.name }
</Link>
</>
);
} catch (error) {}
}
return null;
};
export default GetGasButton;
...@@ -61,3 +61,19 @@ test('with DeFi dropdown +@dark-mode +@mobile', async({ render, page, mockApiRes ...@@ -61,3 +61,19 @@ test('with DeFi dropdown +@dark-mode +@mobile', async({ render, page, mockApiRes
await component.getByText(/DeFi/i).click(); await component.getByText(/DeFi/i).click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
}); });
test('with Get gas button', async({ render, mockApiResponse, mockEnvs, mockAssetResponse }) => {
const ICON_URL = 'https://localhost:3000/my-icon.png';
await mockEnvs([
[
'NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG',
`{"name": "Need gas?", "dapp_id": "duck", "url_template": "https://duck.url/{chainId}", "logo": "${ ICON_URL }"}`,
],
]);
await mockApiResponse('stats', statsMock.base);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await render(<TopBar/>);
await expect(component).toHaveScreenshot();
});
...@@ -10,6 +10,8 @@ import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip'; ...@@ -10,6 +10,8 @@ import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice'; import GasPrice from 'ui/shared/gas/GasPrice';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import GetGasButton from './GetGasButton';
const TopBarStats = () => { const TopBarStats = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -76,14 +78,17 @@ const TopBarStats = () => { ...@@ -76,14 +78,17 @@ const TopBarStats = () => {
) } ) }
{ data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> } { data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && ( { data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
<Skeleton isLoaded={ !isPlaceholderData }> <>
<chakra.span color="text_secondary">Gas </chakra.span> <Skeleton isLoaded={ !isPlaceholderData }>
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }> <chakra.span color="text_secondary">Gas </chakra.span>
<Link> <GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
<GasPrice data={ data.gas_prices.average }/> <Link>
</Link> <GasPrice data={ data.gas_prices.average }/>
</GasInfoTooltip> </Link>
</Skeleton> </GasInfoTooltip>
</Skeleton>
{ !isPlaceholderData && <GetGasButton/> }
</>
) } ) }
</Flex> </Flex>
); );
......
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