Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a "Get gas" button #2108

Merged
merged 12 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions configs/app/features/getGasButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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;
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { default as dataAvailability } from './dataAvailability';
export { default as deFiDropdown } from './deFiDropdown';
export { default as faultProofSystem } from './faultProofSystem';
export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton';
export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
Expand Down
3 changes: 2 additions & 1 deletion configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_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_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
Expand All @@ -59,4 +60,4 @@ NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
14 changes: 14 additions & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ContractCodeIde } from '../../../types/client/contract';
import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
Expand Down Expand Up @@ -638,6 +639,19 @@ const schema = yup
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(),
});
maxaleks marked this conversation as resolved.
Show resolved Hide resolved

return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/test/.env.base
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
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_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'}
22 changes: 22 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#defi-dropdown)
- [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)

&nbsp;
Expand Down Expand Up @@ -706,6 +707,27 @@ If the feature is enabled, a Multichain balance button will be displayed on the

&nbsp;

### Get gas button
maxaleks marked this conversation as resolved.
Show resolved Hide resolved

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

### Google ReCaptcha
Expand Down
6 changes: 6 additions & 0 deletions types/client/gasRefuelProviderConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type GasRefuelProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
};
64 changes: 64 additions & 0 deletions ui/snippets/topBar/GetGasButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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;
16 changes: 16 additions & 0 deletions ui/snippets/topBar/TopBar.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,19 @@ test('with DeFi dropdown +@dark-mode +@mobile', async({ render, page, mockApiRes
await component.getByText(/DeFi/i).click();
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();
});
21 changes: 13 additions & 8 deletions ui/snippets/topBar/TopBarStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import TextSeparator from 'ui/shared/TextSeparator';

import GetGasButton from './GetGasButton';

const TopBarStats = () => {
const isMobile = useIsMobile();

Expand Down Expand Up @@ -76,14 +78,17 @@ const TopBarStats = () => {
) }
{ data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
<Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">Gas </chakra.span>
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
<Link>
<GasPrice data={ data.gas_prices.average }/>
</Link>
</GasInfoTooltip>
</Skeleton>
<>
<Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">Gas </chakra.span>
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
<Link>
<GasPrice data={ data.gas_prices.average }/>
</Link>
</GasInfoTooltip>
</Skeleton>
{ !isPlaceholderData && <GetGasButton/> }
</>
) }
</Flex>
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading