Commit ae26fd9f authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #2283 from blockscout/rewards

Rewards (merits)
parents be89167e e53ef3d7
...@@ -23,6 +23,7 @@ export { default as multichainButton } from './multichainButton'; ...@@ -23,6 +23,7 @@ export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService'; export { default as nameService } from './nameService';
export { default as publicTagsSubmission } from './publicTagsSubmission'; export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as rewards } from './rewards';
export { default as rollup } from './rollup'; export { default as rollup } from './rollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
export { default as saveOnGas } from './saveOnGas'; export { default as saveOnGas } from './saveOnGas';
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import account from './account';
import blockchainInteraction from './blockchainInteraction';
const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST');
const title = 'Rewards service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost && account.isEnabled && blockchainInteraction.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -38,7 +38,7 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com ...@@ -38,7 +38,7 @@ 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_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','/account/rewards']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
...@@ -59,4 +59,6 @@ NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global ...@@ -59,4 +59,6 @@ NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
\ No newline at end of file NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com
...@@ -836,6 +836,7 @@ const schema = yup ...@@ -836,6 +836,7 @@ const schema = yup
return isUndefined || valueSchema.isValidSync(data); return isUndefined || valueSchema.isValidSync(data);
}), }),
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST: yup.string().test(urlTest),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -85,3 +85,4 @@ NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap' ...@@ -85,3 +85,4 @@ NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'
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'} 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_SAVE_ON_GAS_ENABLED=true NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://example.com
...@@ -64,6 +64,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -64,6 +64,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Multichain balance button](ENVS.md#multichain-balance-button) - [Multichain balance button](ENVS.md#multichain-balance-button)
- [Get gas button](ENVS.md#get-gas-button) - [Get gas button](ENVS.md#get-gas-button)
- [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk) - [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk)
- [Rewards service API](ENVS.md#rewards-service-api)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp; &nbsp;
...@@ -793,6 +794,14 @@ The feature enables a "Save with GasHawk" button next to the "Gas used" value on ...@@ -793,6 +794,14 @@ The feature enables a "Save with GasHawk" button next to the "Gas used" value on
&nbsp; &nbsp;
### Rewards service API
This feature enables Blockscout Merits program. It requires that the [My account](ENVS.md#my-account) and [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) features are also enabled.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_REWARDS_SERVICE_API_HOST | `string` | API URL | - | - | `https://example.com` | v1.36.0+ |
## External services configuration ## External services configuration
### Google ReCaptcha ### Google ReCaptcha
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3 3.7a1.7 1.7 0 0 1 3.4 0v.903a1 1 0 1 0 2 0V3.7a3.7 3.7 0 0 0-7.4 0v6.272a1 1 0 0 0 2 0V3.7Zm5.4 6.302a1 1 0 0 0-2 0V16.3a1.7 1.7 0 0 1-3.4 0v-.914a1 1 0 1 0-2 0v.914a3.7 3.7 0 1 0 7.4 0v-6.298ZM3.692 8.3C2.76 8.3 2 9.059 2 10c0 .94.76 1.7 1.693 1.7H10a1 1 0 1 1 0 2H3.693A3.696 3.696 0 0 1 0 10C0 7.96 1.65 6.3 3.693 6.3h.902a1 1 0 0 1 0 2h-.902ZM10 6.3a1 1 0 0 0 0 2h6.294C17.238 8.3 18 9.064 18 10c0 .937-.761 1.7-1.705 1.7h-.865a1 1 0 1 0 0 2h.865A3.702 3.702 0 0 0 20 10c0-2.046-1.66-3.7-3.705-3.7H10Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.3 3.7a1.7 1.7 0 0 1 3.4 0v.903a1 1 0 1 0 2 0V3.7a3.7 3.7 0 0 0-7.4 0v6.272a1 1 0 0 0 2 0V3.7Zm5.4 6.302a1 1 0 0 0-2 0V16.3a1.7 1.7 0 0 1-3.4 0v-.914a1 1 0 1 0-2 0v.914a3.7 3.7 0 1 0 7.4 0v-6.298ZM3.692 8.3C2.76 8.3 2 9.059 2 10c0 .94.76 1.7 1.693 1.7H10a1 1 0 1 1 0 2H3.693A3.696 3.696 0 0 1 0 10c0-2.04 1.65-3.7 3.693-3.7h.902a1 1 0 0 1 0 2h-.902ZM10 6.3a1 1 0 0 0 0 2h6.294C17.238 8.3 18 9.064 18 10c0 .937-.761 1.7-1.705 1.7h-.865a1 1 0 1 0 0 2h.865A3.702 3.702 0 0 0 20 10c0-2.046-1.66-3.7-3.705-3.7H10Z" fill="currentColor"/>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" fill="none">
<path d="M13.229 9.946c.401 0 .727.329.727.735v.636a.731.731 0 0 1-.727.735h-.353a.731.731 0 0 0-.727.735v6.532a.731.731 0 0 1-.727.735h-.695A.731.731 0 0 1 10 19.32v-6.532c0-.406.326-.735.727-.735h.352c.402 0 .728-.33.728-.736v-.635a.73.73 0 0 1 .727-.735h.694Zm4.311 0c.402 0 .728.329.728.735v.636c0 .405.326.735.727.735h.278c.401 0 .727.329.727.735v6.532a.731.731 0 0 1-.727.735h-.695a.731.731 0 0 1-.727-.735v-6.532a.731.731 0 0 0-.727-.735h-.278a.731.731 0 0 1-.727-.736v-.635c0-.406.326-.735.727-.735h.695Zm-2.178 4.031c.402 0 .727.33.727.735v2.622a.731.731 0 0 1-.727.735h-.694a.731.731 0 0 1-.728-.735v-2.622c0-.406.326-.735.728-.735h.694Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.894 4.213a1.98 1.98 0 0 0-1.788 0l-8 4.044A2.024 2.024 0 0 0 5 10.065v9.87c0 .766.428 1.466 1.106 1.808l8 4.044a1.981 1.981 0 0 0 1.788 0l8-4.044A2.024 2.024 0 0 0 25 19.935v-9.87c0-.766-.428-1.466-1.106-1.808l-8-4.044ZM7 10.065l8-4.043 8 4.043v9.87l-8 4.043-8-4.043v-9.87Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 40 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.787 1.923a3.574 3.574 0 0 0-3.574 0l-14.706 8.49a3.574 3.574 0 0 0-1.788 3.096v16.982c0 1.277.682 2.457 1.788 3.095l14.706 8.49a3.574 3.574 0 0 0 3.574 0l14.706-8.49a3.574 3.574 0 0 0 1.788-3.095V13.509a3.574 3.574 0 0 0-1.788-3.095l-14.706-8.49Z" fill="url(#a)" stroke="#fff" stroke-width="1.92"/>
<path d="M18.693 2.755a2.614 2.614 0 0 1 2.614 0l14.706 8.49a2.614 2.614 0 0 1 1.308 2.264v16.982c0 .934-.499 1.797-1.308 2.264l-14.706 8.49a2.614 2.614 0 0 1-2.614 0l-14.706-8.49a2.614 2.614 0 0 1-1.308-2.265V13.51c0-.935.499-1.798 1.308-2.265l14.706-8.49Z" fill="url(#b)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.33 15.164c0-.643-.522-1.164-1.164-1.164h-1.111c-.643 0-1.164.521-1.164 1.164v1.006c0 .642-.521 1.163-1.164 1.163h-.563c-.643 0-1.164.521-1.164 1.164v10.34c0 .642.521 1.163 1.164 1.163h1.11c.643 0 1.164-.521 1.164-1.164V18.497c0-.643.521-1.164 1.164-1.164h.564c.642 0 1.163-.52 1.163-1.163v-1.006Zm6.899 0c0-.643-.521-1.164-1.164-1.164h-1.11c-.643 0-1.164.521-1.164 1.164v1.006c0 .642.52 1.163 1.163 1.163h.444c.643 0 1.164.521 1.164 1.164v10.34c0 .642.52 1.163 1.163 1.163h1.111C27.48 30 28 29.479 28 28.836v-10.34c0-.642-.521-1.163-1.164-1.163h-.443a1.164 1.164 0 0 1-1.164-1.163v-1.006Zm-3.486 6.38c0-.642-.521-1.163-1.164-1.163h-1.11c-.643 0-1.164.521-1.164 1.164v4.149c0 .642.52 1.163 1.163 1.163h1.111c.643 0 1.164-.52 1.164-1.163v-4.15Z" fill="#fff"/>
<defs>
<linearGradient id="a" x1="20" y1="2" x2="20" y2="42" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C5282"/>
<stop offset="1" stop-color="#153967"/>
</linearGradient>
<linearGradient id="b" x1="20" y1="2" x2="20" y2="42" gradientUnits="userSpaceOnUse">
<stop stop-color="#008BE4"/>
<stop offset="1" stop-color="#81C5F1"/>
</linearGradient>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M10.229 6.946c.401 0 .727.329.727.735v.636a.731.731 0 0 1-.727.735h-.353a.731.731 0 0 0-.727.735v6.532a.731.731 0 0 1-.727.735h-.695A.731.731 0 0 1 7 16.32V9.787c0-.406.326-.735.727-.735h.353c.401 0 .727-.33.727-.736v-.635a.73.73 0 0 1 .727-.735h.694Zm4.311 0a.73.73 0 0 1 .728.735v.635c0 .407.326.736.727.736h.278c.401 0 .727.329.727.735v6.532a.731.731 0 0 1-.727.735h-.695a.731.731 0 0 1-.727-.735V9.787a.731.731 0 0 0-.727-.735h-.278a.731.731 0 0 1-.727-.735V7.68c0-.406.326-.735.727-.735h.695Zm-2.178 4.031c.402 0 .727.33.727.735v2.622a.731.731 0 0 1-.727.735h-.694a.731.731 0 0 1-.728-.736v-2.62c0-.407.326-.736.728-.736h.694Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.894 1.213a1.98 1.98 0 0 0-1.788 0l-8 4.044A2.024 2.024 0 0 0 2 7.065v9.87c0 .766.428 1.466 1.106 1.808l8 4.044a1.981 1.981 0 0 0 1.788 0l8-4.044A2.024 2.024 0 0 0 22 16.935v-9.87c0-.766-.428-1.466-1.106-1.808l-8-4.044ZM4 7.065l8-4.043 8 4.043v9.87l-8 4.043-8-4.043v-9.87Z" fill="currentColor"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="m19.133 5.85-3.239-1.637a1.98 1.98 0 0 0-1.788 0l-8 4.044A2.024 2.024 0 0 0 5 10.065v9.87c0 .766.428 1.466 1.106 1.808l8 4.044a1.981 1.981 0 0 0 1.788 0l8-4.044A2.024 2.024 0 0 0 25 19.935V11.9a5.022 5.022 0 0 1-2 0v8.035l-8 4.043-8-4.043v-9.87l8-4.043 4.123 2.083a5.02 5.02 0 0 1 .01-2.255Zm-5.177 4.83a.731.731 0 0 0-.727-.734h-.695a.731.731 0 0 0-.727.735v.636a.731.731 0 0 1-.728.735h-.352a.731.731 0 0 0-.727.735v6.532c0 .406.326.735.727.735h.695a.731.731 0 0 0 .727-.735v-6.532c0-.406.326-.735.727-.735h.353c.401 0 .727-.33.727-.736v-.635Zm4.312 0a.731.731 0 0 0-.727-.734h-.695a.731.731 0 0 0-.727.735v.636c0 .406.326.735.727.735h.278a.73.73 0 0 1 .727.735v6.532c0 .406.326.735.727.735h.695a.731.731 0 0 0 .727-.735v-6.532a.731.731 0 0 0-.727-.735h-.278a.731.731 0 0 1-.727-.736v-.635Zm-2.906 3.297c.402 0 .727.33.727.735v2.622a.731.731 0 0 1-.727.735h-.694a.731.731 0 0 1-.728-.735v-2.622c0-.406.326-.735.728-.735h.694Z" fill="currentColor"/>
<circle cx="24" cy="7" r="3" fill="#E53E3E"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.133 2.85-3.239-1.637a1.98 1.98 0 0 0-1.788 0l-8 4.044A2.024 2.024 0 0 0 2 7.065v9.87c0 .766.428 1.466 1.106 1.808l8 4.044a1.981 1.981 0 0 0 1.788 0l8-4.044A2.024 2.024 0 0 0 22 16.935V8.9a5.022 5.022 0 0 1-2 0v8.035l-8 4.043-8-4.043v-9.87l8-4.043 4.123 2.083a5.02 5.02 0 0 1 .01-2.255Zm-5.177 4.83a.731.731 0 0 0-.727-.734h-.695a.731.731 0 0 0-.727.735v.635a.731.731 0 0 1-.727.736h-.353A.731.731 0 0 0 7 9.787v6.532c0 .406.326.735.727.735h.695a.731.731 0 0 0 .727-.735V9.787c0-.406.326-.735.727-.735h.353c.401 0 .727-.33.727-.735V7.68Zm4.312 0a.731.731 0 0 0-.727-.734h-.695a.731.731 0 0 0-.727.735v.636c0 .406.326.735.727.735h.278c.401 0 .727.329.727.735v6.532c0 .406.326.735.727.735h.695a.731.731 0 0 0 .727-.735V9.787a.731.731 0 0 0-.727-.735h-.278a.731.731 0 0 1-.727-.736v-.635Zm-2.906 3.297c.402 0 .727.33.727.735v2.622a.731.731 0 0 1-.727.735h-.694a.731.731 0 0 1-.728-.736v-2.62c0-.407.326-.736.728-.736h.694Z" fill="currentColor"/>
<circle cx="21" cy="4" r="3" fill="#E53E3E"/>
</svg>
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.605 21a2.26 2.26 0 0 1-1.614-.665l-6.314-6.318a2.266 2.266 0 0 1 0-3.23l8.45-8.457C10.888 1.57 12.265 1 13.31 1h5.412C19.956 1 21 2.045 21 3.28v5.416c0 .403-.085.856-.233 1.304H19.18c.208-.443.348-.944.348-1.352V3.233a.851.851 0 0 0-.854-.855h-5.365v.047c-.665 0-1.71.428-2.184.903l-8.451 8.456a.832.832 0 0 0 0 1.188l6.314 6.318c.332.332.902.332 1.187 0l1.818-1.82v2.09l-.773.775A2.26 2.26 0 0 1 9.605 21Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.605 21a2.26 2.26 0 0 1-1.614-.665l-6.314-6.318a2.266 2.266 0 0 1 0-3.23l8.45-8.457C10.888 1.57 12.265 1 13.31 1h5.412C19.956 1 21 2.045 21 3.28v5.416c0 .403-.085.856-.233 1.304H19.18c.208-.443.348-.944.348-1.352V3.233a.851.851 0 0 0-.854-.855h-5.365v.047c-.665 0-1.71.428-2.184.903l-8.451 8.456a.832.832 0 0 0 0 1.188l6.314 6.318c.332.332.902.332 1.187 0l1.818-1.82v2.09l-.773.775A2.26 2.26 0 0 1 9.605 21Z" fill="currentColor"/>
<path d="m7.991 20.335-.177.177.177-.177Zm-6.314-6.318.176-.177-.176.177Zm0-3.23.176.176-.176-.177Zm8.45-8.457-.176-.177.177.177ZM20.768 10v.25h.181l.057-.172-.238-.078Zm-1.587 0-.226-.106-.168.356h.394V10Zm-5.871-7.622v-.25h-.25v.25h.25Zm0 .047v.25h.25v-.25h-.25Zm-2.184.903-.177-.177.177.177Zm-8.451 8.456.176.177-.176-.177Zm0 1.188-.177.176.177-.176Zm6.314 6.318-.177.177.177-.177Zm1.187 0-.177-.177-.007.007-.006.007.19.163Zm1.818-1.82h.25v-.604l-.426.428.176.176Zm0 2.09.177.177.073-.073v-.103h-.25Zm-.773.775.176.177-.176-.177Zm-3.406.177a2.51 2.51 0 0 0 1.791.738v-.5a2.01 2.01 0 0 1-1.437-.592l-.354.354ZM1.5 14.193l6.314 6.319.354-.354-6.315-6.318-.353.353Zm0-3.583c-1 1-1 2.583 0 3.583l.353-.353a2.016 2.016 0 0 1 0-2.877L1.5 10.61Zm8.45-8.457L1.5 10.61l.353.353 8.451-8.456-.353-.354ZM13.31.75c-.564 0-1.202.153-1.794.4-.592.246-1.156.595-1.564 1.003l.353.354c.352-.352.856-.668 1.403-.896.548-.229 1.12-.361 1.602-.361v-.5Zm5.412 0H13.31v.5h5.412v-.5Zm2.529 2.53c0-1.373-1.156-2.53-2.529-2.53v.5c1.096 0 2.029.933 2.029 2.03h.5Zm0 5.416V3.28h-.5v5.416h.5Zm-.245 1.382c.154-.466.245-.946.245-1.382h-.5c0 .37-.078.797-.22 1.226l.475.156Zm-1.825.172h1.587v-.5H19.18v.5Zm.098-1.602c0 .36-.126.823-.324 1.246l.452.213c.218-.464.372-1.002.372-1.459h-.5Zm0-5.415v5.415h.5V3.233h-.5Zm-.604-.605c.336 0 .604.268.604.605h.5c0-.613-.491-1.105-1.104-1.105v.5Zm-5.365 0h5.365v-.5h-5.365v.5Zm.25-.203v-.047h-.5v.047h.5Zm-2.257 1.08c.205-.206.552-.416.939-.576.386-.159.78-.254 1.068-.254v-.5c-.377 0-.839.119-1.259.292-.42.173-.833.414-1.102.684l.354.354ZM2.85 11.96l8.452-8.456-.354-.354-8.451 8.456.353.354Zm0 .834a.582.582 0 0 1 0-.834l-.353-.354c-.43.43-.43 1.111 0 1.541l.353-.353Zm6.315 6.318L2.85 12.795l-.353.353 6.314 6.319.354-.354Zm.82.014c-.178.208-.577.23-.82-.014l-.354.354c.422.422 1.162.443 1.554-.015l-.38-.325Zm1.832-1.833-1.819 1.82.354.352 1.818-1.819-.353-.353Zm.426 2.267v-2.09h-.5v2.09h.5Zm-.847.95.774-.774-.353-.353-.774.774.353.354Zm-1.79.739a2.51 2.51 0 0 0 1.79-.738l-.353-.354a2.01 2.01 0 0 1-1.438.592v.5ZM20.988 20v-5c0-.55-.45-1-1-1h-5.996c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h5.996c.55 0 1-.45 1-1Zm-2.998-2.5c0 .55-.45 1-1 1s-1-.45-1-1 .45-1 1-1 1 .45 1 1Z" fill="currentColor"/> <path d="m7.991 20.335-.177.177.177-.177Zm-6.314-6.318.176-.177-.176.177Zm0-3.23.176.176-.176-.177Zm8.45-8.457-.176-.177.177.177ZM20.768 10v.25h.181l.057-.172-.238-.078Zm-1.587 0-.226-.106-.168.356h.394V10ZM13.31 2.378v-.25h-.25v.25h.25Zm0 .047v.25h.25v-.25h-.25Zm-2.184.903-.177-.177.177.177Zm-8.451 8.456.176.177-.176-.177Zm0 1.188-.177.176.177-.176Zm6.314 6.318-.177.177.177-.177Zm1.187 0-.177-.177-.007.007-.006.007.19.163Zm1.818-1.82h.25v-.604l-.426.428.176.176Zm0 2.09.177.177.073-.073v-.103h-.25Zm-.773.775.176.177-.176-.177Zm-3.406.177a2.51 2.51 0 0 0 1.791.738v-.5a2.01 2.01 0 0 1-1.437-.592l-.354.354ZM1.5 14.193l6.314 6.319.354-.354-6.315-6.318-.353.353Zm0-3.583c-1 1-1 2.583 0 3.583l.353-.353a2.016 2.016 0 0 1 0-2.877L1.5 10.61Zm8.45-8.457L1.5 10.61l.353.353 8.451-8.456-.353-.354ZM13.31.75c-.564 0-1.202.153-1.794.4-.592.246-1.156.595-1.564 1.003l.353.354c.352-.352.856-.668 1.403-.896.548-.229 1.12-.361 1.602-.361v-.5Zm5.412 0H13.31v.5h5.412v-.5Zm2.529 2.53c0-1.373-1.156-2.53-2.529-2.53v.5c1.096 0 2.029.933 2.029 2.03h.5Zm0 5.416V3.28h-.5v5.416h.5Zm-.245 1.382c.154-.466.245-.946.245-1.382h-.5c0 .37-.078.797-.22 1.226l.475.156Zm-1.825.172h1.587v-.5H19.18v.5Zm.098-1.602c0 .36-.126.823-.324 1.246l.452.213c.218-.464.372-1.002.372-1.459h-.5Zm0-5.415v5.415h.5V3.233h-.5Zm-.604-.605c.336 0 .604.268.604.605h.5a1.1 1.1 0 0 0-1.104-1.105v.5Zm-5.365 0h5.365v-.5H13.31v.5Zm.25-.203v-.047h-.5v.047h.5Zm-2.257 1.08c.205-.206.552-.416.939-.576.386-.159.78-.254 1.068-.254v-.5c-.377 0-.839.119-1.259.292-.42.173-.833.414-1.102.684l.354.354ZM2.85 11.96l8.452-8.456-.354-.354-8.451 8.456.353.354Zm0 .834a.582.582 0 0 1 0-.834l-.353-.354c-.43.43-.43 1.111 0 1.541l.353-.353Zm6.315 6.318L2.85 12.795l-.353.353 6.314 6.319.354-.354Zm.82.014c-.178.208-.577.23-.82-.014l-.354.354c.422.422 1.162.443 1.554-.015l-.38-.325Zm1.832-1.833-1.819 1.82.354.352 1.818-1.819-.353-.353Zm.426 2.267v-2.09h-.5v2.09h.5Zm-.847.95.774-.774-.353-.353-.774.774.353.354Zm-1.79.739a2.51 2.51 0 0 0 1.79-.738l-.353-.354a2.01 2.01 0 0 1-1.438.592v.5ZM20.988 20v-5c0-.55-.45-1-1-1h-5.996c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h5.996c.55 0 1-.45 1-1Zm-2.998-2.5c0 .55-.45 1-1 1s-1-.45-1-1 .45-1 1-1 1 .45 1 1Z" fill="currentColor"/>
<path d="M19.489 16v-2.5c0-1.4-1.1-2.5-2.499-2.5s-2.498 1.1-2.498 2.5V16" stroke="currentColor" stroke-opacity=".8" stroke-miterlimit="10"/> <path d="M19.489 16v-2.5c0-1.4-1.1-2.5-2.499-2.5s-2.498 1.1-2.498 2.5V16" stroke="currentColor" stroke-opacity=".8" stroke-miterlimit="10"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.916 9.556c-.111-.111-.111-.223-.222-.334L16.356 5.89a1.077 1.077 0 0 0-1.558 0 1.073 1.073 0 0 0 0 1.556l1.446 1.444h-5.118c-.667 0-1.112.444-1.112 1.111s.445 1.111 1.112 1.111h5.118l-1.446 1.445a1.073 1.073 0 0 0 0 1.555c.223.222.556.334.779.334.223 0 .556-.112.779-.334l3.338-3.333c.111-.111.222-.222.222-.333a1.225 1.225 0 0 0 0-.89Z" fill="currentColor"/> <path d="M19.916 9.556c-.111-.111-.111-.223-.222-.334L16.356 5.89a1.077 1.077 0 0 0-1.558 0 1.073 1.073 0 0 0 0 1.556l1.446 1.444h-5.118c-.667 0-1.112.444-1.112 1.111s.445 1.111 1.112 1.111h5.118l-1.446 1.445a1.073 1.073 0 0 0 0 1.555c.223.222.556.334.779.334.223 0 .556-.112.779-.334l3.338-3.333c.111-.111.222-.222.222-.333a1.225 1.225 0 0 0 0-.89Z" fill="currentColor"/>
<path d="M13.908 16.778c-1.224.666-2.559 1-3.894 1-4.34 0-7.789-3.445-7.789-7.778s3.45-7.778 7.789-7.778c1.335 0 2.67.334 3.894 1 .556.334 1.224.111 1.558-.444.334-.556.111-1.222-.445-1.556C13.463.444 11.794 0 10.014 0A9.965 9.965 0 0 0 0 10c0 5.556 4.45 10 10.014 10a9.94 9.94 0 0 0 5.007-1.333c.556-.334.667-1 .445-1.556-.334-.444-1.002-.667-1.558-.333Z" fill="currentColor"/> <path d="M13.908 16.778a8.135 8.135 0 0 1-3.894 1c-4.34 0-7.789-3.445-7.789-7.778s3.45-7.778 7.789-7.778c1.335 0 2.67.334 3.894 1 .556.334 1.224.111 1.558-.444.334-.556.111-1.222-.445-1.556C13.463.444 11.794 0 10.014 0A9.965 9.965 0 0 0 0 10c0 5.556 4.45 10 10.014 10a9.94 9.94 0 0 0 5.007-1.333c.556-.334.667-1 .445-1.556-.334-.444-1.002-.667-1.558-.333Z" fill="currentColor"/>
</svg> </svg>
...@@ -93,6 +93,17 @@ import type { ...@@ -93,6 +93,17 @@ import type {
OptimismL2BatchBlocks, OptimismL2BatchBlocks,
} from 'types/api/optimisticL2'; } from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type {
RewardsConfigResponse,
RewardsCheckRefCodeResponse,
RewardsNonceResponse,
RewardsCheckUserResponse,
RewardsLoginResponse,
RewardsUserBalancesResponse,
RewardsUserDailyCheckResponse,
RewardsUserDailyClaimResponse,
RewardsUserReferralsResponse,
} from 'types/api/rewards';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
...@@ -347,6 +358,60 @@ export const RESOURCES = { ...@@ -347,6 +358,60 @@ export const RESOURCES = {
basePath: marketplaceApi?.basePath, basePath: marketplaceApi?.basePath,
}, },
// REWARDS SERVICE
rewards_config: {
path: '/api/v1/config',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_check_ref_code: {
path: '/api/v1/auth/code/:code',
pathParams: [ 'code' as const ],
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_nonce: {
path: '/api/v1/auth/nonce',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_check_user: {
path: '/api/v1/auth/user/:address',
pathParams: [ 'address' as const ],
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_login: {
path: '/api/v1/auth/login',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_logout: {
path: '/api/v1/auth/logout',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_balances: {
path: '/api/v1/user/balances',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_daily_check: {
path: '/api/v1/user/daily/check',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_daily_claim: {
path: '/api/v1/user/daily/claim',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_referrals: {
path: '/api/v1/user/referrals',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
path: '/api/v2/blocks', path: '/api/v2/blocks',
...@@ -1229,6 +1294,15 @@ Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo : ...@@ -1229,6 +1294,15 @@ Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse : Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_config' ? RewardsConfigResponse :
Q extends 'rewards_check_ref_code' ? RewardsCheckRefCodeResponse :
Q extends 'rewards_nonce' ? RewardsNonceResponse :
Q extends 'rewards_check_user' ? RewardsCheckUserResponse :
Q extends 'rewards_login' ? RewardsLoginResponse :
Q extends 'rewards_user_balances' ? RewardsUserBalancesResponse :
Q extends 'rewards_user_daily_check' ? RewardsUserDailyCheckResponse :
Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse :
Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse :
Q extends 'token_transfers_all' ? TokenTransferResponse : Q extends 'token_transfers_all' ? TokenTransferResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
This diff is collapsed.
...@@ -5,6 +5,8 @@ import isBrowser from './isBrowser'; ...@@ -5,6 +5,8 @@ import isBrowser from './isBrowser';
export enum NAMES { export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
REWARDS_API_TOKEN='rewards_api_token',
REWARDS_REFERRAL_CODE='rewards_ref_code',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex', COLOR_MODE_HEX='chakra-ui-color-mode-hex',
......
interface JWTHeader {
alg: string;
typ?: string;
[key: string]: unknown;
}
interface JWTPayload {
[key: string]: unknown;
}
const base64UrlDecode = (str: string): string => {
// Replace characters according to Base64Url standard
str = str.replace(/-/g, '+').replace(/_/g, '/');
// Add padding '=' characters for correct decoding
const pad = str.length % 4;
if (pad) {
str += '='.repeat(4 - pad);
}
// Decode from Base64 to string
const decodedStr = atob(str);
return decodedStr;
};
export default function decodeJWT(token: string): { header: JWTHeader; payload: JWTPayload; signature: string } | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}
const [ encodedHeader, encodedPayload, signature ] = parts;
const headerJson = base64UrlDecode(encodedHeader);
const payloadJson = base64UrlDecode(encodedPayload);
const header = JSON.parse(headerJson) as JWTHeader;
const payload = JSON.parse(payloadJson) as JWTPayload;
return { header, payload, signature };
} catch (error) {
return null;
}
}
...@@ -28,6 +28,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -28,6 +28,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/graphiql': 'Regular page', '/graphiql': 'Regular page',
'/search-results': 'Regular page', '/search-results': 'Regular page',
'/auth/profile': 'Root page', '/auth/profile': 'Root page',
'/account/rewards': 'Regular page',
'/account/watchlist': 'Regular page', '/account/watchlist': 'Regular page',
'/account/api-key': 'Regular page', '/account/api-key': 'Regular page',
'/account/custom-abi': 'Regular page', '/account/custom-abi': 'Regular page',
......
...@@ -32,6 +32,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -32,6 +32,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/graphiql': DEFAULT_TEMPLATE, '/graphiql': DEFAULT_TEMPLATE,
'/search-results': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE,
'/auth/profile': DEFAULT_TEMPLATE, '/auth/profile': DEFAULT_TEMPLATE,
'/account/rewards': DEFAULT_TEMPLATE,
'/account/watchlist': DEFAULT_TEMPLATE, '/account/watchlist': DEFAULT_TEMPLATE,
'/account/api-key': DEFAULT_TEMPLATE, '/account/api-key': DEFAULT_TEMPLATE,
'/account/custom-abi': DEFAULT_TEMPLATE, '/account/custom-abi': DEFAULT_TEMPLATE,
......
...@@ -28,6 +28,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -28,6 +28,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/graphiql': 'GraphQL for %network_name% - %network_name% data query', '/graphiql': 'GraphQL for %network_name% - %network_name% data query',
'/search-results': '%network_name% search result for %q%', '/search-results': '%network_name% search result for %q%',
'/auth/profile': '%network_name% - my profile', '/auth/profile': '%network_name% - my profile',
'/account/rewards': '%network_name% - rewards',
'/account/watchlist': '%network_name% - watchlist', '/account/watchlist': '%network_name% - watchlist',
'/account/api-key': '%network_name% - API keys', '/account/api-key': '%network_name% - API keys',
'/account/custom-abi': '%network_name% - custom ABI', '/account/custom-abi': '%network_name% - custom ABI',
......
...@@ -26,6 +26,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -26,6 +26,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/graphiql': 'GraphQL', '/graphiql': 'GraphQL',
'/search-results': 'Search results', '/search-results': 'Search results',
'/auth/profile': 'Profile', '/auth/profile': 'Profile',
'/account/rewards': 'Merits',
'/account/watchlist': 'Watchlist', '/account/watchlist': 'Watchlist',
'/account/api-key': 'API keys', '/account/api-key': 'API keys',
'/account/custom-abi': 'Custom ABI', '/account/custom-abi': 'Custom ABI',
......
...@@ -97,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? { ...@@ -97,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
} : } :
Type extends EventTypes.WALLET_CONNECT ? { Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button'; 'Source': 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button' | 'Merits';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.WALLET_ACTION ? ( Type extends EventTypes.WALLET_ACTION ? (
......
import type { RewardsUserBalancesResponse } from 'types/api/rewards';
export const base: RewardsUserBalancesResponse = {
total: '250',
staked: '0',
unstaked: '0',
total_staking_rewards: '0',
total_referral_rewards: '0',
pending_referral_rewards: '0',
};
import type { RewardsUserDailyCheckResponse } from 'types/api/rewards';
export const base: RewardsUserDailyCheckResponse = {
available: true,
daily_reward: '10',
pending_referral_rewards: '0',
date: '',
reset_at: '',
};
import type { RewardsUserReferralsResponse } from 'types/api/rewards';
export const base: RewardsUserReferralsResponse = {
code: 'QWERTY',
link: 'https://example.com?ref=QWERTY',
referrals: '15',
};
import type { RewardsConfigResponse } from 'types/api/rewards';
export const base: RewardsConfigResponse = {
rewards: {
registration: '100',
registration_with_referral: '200',
daily_claim: '10',
referral_share: '0.1',
},
};
...@@ -15,3 +15,11 @@ export const withoutEmail: UserInfo = { ...@@ -15,3 +15,11 @@ export const withoutEmail: UserInfo = {
nickname: 'tom2drum', nickname: 'tom2drum',
address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
}; };
export const withEmailAndWallet: UserInfo = {
avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: 'tom@ohhhh.me',
name: 'tom goriunov',
nickname: 'tom2drum',
address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
};
...@@ -67,6 +67,7 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -67,6 +67,7 @@ export function app(): CspDev.DirectiveDescriptor {
getFeaturePayload(config.features.addressVerification)?.api.endpoint, getFeaturePayload(config.features.addressVerification)?.api.endpoint,
getFeaturePayload(config.features.nameService)?.api.endpoint, getFeaturePayload(config.features.nameService)?.api.endpoint,
getFeaturePayload(config.features.addressMetadata)?.api.endpoint, getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
getFeaturePayload(config.features.rewards)?.api.endpoint,
// chain RPC server // chain RPC server
config.chain.rpcUrl, config.chain.rpcUrl,
......
...@@ -9,6 +9,7 @@ declare module "nextjs-routes" { ...@@ -9,6 +9,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/404"> | StaticRoute<"/404">
| StaticRoute<"/account/api-key"> | StaticRoute<"/account/api-key">
| StaticRoute<"/account/custom-abi"> | StaticRoute<"/account/custom-abi">
| StaticRoute<"/account/rewards">
| StaticRoute<"/account/tag-address"> | StaticRoute<"/account/tag-address">
| StaticRoute<"/account/verified-addresses"> | StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist"> | StaticRoute<"/account/watchlist">
......
...@@ -23,7 +23,8 @@ export default function fetchFactory( ...@@ -23,7 +23,8 @@ export default function fetchFactory(
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '', cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
..._pick(_req.headers, [ ..._pick(_req.headers, [
'x-csrf-token', 'x-csrf-token',
'Authorization', 'Authorization', // the old value, just in case
'authorization', // Node.js automatically lowercases headers
// feature flags // feature flags
'updated-gas-oracle', 'updated-gas-oracle',
]) as Record<string, string | undefined>, ]) as Record<string, string | undefined>,
......
...@@ -13,11 +13,13 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig'; ...@@ -13,11 +13,13 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { ChakraProvider } from 'lib/contexts/chakra'; import { ChakraProvider } from 'lib/contexts/chakra';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { growthBook } from 'lib/growthbook/init'; import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer'; import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
...@@ -69,9 +71,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -69,9 +71,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<GrowthBookProvider growthbook={ growthBook }> <GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider> <ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<MarketplaceContextProvider> <RewardsContextProvider>
{ getLayout(<Component { ...pageProps }/>) } <MarketplaceContextProvider>
</MarketplaceContextProvider> { getLayout(<Component { ...pageProps }/>) }
{ config.features.rewards.isEnabled && <RewardsLoginModal/> }
</MarketplaceContextProvider>
</RewardsContextProvider>
</SocketProvider> </SocketProvider>
</ScrollDirectionProvider> </ScrollDirectionProvider>
</GrowthBookProvider> </GrowthBookProvider>
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const RewardsDashboard = dynamic(() => import('ui/pages/RewardsDashboard'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/account/rewards">
<RewardsDashboard/>
</PageNextJs>
);
};
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -11,6 +11,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps'; ...@@ -11,6 +11,7 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app'; import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContext } from 'lib/contexts/marketplace'; import { MarketplaceContext } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain'; import currentChain from 'lib/web3/currentChain';
import theme from 'theme/theme'; import theme from 'theme/theme';
...@@ -77,7 +78,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp ...@@ -77,7 +78,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp
<MarketplaceContext.Provider value={ marketplaceContext }> <MarketplaceContext.Provider value={ marketplaceContext }>
<GrowthBookProvider> <GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }> <WagmiProvider config={ wagmiConfig }>
{ children } <RewardsContextProvider>
{ children }
</RewardsContextProvider>
</WagmiProvider> </WagmiProvider>
</GrowthBookProvider> </GrowthBookProvider>
</MarketplaceContext.Provider> </MarketplaceContext.Provider>
......
...@@ -81,4 +81,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -81,4 +81,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
nameService: [ nameService: [
[ 'NEXT_PUBLIC_NAME_SERVICE_API_HOST', 'https://localhost:3101' ], [ 'NEXT_PUBLIC_NAME_SERVICE_API_HOST', 'https://localhost:3101' ],
], ],
rewardsService: [
[ 'NEXT_PUBLIC_REWARDS_SERVICE_API_HOST', 'http://localhost:3003' ],
],
}; };
import type { BrowserContext, TestFixture } from '@playwright/test';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
// This JWT token contains 0xd789a607CEac2f0E14867de4EB15b15C9FFB5859 address
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIweGQ3ODlhNjA3Q0VhYzJmMEUxNDg2N2RlNEVCMTViMTVDOUZGQjU4NTkiLCJpYXQiOjE3MzA0NzAyNTIsImV4cCI6MTczMDQ3MDU1Mn0.uhWH59mJQhpWcK8RHaLQ-X_nieXZsYE-VdcPrjYNvp4'; // eslint-disable-line max-len
export function authenticateUser(context: BrowserContext) {
context.addCookies([ { name: cookies.NAMES.REWARDS_API_TOKEN, value: token, domain: config.app.host, path: '/' } ]);
}
export const contextWithRewards: TestFixture<BrowserContext, { context: BrowserContext }> = async({ context }, use) => {
authenticateUser(context);
use(context);
};
...@@ -86,6 +86,11 @@ ...@@ -86,6 +86,11 @@
| "link_external" | "link_external"
| "link" | "link"
| "lock" | "lock"
| "merits_colored"
| "merits_slim"
| "merits_with_dot_slim"
| "merits_with_dot"
| "merits"
| "minus" | "minus"
| "monaco/file" | "monaco/file"
| "monaco/folder-open" | "monaco/folder-open"
......
<svg viewBox="0 0 89 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.507 3.196a16.5 16.5 0 0 0-16.514 0l-27.25 15.75A16.5 16.5 0 0 0 .5 33.233v31.536a16.5 16.5 0 0 0 8.243 14.285l27.25 15.752a16.5 16.5 0 0 0 16.514 0l27.25-15.752A16.5 16.5 0 0 0 88 64.768V33.232a16.5 16.5 0 0 0-8.243-14.285L52.507 3.195Z" fill="#ECF5FF" stroke="#A7BFDA"/>
<path d="M44.25 1.48V49L3.14 25.237a15.99 15.99 0 0 1 5.853-5.857l27.25-15.752a15.99 15.99 0 0 1 8.007-2.147Z" fill="#F8FBFF"/>
<path d="M1 33.232v31.536c0 2.856.761 5.603 2.14 7.995L44.25 49 3.14 25.237A15.99 15.99 0 0 0 1 33.232Z" fill="url(#a)"/>
<path d="M87.5 64.78V33.246a15.99 15.99 0 0 0-2.14-7.995L44.25 49.013l41.11 23.762a15.99 15.99 0 0 0 2.14-7.995Z" fill="#E0EEFF"/>
<path d="M44.25 1.5v47.52l41.11-23.763a15.99 15.99 0 0 0-5.853-5.858L52.257 3.648A15.99 15.99 0 0 0 44.25 1.5Z" fill="url(#b)"/>
<path d="M8.99 78.618a15.99 15.99 0 0 1-5.85-5.855L44.25 49v47.52a15.99 15.99 0 0 1-8.005-2.147l-.002-.001L8.993 78.62a.056.056 0 0 0-.003-.002Z" fill="url(#c)"/>
<path d="m52.257 94.372 27.25-15.752a15.99 15.99 0 0 0 5.852-5.857L44.25 49v47.52c2.765 0 5.53-.717 8.007-2.148Z" fill="#D1E5FE"/>
<path d="M37.243 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L51.256 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.19 62.922V35.078a14 14 0 0 1 6.994-12.12L37.243 9.05Z" fill="#A7BFDA"/>
<path d="M51.506 8.617a14.5 14.5 0 0 0-14.513 0l-24.06 13.907a14.5 14.5 0 0 0-7.244 12.554v27.844a14.5 14.5 0 0 0 7.244 12.553l24.06 13.908a14.5 14.5 0 0 0 14.513 0l24.06-13.907a14.5 14.5 0 0 0 7.243-12.554V35.078a14.5 14.5 0 0 0-7.243-12.554L51.506 8.617Z" stroke="#F8FBFF"/>
<path d="M37.493 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L13.433 74.61A13.5 13.5 0 0 1 6.69 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" stroke="#A7BFDA"/>
<path d="M37.243 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L51.256 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.19 62.922V35.078a14 14 0 0 1 6.994-12.12L37.243 9.05Z" fill="url(#d)"/>
<path d="M37.493 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L13.433 74.61A13.5 13.5 0 0 1 6.69 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" fill="url(#e)" stroke="#6F89A8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.98 18.284a7.875 7.875 0 0 1 7.071-4.409H49.2a7.875 7.875 0 0 1 7.071 4.409l1.422 2.902c.374-.297.72-.54.981-.712.8-.796 1.516-1.142 2.137-1.012.618.13.988.7 1.209 1.304.552 1.507.572 3.595.107 6.144.712.116 1.396.24 2.048.37 2.568.511 4.671 1.124 6.14 1.816.735.346 1.33.72 1.749 1.127.418.407.687.877.687 1.402 0 .592-.34 1.113-.856 1.557-.518.446-1.258.856-2.171 1.232-1.736.716-4.175 1.342-7.12 1.847a53.99 53.99 0 0 1 .467 3.86l.095.13h.001c.145.198.384.497.7.892 2.829 3.537 11.829 14.793 14.797 29.645a12.99 12.99 0 0 1-3.845 3.39l-24.06 13.906a12.99 12.99 0 0 1-13.01 0l-24.06-13.907a13.025 13.025 0 0 1-3.98-3.57C12.848 54.83 23.12 42.72 25.567 40.11c.004-1.325.067-2.568.182-3.734-2.97-.51-5.54-1.259-7.277-1.984-.896-.373-1.623-.78-2.131-1.223-.507-.44-.841-.957-.841-1.543 0-.525.269-.995.687-1.402.418-.407 1.014-.781 1.748-1.127 1.47-.692 3.573-1.305 6.141-1.816.653-.13 1.338-.254 2.052-.371-.465-2.549-.445-4.636.107-6.143.221-.604.59-1.175 1.209-1.304.62-.13 1.336.216 2.137 1.012.261.172.605.413.978.71l1.421-2.9Z" fill="url(#f)"/>
<path d="M10.084 72.608c-.214-.223-.42-.454-.619-.69v.002c.198.237.404.467.618.69l.001-.002Z" fill="url(#g)"/>
<defs>
<linearGradient id="a" x1="22.625" y1="25.237" x2="22.625" y2="72.763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="b" x1="44.25" y1="3.875" x2="85.25" y2="3.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="c" x1="4.125" y1="72.75" x2="44.25" y2="72.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1E5FE"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="d" x1="6.249" y1="46.375" x2="82.249" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5E8FF"/>
<stop offset="1" stop-color="#89BEFF"/>
</linearGradient>
<linearGradient id="e" x1="6.249" y1="46.375" x2="82.249" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D0DCEC"/>
<stop offset=".5" stop-color="#A8BCD5"/>
</linearGradient>
<linearGradient id="f" x1="44.064" y1="13.875" x2="44.064" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="g" x1="44.064" y1="13.875" x2="44.064" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
</defs>
</svg>
<svg viewBox="0 0 90 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.632 3.196a16.5 16.5 0 0 0-16.514 0l-27.25 15.75a16.5 16.5 0 0 0-8.243 14.286v31.536a16.5 16.5 0 0 0 8.243 14.285l27.25 15.752a16.5 16.5 0 0 0 16.514 0l27.25-15.752a16.5 16.5 0 0 0 8.243-14.285V33.232a16.5 16.5 0 0 0-8.243-14.285L53.632 3.195Z" fill="#ECF5FF" stroke="#A7BFDA"/>
<path d="M45.375 1.48V49L4.265 25.237a15.99 15.99 0 0 1 5.849-5.855l.004-.002 27.25-15.752a15.99 15.99 0 0 1 8.007-2.147Z" fill="#F8FBFF"/>
<path d="M2.125 33.232v31.536c0 2.856.761 5.603 2.14 7.995L45.376 49 4.265 25.237a15.99 15.99 0 0 0-2.14 7.995Z" fill="url(#a)"/>
<path d="M88.625 64.78V33.246a15.99 15.99 0 0 0-2.14-7.995l-41.11 23.763 41.11 23.762a15.99 15.99 0 0 0 2.14-7.995Z" fill="#E0EEFF"/>
<path d="M45.375 1.5v47.52l41.11-23.763a15.99 15.99 0 0 0-5.853-5.858L53.382 3.648A15.99 15.99 0 0 0 45.375 1.5Z" fill="url(#b)"/>
<path d="M10.115 78.618a15.99 15.99 0 0 1-5.85-5.855L45.376 49v47.52a15.99 15.99 0 0 1-8.005-2.147l-.002-.001-27.25-15.752a.056.056 0 0 0-.003-.002Z" fill="url(#c)"/>
<path d="m53.382 94.372 27.25-15.752a15.99 15.99 0 0 0 5.852-5.857L45.375 49v47.52c2.765 0 5.53-.717 8.007-2.148Z" fill="#D1E5FE"/>
<path d="M38.368 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.38 88.95a14 14 0 0 1-14.013 0l-24.06-13.907a14 14 0 0 1-6.994-12.121V35.078a14 14 0 0 1 6.994-12.12L38.368 9.05Z" fill="#A7BFDA"/>
<path d="M52.63 8.617a14.5 14.5 0 0 0-14.512 0l-24.06 13.907a14.5 14.5 0 0 0-7.244 12.554v27.844a14.5 14.5 0 0 0 7.244 12.553l24.06 13.908a14.5 14.5 0 0 0 14.513 0l24.06-13.907a14.5 14.5 0 0 0 7.243-12.554V35.078a14.5 14.5 0 0 0-7.243-12.554L52.63 8.617Z" stroke="#F8FBFF"/>
<path d="M38.618 9.483a13.5 13.5 0 0 1 13.512 0L76.19 23.39a13.5 13.5 0 0 1 6.745 11.688v27.844A13.5 13.5 0 0 1 76.19 74.61L52.13 88.517a13.5 13.5 0 0 1-13.512 0L14.558 74.61a13.5 13.5 0 0 1-6.744-11.688V35.078A13.5 13.5 0 0 1 14.56 23.39L38.62 9.483Z" stroke="#A7BFDA"/>
<path d="M38.429 9.05a14 14 0 0 1 14.012 0l24.06 13.907a14 14 0 0 1 6.994 12.121v27.844a14 14 0 0 1-6.994 12.12L52.441 88.95a14 14 0 0 1-14.012 0l-24.06-13.907a14 14 0 0 1-6.994-12.121V35.078a14 14 0 0 1 6.994-12.12L38.429 9.05Z" fill="url(#d)"/>
<path d="M38.679 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.744 11.688v27.844a13.5 13.5 0 0 1-6.744 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.619 74.61a13.5 13.5 0 0 1-6.744-11.688V35.078a13.5 13.5 0 0 1 6.744-11.688l24.06-13.907Z" fill="url(#e)" stroke="#6F89A8"/>
<path d="M57.605 24.766c.365-.719.768-1.251 1.241-1.667.617-.541 1.327-.861 2.153-1.155 2.047-.728 3.511-.7 4.455-.496.6.13 1.043.4 1.333.803.284.395.39.874.41 1.368.02.493-.046 1.034-.138 1.578-.052.308-.117.637-.18.964l-.001.001a55.57 55.57 0 0 0-.134.702c-.204 1.121-.838 1.867-1.625 2.375-.774.5-1.702.772-2.523.97-.207.05-.42.097-.623.142.78 1.504 1.457 3.15 2.107 4.888.445 1.191.874 2.418 1.314 3.676.637 1.819 1.296 3.703 2.06 5.641 5.526 6.93 7.787 14.416 8.667 20.168a44.44 44.44 0 0 1 .52 7.064 32.822 32.822 0 0 1-.076 2.004c-.203.134-.411.262-.624.385l-24.06 13.907a13 13 0 0 1-13.011 0l-16.878-9.756c-.049-13.024 4.609-22.676 7.301-25.99-.924-1.6-1.71-3.503-2.27-5.567l-.149.076c-.844.429-1.758.862-2.591 1.188-.819.321-1.61.56-2.189.562a.375.375 0 0 1-.002-.75c.425 0 1.1-.19 1.917-.51a29.824 29.824 0 0 0 2.526-1.159c.1-.05.199-.102.297-.152a26.75 26.75 0 0 1-.427-2.173c-1.877.692-4.21 1.401-5.984 1.43a.375.375 0 1 1-.012-.75c1.66-.027 3.928-.713 5.817-1.414l.074-.027c-.334-2.7-.25-5.52.414-8.192.734-2.952 2.178-5.732 4.558-7.972-1.089-3.514-.125-4.939.603-5.303.729-.364 3.345-.61 5.442 1.668 1.627-.593 3.46-1.032 5.511-1.29 4.63-.581 8.18-.297 10.982.689a13.008 13.008 0 0 1 3.795 2.074Z" fill="url(#f)"/>
<defs>
<linearGradient id="a" x1="23.75" y1="25.237" x2="23.75" y2="72.763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="b" x1="45.375" y1="3.875" x2="86.375" y2="3.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="c" x1="5.25" y1="72.75" x2="45.375" y2="72.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1E5FE"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="d" x1="7.435" y1="46.375" x2="83.435" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5E8FF"/>
<stop offset="1" stop-color="#89BEFF"/>
</linearGradient>
<linearGradient id="e" x1="7.435" y1="46.375" x2="83.435" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D0DCEC"/>
<stop offset=".5" stop-color="#A8BCD5"/>
</linearGradient>
<linearGradient id="f" x1="48.341" y1="21.323" x2="48.341" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
</defs>
</svg>
<svg viewBox="0 0 90 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.257 3.196a16.5 16.5 0 0 0-16.514 0l-27.25 15.75A16.5 16.5 0 0 0 1.25 33.233v31.536a16.5 16.5 0 0 0 8.243 14.285l27.25 15.752a16.5 16.5 0 0 0 16.514 0l27.25-15.752a16.5 16.5 0 0 0 8.243-14.285V33.232a16.5 16.5 0 0 0-8.243-14.285L53.257 3.195Z" fill="#ECF5FF" stroke="#A7BFDA"/>
<path d="M45 1.48V49L3.89 25.237a15.99 15.99 0 0 1 5.853-5.857l27.25-15.752A15.99 15.99 0 0 1 45 1.481Z" fill="#F8FBFF"/>
<path d="M1.75 33.232v31.536c0 2.856.761 5.603 2.14 7.995L45 49 3.89 25.237a15.99 15.99 0 0 0-2.14 7.995Z" fill="url(#a)"/>
<path d="M88.25 64.78V33.246a15.99 15.99 0 0 0-2.14-7.995L45 49.013l41.11 23.762a15.99 15.99 0 0 0 2.14-7.995Z" fill="#E0EEFF"/>
<path d="M45 1.5v47.52l41.11-23.763a15.99 15.99 0 0 0-5.853-5.858L53.007 3.648A15.99 15.99 0 0 0 45 1.5Z" fill="url(#b)"/>
<path d="M9.74 78.618a15.99 15.99 0 0 1-5.85-5.855L45 49v47.52a15.99 15.99 0 0 1-8.005-2.147l-.002-.001L9.743 78.62a.056.056 0 0 0-.003-.002Z" fill="url(#c)"/>
<path d="m53.007 94.372 27.25-15.752a15.99 15.99 0 0 0 5.852-5.857L45 49v47.52c2.765 0 5.53-.717 8.007-2.148Z" fill="#D1E5FE"/>
<path d="M37.993 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.006 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.94 62.922V35.078a14 14 0 0 1 6.994-12.12L37.993 9.05Z" fill="#A7BFDA"/>
<path d="M52.256 8.617a14.5 14.5 0 0 0-14.513 0l-24.06 13.907a14.5 14.5 0 0 0-7.244 12.554v27.844a14.5 14.5 0 0 0 7.244 12.553l24.06 13.908a14.5 14.5 0 0 0 14.513 0l24.06-13.907a14.5 14.5 0 0 0 7.243-12.554V35.078a14.5 14.5 0 0 0-7.243-12.554L52.256 8.617Z" stroke="#F8FBFF"/>
<path d="M38.243 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.183 74.61A13.5 13.5 0 0 1 7.44 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" stroke="#A7BFDA"/>
<path d="M37.993 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.006 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.94 62.922V35.078a14 14 0 0 1 6.994-12.12L37.993 9.05Z" fill="url(#d)"/>
<path d="M38.243 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.183 74.61A13.5 13.5 0 0 1 7.44 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" fill="url(#e)" stroke="#6F89A8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.32 12.25a.75.75 0 0 0-.75.75v.125h-1.473a9.375 9.375 0 0 0-9.095 7.101l-.506 2.027-.03-.031a13.31 13.31 0 0 0-2.135-1.748c-.8-.796-1.516-1.142-2.137-1.012-.618.13-.988.7-1.21 1.304-.752 2.057-.516 5.196.596 9.113.036.128.124.23.23.28-.927 2.658-1.48 5.928-1.492 9.95-1.26 1.344-4.592 5.204-7.958 10.786a.385.385 0 0 0-.02.028l.003.001-.003-.001-.004.006-.01.018-.012.018-.031.049-.161.258a74.43 74.43 0 0 0-2.431 4.27c-1.399 2.662-2.972 6.074-3.612 9.003-.691 3.163-.658 5.721-.112 7.782.347.331.713.645 1.099.938l-.001-.002c.434.33.893.636 1.373.914l9.022 5.215h-.002l1.374.794h.002l.453.262.05-.01 13.229 7.646a12.99 12.99 0 0 0 6.716 1.744 12.99 12.99 0 0 0 6.295-1.744l24.06-13.907.238-.137c.523-.302.696-.401.826-.54.039-.042.074-.087.113-.143a13.473 13.473 0 0 0 .696-.554c.68-2.14.78-4.848.034-8.258a25.524 25.524 0 0 0-.926-3.134l.048.028c-.343-.885-.7-1.749-1.07-2.59a62.62 62.62 0 0 0-1.758-3.698 76.564 76.564 0 0 0-1.76-3.235 5.73 5.73 0 0 0 .556-.156c2.214-.744 4.7-2.172 5.483-5.086.341-1.272.225-2.422-.287-3.339-.512-.918-1.398-1.56-2.516-1.86-1.466-.393-2.733.18-3.462.72-.361-.833-1.173-1.962-2.64-2.355-1.118-.3-2.206-.186-3.109.353a3.87 3.87 0 0 0-1.26 1.232l-.225-.282c-.317-.396-.556-.694-.7-.891l-.001-.001-.096-.13c-.345-3.99-1.015-7.261-1.946-9.936a.436.436 0 0 0 .3-.306c1.11-3.917 1.347-7.056.594-9.113-.221-.604-.59-1.175-1.209-1.304-.62-.13-1.336.216-2.137 1.012-.46.302-1.174.82-1.855 1.47l-.43-1.718a9.375 9.375 0 0 0-9.095-7.101H46.07V13a.75.75 0 0 0-.75-.75h-1ZM12.942 64.427l-.017-.15.045-.133-.028.283Z" fill="url(#f)"/>
<path d="m23.846 80.77-9.909-5.727a14 14 0 0 1-1.805-1.241v.001c.176.143.356.281.54.415v.003c.403.294.824.569 1.261.822l.176.101-.002-.002 6.751 3.903-.003-.001 1.785 1.032h.003l1.202.695Z" fill="url(#g)"/>
<path d="M11.01 33.585c.512-.918 1.398-1.56 2.516-1.86 1.466-.393 2.735.18 3.464.72.36-.833 1.171-1.962 2.637-2.355 1.119-.3 2.207-.186 3.11.353.902.538 1.577 1.476 1.918 2.748.78 2.915-.66 5.395-2.205 7.146a5.884 5.884 0 0 1-6.243 1.673c-2.214-.743-4.701-2.17-5.484-5.086-.341-1.272-.225-2.422.287-3.339Z" fill="url(#h)"/>
<defs>
<linearGradient id="a" x1="23.375" y1="25.237" x2="23.375" y2="72.763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="b" x1="45" y1="3.875" x2="86" y2="3.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="c" x1="4.875" y1="72.75" x2="45" y2="72.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1E5FE"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="d" x1="6.999" y1="46.375" x2="82.999" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5E8FF"/>
<stop offset="1" stop-color="#89BEFF"/>
</linearGradient>
<linearGradient id="e" x1="6.999" y1="46.375" x2="82.999" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D0DCEC"/>
<stop offset=".5" stop-color="#A8BCD5"/>
</linearGradient>
<linearGradient id="f" x1="44.42" y1="12.25" x2="44.42" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="g" x1="44.42" y1="12.25" x2="44.42" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="h" x1="44.42" y1="12.25" x2="44.42" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
</defs>
</svg>
<svg viewBox="0 0 90 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.507 3.196a16.5 16.5 0 0 0-16.514 0l-27.25 15.75A16.5 16.5 0 0 0 1.5 33.233v31.536a16.5 16.5 0 0 0 8.243 14.285l27.25 15.752a16.5 16.5 0 0 0 16.514 0l27.25-15.752A16.5 16.5 0 0 0 89 64.768V33.232a16.5 16.5 0 0 0-8.243-14.285L53.507 3.195Z" fill="#ECF5FF" stroke="#A7BFDA"/>
<path d="M45.25 1.48V49L4.14 25.237a15.99 15.99 0 0 1 5.853-5.857l27.25-15.752a15.99 15.99 0 0 1 8.007-2.147Z" fill="#F8FBFF"/>
<path d="M2 33.232v31.536c0 2.856.761 5.603 2.14 7.995L45.25 49 4.14 25.237A15.99 15.99 0 0 0 2 33.232Z" fill="url(#a)"/>
<path d="M88.5 64.78V33.246a15.99 15.99 0 0 0-2.14-7.995L45.25 49.013l41.11 23.762a15.99 15.99 0 0 0 2.14-7.995Z" fill="#E0EEFF"/>
<path d="M45.25 1.5v47.52l41.11-23.763a15.99 15.99 0 0 0-5.853-5.858L53.257 3.648A15.99 15.99 0 0 0 45.25 1.5Z" fill="url(#b)"/>
<path d="M9.99 78.618a15.99 15.99 0 0 1-5.85-5.855L45.25 49v47.52a15.99 15.99 0 0 1-8.005-2.147l-.002-.001L9.993 78.62a.056.056 0 0 0-.003-.002Z" fill="url(#c)"/>
<path d="m53.257 94.372 27.25-15.752a15.99 15.99 0 0 0 5.852-5.857L45.25 49v47.52c2.765 0 5.53-.717 8.007-2.148Z" fill="#D1E5FE"/>
<path d="M38.243 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.256 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 7.19 62.922V35.078a14 14 0 0 1 6.994-12.12L38.243 9.05Z" fill="#A7BFDA"/>
<path d="M52.506 8.617a14.5 14.5 0 0 0-14.513 0l-24.06 13.907a14.5 14.5 0 0 0-7.244 12.554v27.844a14.5 14.5 0 0 0 7.244 12.553l24.06 13.908a14.5 14.5 0 0 0 14.513 0l24.06-13.907a14.5 14.5 0 0 0 7.243-12.554V35.078a14.5 14.5 0 0 0-7.243-12.554L52.506 8.617Z" stroke="#F8FBFF"/>
<path d="M38.493 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.433 74.61A13.5 13.5 0 0 1 7.69 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" stroke="#A7BFDA"/>
<path d="M38.243 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.256 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 7.19 62.922V35.078a14 14 0 0 1 6.994-12.12L38.243 9.05Z" fill="url(#d)"/>
<path d="M38.493 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.433 74.61A13.5 13.5 0 0 1 7.69 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" fill="url(#e)" stroke="#6F89A8"/>
<path d="M47.337 12.236c-.87-1.767-3.387-1.77-4.26-.005l-1.888 3.816c-.237.48-.695.811-1.225.887l-4.214.608c-1.948.281-2.728 2.674-1.32 4.05l3.045 2.975c.383.374.557.912.466 1.439l-.724 4.195c-.335 1.94 1.7 3.422 3.443 2.508l3.77-1.977a1.625 1.625 0 0 1 1.513.001l3.767 1.986c1.741.917 3.78-.56 3.448-2.5l-.715-4.198a1.625 1.625 0 0 1 .47-1.437l3.051-2.969c1.411-1.372.636-3.767-1.312-4.052l-4.212-.617a1.625 1.625 0 0 1-1.223-.89l-1.88-3.82Z" fill="url(#f)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M73.851 73.708c.17.439.337.878.498 1.316l-.659.381-21.935 12.68a13 13 0 0 1-13.012 0l-22.59-13.059c.162-.439.328-.879.499-1.318 2.17-5.58 5.094-11.122 8.612-14.384.323-4.025.816-7.599 1.718-10.653a.443.443 0 0 1-.26-.292c-1.189-4.194-1.44-7.548-.637-9.742.236-.645.628-1.247 1.279-1.383.653-.136 1.412.227 2.267 1.079a14.224 14.224 0 0 1 2.413 2.004c2.966-2.444 7.187-3.712 13.208-3.712 6 0 10.213 1.26 13.177 3.687l.105-.11a14.228 14.228 0 0 1 2.284-1.87c.856-.85 1.614-1.214 2.268-1.078.65.136 1.042.738 1.278 1.383.803 2.194.552 5.548-.638 9.742a.45.45 0 0 1-.211.27c.906 3.059 1.4 6.64 1.724 10.675 3.518 3.262 6.442 8.804 8.612 14.384Z" fill="url(#g)"/>
<path d="M21.439 23.179c.244-1.22 1.78-1.629 2.599-.694l1.6 1.829a1.5 1.5 0 0 0 1.303.501l2.414-.282c1.235-.145 2.099 1.19 1.462 2.258l-1.244 2.087a1.5 1.5 0 0 0-.074 1.394l1.014 2.208c.52 1.13-.483 2.364-1.695 2.089l-2.37-.538a1.5 1.5 0 0 0-1.348.36l-1.787 1.647c-.914.842-2.398.27-2.51-.967l-.221-2.42a1.5 1.5 0 0 0-.759-1.172l-2.118-1.19c-1.084-.609-1-2.196.143-2.686l2.233-.958a1.5 1.5 0 0 0 .88-1.084l.478-2.382Z" fill="url(#h)"/>
<path d="M66.36 22.238c1.023-1.169 2.944-.657 3.249.867l.477 2.383c.073.363.32.667.66.813l2.233.957c1.428.613 1.535 2.597.18 3.358l-2.119 1.19c-.322.182-.535.51-.569.879l-.22 2.42c-.141 1.548-1.996 2.262-3.139 1.208l-1.786-1.647a1.125 1.125 0 0 0-1.012-.27l-2.37.539c-1.515.344-2.767-1.2-2.118-2.611l1.014-2.208a1.125 1.125 0 0 0-.056-1.046l-1.244-2.087c-.795-1.335.285-3.003 1.828-2.823l2.414.283c.368.043.733-.098.977-.377l1.6-1.828Z" fill="url(#i)"/>
<defs>
<linearGradient id="a" x1="23.625" y1="25.237" x2="23.625" y2="72.763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="b" x1="45.25" y1="3.875" x2="86.25" y2="3.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="c" x1="5.125" y1="72.75" x2="45.25" y2="72.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1E5FE"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="d" x1="7.249" y1="46.375" x2="83.249" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5E8FF"/>
<stop offset="1" stop-color="#89BEFF"/>
</linearGradient>
<linearGradient id="e" x1="7.249" y1="46.375" x2="83.249" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D0DCEC"/>
<stop offset=".5" stop-color="#A8BCD5"/>
</linearGradient>
<linearGradient id="f" x1="45.251" y1="10.909" x2="45.251" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="g" x1="45.251" y1="10.909" x2="45.251" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="h" x1="45.251" y1="10.909" x2="45.251" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="i" x1="45.251" y1="10.909" x2="45.251" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
</defs>
</svg>
<svg viewBox="0 0 90 98" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.257 3.196a16.5 16.5 0 0 0-16.514 0l-27.25 15.75A16.5 16.5 0 0 0 1.25 33.233v31.536a16.5 16.5 0 0 0 8.243 14.285l27.25 15.752a16.5 16.5 0 0 0 16.514 0l27.25-15.752a16.5 16.5 0 0 0 8.243-14.285V33.232a16.5 16.5 0 0 0-8.243-14.285L53.257 3.195Z" fill="#ECF5FF" stroke="#A7BFDA"/>
<path d="M45 1.48V49L3.89 25.237a15.99 15.99 0 0 1 5.853-5.857l27.25-15.752A15.99 15.99 0 0 1 45 1.481Z" fill="#F8FBFF"/>
<path d="M1.75 33.232v31.536c0 2.856.761 5.603 2.14 7.995L45 49 3.89 25.237a15.99 15.99 0 0 0-2.14 7.995Z" fill="url(#a)"/>
<path d="M88.25 64.78V33.246a15.99 15.99 0 0 0-2.14-7.995L45 49.013l41.11 23.762a15.99 15.99 0 0 0 2.14-7.995Z" fill="#E0EEFF"/>
<path d="M45 1.5v47.52l41.11-23.763a15.99 15.99 0 0 0-5.853-5.858L53.007 3.648A15.99 15.99 0 0 0 45 1.5Z" fill="url(#b)"/>
<path d="M9.74 78.618a15.99 15.99 0 0 1-5.85-5.855L45 49v47.52a15.99 15.99 0 0 1-8.005-2.147l-.002-.001L9.743 78.62a.056.056 0 0 0-.003-.002Z" fill="url(#c)"/>
<path d="m53.007 94.372 27.25-15.752a15.99 15.99 0 0 0 5.852-5.857L45 49v47.52c2.765 0 5.53-.717 8.007-2.148Z" fill="#D1E5FE"/>
<path d="M37.993 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.006 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.94 62.922V35.078a14 14 0 0 1 6.994-12.12L37.993 9.05Z" fill="#A7BFDA"/>
<path d="M52.256 8.617a14.5 14.5 0 0 0-14.513 0l-24.06 13.907a14.5 14.5 0 0 0-7.244 12.554v27.844a14.5 14.5 0 0 0 7.244 12.553l24.06 13.908a14.5 14.5 0 0 0 14.513 0l24.06-13.907a14.5 14.5 0 0 0 7.243-12.554V35.078a14.5 14.5 0 0 0-7.243-12.554L52.256 8.617Z" stroke="#F8FBFF"/>
<path d="M38.243 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.183 74.61A13.5 13.5 0 0 1 7.44 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" stroke="#A7BFDA"/>
<path d="M37.993 9.05a14 14 0 0 1 14.013 0l24.06 13.907a14 14 0 0 1 6.993 12.121v27.844a14 14 0 0 1-6.993 12.12L52.006 88.95a14 14 0 0 1-14.013 0l-24.06-13.907A14 14 0 0 1 6.94 62.922V35.078a14 14 0 0 1 6.994-12.12L37.993 9.05Z" fill="url(#d)"/>
<path d="M38.243 9.483a13.5 13.5 0 0 1 13.512 0l24.06 13.907a13.5 13.5 0 0 1 6.745 11.688v27.844a13.5 13.5 0 0 1-6.745 11.688l-24.06 13.907a13.5 13.5 0 0 1-13.512 0L14.183 74.61A13.5 13.5 0 0 1 7.44 62.922V35.078a13.5 13.5 0 0 1 6.745-11.688l24.06-13.907Z" fill="url(#e)" stroke="#6F89A8"/>
<path d="m8.41 66.388-.016-.056a12.979 12.979 0 0 1-.332-1.626 45.514 45.514 0 0 1 1.248-3.714 63.525 63.525 0 0 1 4.311-9.055c2.51-4.329 3.906-5.957 5.005-6.991.252-.238.483-.44.704-.632.737-.643 1.364-1.191 2.3-2.642.179-.276.354-.784.556-1.524.111-.405.224-.855.348-1.349.1-.4.207-.828.327-1.284.49-1.876 1.16-4.136 2.26-6.361a68.344 68.344 0 0 0-.28-.022h-.001c-.208-.016-.431-.033-.646-.053-.761-.072-1.632-.198-2.395-.549-.776-.357-1.448-.95-1.777-1.94a49.016 49.016 0 0 0-.21-.61c-.1-.286-.2-.573-.286-.843-.152-.476-.281-.954-.327-1.402-.046-.448-.012-.9.196-1.298.213-.407.583-.71 1.11-.906.826-.306 2.14-.519 4.072-.128 1.528.31 2.796 1.392 3.696 3.135a15.52 15.52 0 0 1 2.778-2.33c2.63-1.725 6.08-2.833 10.645-2.833 2.565 0 4.758.366 6.625 1.017 1.946-1.573 4.1-1.342 4.75-1.017.679.34 1.563 1.6.797 4.618.502.549.952 1.135 1.352 1.75.64.048 1.403.96 1.78 2.175.24.768.272 1.49.129 1.992.692 2.267.97 4.668.99 6.956 1.54.174 2.691-.152 3.503-.569a5.162 5.162 0 0 0 1.292-.935 4.063 4.063 0 0 0 .396-.461l.002-.004.343.152.341.153-.001.002-.002.004a.6.6 0 0 1-.034.047c-.022.03-.053.073-.095.126a4.891 4.891 0 0 1-.37.413c-.33.33-.83.747-1.516 1.099-.954.488-2.234.835-3.86.683a32.323 32.323 0 0 1-.06 1.54c1.255.33 2.218.239 2.892.05a3.8 3.8 0 0 0 1.005-.44 2.574 2.574 0 0 0 .296-.218l.01-.01c.154-.15.405-.173.563-.052.159.121.163.341.01.491l-.287-.219.287.22-.002.001-.003.003-.008.008a.67.67 0 0 1-.015.013l-.01.01c-.021.018-.05.044-.088.074-.075.06-.183.142-.325.232a4.74 4.74 0 0 1-1.253.55c-.797.224-1.842.313-3.125.016l-.005.062c-.18 2.094-.54 3.984-.943 5.443-.372 1.347-.151 2.5.45 3.497.61 1.009 1.623 1.874 2.856 2.598 1.502.881 3.292 1.53 4.978 1.968L67 54.846c.183-1.566 1.412-4.568 5.2-5.115 5.073-.731 6.897 2.29 7.005 3.032.011.08.032.197.059.344.221 1.22.808 4.456-.848 4.695l-1.114.16c.09.619-.03 1.899-1.217 2.07-.046.554-.539 1.694-2.137 1.824-1.17.094-1.867-.139-2.47-.341-.384-.128-.729-.244-1.133-.255l.148.357c1.943.168 3.382 2.09 3.382 4.3 0 .108-.004.214-.01.32l2.893 7.161a12.98 12.98 0 0 1-.633.437v-.001c-.183.118-.37.233-.56.343l-.27.156-23.789 13.751a12.992 12.992 0 0 1-6.983 1.737 12.99 12.99 0 0 1-6.03-1.737l-2.29-1.324-21.768-12.583a13 13 0 0 1-6.024-7.79Z" fill="url(#f)"/>
<path d="m63.654 37.05-.341-.153a.455.455 0 0 1 .547-.162c.189.084.25.293.136.467l-.342-.153Z" fill="url(#g)"/>
<defs>
<linearGradient id="a" x1="23.375" y1="25.237" x2="23.375" y2="72.763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="b" x1="45" y1="3.875" x2="86" y2="3.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#E0EEFF"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="c" x1="4.875" y1="72.75" x2="45" y2="72.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1E5FE"/>
<stop offset="1" stop-color="#F8FBFF"/>
</linearGradient>
<linearGradient id="d" x1="6.999" y1="46.375" x2="82.999" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5E8FF"/>
<stop offset="1" stop-color="#89BEFF"/>
</linearGradient>
<linearGradient id="e" x1="6.999" y1="46.375" x2="82.999" y2="46.375" gradientUnits="userSpaceOnUse">
<stop stop-color="#D0DCEC"/>
<stop offset=".5" stop-color="#A8BCD5"/>
</linearGradient>
<linearGradient id="f" x1="43.804" y1="21.139" x2="43.804" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
<linearGradient id="g" x1="43.804" y1="21.139" x2="43.804" y2="89.829" gradientUnits="userSpaceOnUse">
<stop stop-color="#4C6788"/>
<stop offset="1" stop-color="#6F89A8"/>
</linearGradient>
</defs>
</svg>
export type RewardsConfigResponse = {
rewards: {
registration: string;
registration_with_referral: string;
daily_claim: string;
referral_share: string;
};
};
export type RewardsCheckRefCodeResponse = {
valid: boolean;
};
export type RewardsNonceResponse = {
nonce: string;
};
export type RewardsCheckUserResponse = {
exists: boolean;
};
export type RewardsLoginResponse = {
created: boolean;
token: string;
};
export type RewardsUserBalancesResponse = {
total: string;
staked: string;
unstaked: string;
total_staking_rewards: string;
total_referral_rewards: string;
pending_referral_rewards: string;
};
export type RewardsUserDailyCheckResponse = {
available: boolean;
daily_reward: string;
pending_referral_rewards: string;
date: string;
reset_at: string;
};
export type RewardsUserDailyClaimResponse = {
daily_reward: string;
pending_referral_rewards: string;
};
export type RewardsUserReferralsResponse = {
code: string;
link: string;
referrals: string;
};
import type { BrowserContext } from '@playwright/test'; import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as rewardsBalanceMock from 'mocks/rewards/balance';
import * as dailyRewardMock from 'mocks/rewards/dailyReward';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import { contextWithAuth } from 'playwright/fixtures/auth'; import { contextWithAuth } from 'playwright/fixtures/auth';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { contextWithRewards } from 'playwright/fixtures/rewards';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config'; import * as pwConfig from 'playwright/utils/config';
...@@ -10,12 +14,15 @@ import HeroBanner from './HeroBanner'; ...@@ -10,12 +14,15 @@ import HeroBanner from './HeroBanner';
const authTest = test.extend<{ context: BrowserContext }>({ const authTest = test.extend<{ context: BrowserContext }>({
context: contextWithAuth, context: contextWithAuth,
}).extend<{ context: BrowserContext }>({
context: contextWithRewards,
}); });
authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse }) => { authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse }) => {
const IMAGE_URL = 'https://localhost:3000/my-image.png'; const IMAGE_URL = 'https://localhost:3000/my-image.png';
await mockEnvs([ await mockEnvs([
...ENVS_MAP.rewardsService,
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
[ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ], [ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ],
]); ]);
...@@ -27,7 +34,9 @@ authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiRes ...@@ -27,7 +34,9 @@ authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiRes
}); });
}); });
await mockApiResponse('user_info', profileMock.base); await mockApiResponse('user_info', profileMock.withEmailAndWallet);
await mockApiResponse('rewards_user_balances', rewardsBalanceMock.base);
await mockApiResponse('rewards_user_daily_check', dailyRewardMock.base);
const component = await render(<HeroBanner/>); const component = await render(<HeroBanner/>);
......
...@@ -2,6 +2,7 @@ import { Box, Flex, Heading, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Box, Flex, Heading, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import RewardsButton from 'ui/rewards/RewardsButton';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop'; import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
...@@ -67,7 +68,8 @@ const HeroBanner = () => { ...@@ -67,7 +68,8 @@ const HeroBanner = () => {
} }
</Heading> </Heading>
{ config.UI.navigation.layout === 'vertical' && ( { config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'flex' }} gap={ 2 }>
{ config.features.rewards.isEnabled && <RewardsButton variant="hero"/> }
{ {
(config.features.account.isEnabled && <UserProfileDesktop buttonVariant="hero"/>) || (config.features.account.isEnabled && <UserProfileDesktop buttonVariant="hero"/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonVariant="hero"/>) (config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonVariant="hero"/>)
......
import { chakra, Flex, Tooltip, Skeleton, Box } from '@chakra-ui/react'; import { chakra, Flex, Tooltip, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReport, ContractListTypes } from 'types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReport, ContractListTypes } from 'types/client/marketplace';
...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes'; ...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import RewardsButton from 'ui/rewards/RewardsButton';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
...@@ -98,12 +99,13 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) ...@@ -98,12 +99,13 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
source="App page" source="App page"
/> />
{ !isMobile && ( { !isMobile && (
<Box ml="auto"> <Flex ml="auto" gap={ 2 }>
{ config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
{ {
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) || (config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>) (config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
} }
</Box> </Flex>
) } ) }
</Flex> </Flex>
{ contractListType && ( { contractListType && (
......
...@@ -4,7 +4,9 @@ import React from 'react'; ...@@ -4,7 +4,9 @@ import React from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import config from 'configs/app';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props { interface Props {
profileQuery: UseQueryResult<UserInfo, unknown>; profileQuery: UseQueryResult<UserInfo, unknown>;
...@@ -18,7 +20,15 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => { ...@@ -18,7 +20,15 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => {
<section> <section>
<Heading as="h2" size="sm" mb={ 3 }>My linked wallet</Heading> <Heading as="h2" size="sm" mb={ 3 }>My linked wallet</Heading>
<Text mb={ 3 } > <Text mb={ 3 } >
This wallet address is used for login and participation in the merit program This wallet address is used for login{ ' ' }
{ config.features.rewards.isEnabled && (
<>
and participation in the Merits Program.
<LinkExternal href="https://docs.blockscout.com/using-blockscout/merits" ml={ 1 }>
Learn more
</LinkExternal>
</>
) }
</Text> </Text>
{ profileQuery.data?.address_hash ? ( { profileQuery.data?.address_hash ? (
<Box px={ 3 } py="18px" bgColor={ bgColor } borderRadius="base"> <Box px={ 3 } py="18px" bgColor={ bgColor } borderRadius="base">
......
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import * as rewardsBalanceMock from 'mocks/rewards/balance';
import * as dailyRewardMock from 'mocks/rewards/dailyReward';
import * as referralsMock from 'mocks/rewards/referrals';
import * as rewardsConfigMock from 'mocks/rewards/rewardsConfig';
import * as profileMock from 'mocks/user/profile';
import { contextWithAuth } from 'playwright/fixtures/auth';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { contextWithRewards } from 'playwright/fixtures/rewards';
import { test, expect } from 'playwright/lib';
import RewardsDashboard from './RewardsDashboard';
const testWithAuth = test.extend<{ context: BrowserContext }>({
context: contextWithAuth,
}).extend<{ context: BrowserContext }>({
context: contextWithRewards,
});
testWithAuth.beforeEach(async({ mockEnvs, mockApiResponse }) => {
await mockEnvs([ ...ENVS_MAP.rewardsService ]);
await mockApiResponse('user_info', profileMock.withEmailAndWallet);
});
testWithAuth('base view +@dark-mode +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('rewards_user_balances', rewardsBalanceMock.base);
await mockApiResponse('rewards_user_daily_check', dailyRewardMock.base);
await mockApiResponse('rewards_user_referrals', referralsMock.base);
await mockApiResponse('rewards_config', rewardsConfigMock.base);
const component = await render(<RewardsDashboard/>);
await expect(component).toHaveScreenshot();
});
testWithAuth('with error', async({ render }) => {
const component = await render(<RewardsDashboard/>);
await expect(component).toHaveScreenshot();
});
import { Flex, Skeleton, Image, Alert } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import config from 'configs/app';
import { useRewardsContext } from 'lib/contexts/rewards';
import { apos } from 'lib/html-entities';
import DailyRewardClaimButton from 'ui/rewards/dashboard/DailyRewardClaimButton';
import RewardsDashboardCard from 'ui/rewards/dashboard/RewardsDashboardCard';
import RewardsDashboardCardValue from 'ui/rewards/dashboard/RewardsDashboardCardValue';
import RewardsReadOnlyInputWithCopy from 'ui/rewards/RewardsReadOnlyInputWithCopy';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
const RewardsDashboard = () => {
const { balancesQuery, apiToken, referralsQuery, rewardsConfigQuery, dailyRewardQuery, isInitialized } = useRewardsContext();
const [ isError, setIsError ] = useState(false);
useRedirectForInvalidAuthToken();
useEffect(() => {
if (!config.features.rewards.isEnabled || (isInitialized && !apiToken)) {
window.location.assign('/');
}
}, [ isInitialized, apiToken ]);
useEffect(() => {
setIsError(balancesQuery.isError || referralsQuery.isError || rewardsConfigQuery.isError || dailyRewardQuery.isError);
}, [ balancesQuery.isError, referralsQuery.isError, rewardsConfigQuery.isError, dailyRewardQuery.isError ]);
if (!config.features.rewards.isEnabled) {
return null;
}
return (
<>
<PageTitle
title="Dashboard"
secondRow={ (
<span>
The Blockscout Merits Program is just getting started! Learn more about the details,
features, and future plans in our{ ' ' }
<LinkExternal href="https://www.blog.blockscout.com/blockscout-merits-rewarding-block-explorer-skills">
blog post
</LinkExternal>.
</span>
) }
/>
<Flex flexDirection="column" alignItems="flex-start" w="full" gap={ 6 }>
{ isError && <Alert status="error">Failed to load some data. Please try again later.</Alert> }
<Flex gap={ 6 } flexDirection={{ base: 'column', md: 'row' }}>
<RewardsDashboardCard
description="Claim your daily Merits and any Merits received from referrals."
direction="column-reverse"
contentAfter={ <DailyRewardClaimButton/> }
>
<RewardsDashboardCardValue
label="Total balance"
value={ balancesQuery.data?.total || 'N/A' }
isLoading={ balancesQuery.isPending }
withIcon
hint={ (
<>
Total number of Merits earned from all activities.{ ' ' }
<LinkExternal href="https://docs.blockscout.com/using-blockscout/merits">
More info on Merits
</LinkExternal>
</>
) }
/>
</RewardsDashboardCard>
<RewardsDashboardCard
title="Referrals"
description="Total number of users who have joined the program using your code or referral link."
direction="column-reverse"
>
<RewardsDashboardCardValue
label="Referrals"
value={ referralsQuery.data?.referrals ?
`${ referralsQuery.data?.referrals } user${ Number(referralsQuery.data?.referrals) === 1 ? '' : 's' }` :
'N/A'
}
isLoading={ referralsQuery.isPending }
hint="The number of referrals who registered with your code/link."
/>
</RewardsDashboardCard>
<RewardsDashboardCard
title="Streaks"
description={ `Current number of consecutive days you${ apos }ve claimed your daily Merits.` }
direction="column-reverse"
availableSoon
blurFilter
>
<RewardsDashboardCardValue label="Streaks" value="5 days"/>
</RewardsDashboardCard>
</Flex>
<RewardsDashboardCard
title="Referral program"
description={ (
<>
Refer friends and boost your Merits! You receive a{ ' ' }
<Skeleton as="span" isLoaded={ !rewardsConfigQuery.isPending }>
{ rewardsConfigQuery.data?.rewards.referral_share ?
`${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` :
'N/A'
}
</Skeleton>
{ ' ' }bonus on all Merits earned by your referrals.
</>
) }
direction="row"
>
<Flex
flex={ 1 }
gap={{ base: 2, md: 6 }}
px={{ base: 4, md: 6 }}
py={{ base: 4, md: 0 }}
flexDirection={{ base: 'column', md: 'row' }}
>
<RewardsReadOnlyInputWithCopy
label="Referral link"
value={ referralsQuery.data?.link || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 2 }
/>
<RewardsReadOnlyInputWithCopy
label="Referral code"
value={ referralsQuery.data?.code || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 1 }
/>
</Flex>
</RewardsDashboardCard>
<Flex gap={ 6 } flexDirection={{ base: 'column', md: 'row' }}>
<RewardsDashboardCard
title="Activity"
description="Earn Merits for your everyday Blockscout activities. You deserve to be rewarded for choosing open-source public goods!"
availableSoon
blurFilter
>
<RewardsDashboardCardValue label="Activity" value="0%"/>
<RewardsDashboardCardValue label="Received" value="0" withIcon/>
</RewardsDashboardCard>
<RewardsDashboardCard
title="Verify contracts"
description="Verified contracts are so important for transparency and interaction. Verify your contracts on Blockscout and receive Merits for your efforts." // eslint-disable-line max-len
availableSoon
blurFilter
>
<RewardsDashboardCardValue label="Activity" value="0%"/>
<RewardsDashboardCardValue label="Received" value="0" withIcon/>
</RewardsDashboardCard>
</Flex>
<RewardsDashboardCard
title="Badges"
description={ (
<Flex flexDir="column" gap={ 2 }>
<span>
Collect limited and legendary badges by completing different Blockscout related tasks.
Go to the badges website to see what{ apos }s available and start your collection today.
</span>
<LinkExternal
href="https://badges.blockscout.com?utm_source=blockscout&utm_medium=merits-dashboard"
fontSize="md"
fontWeight="500"
>
Go to website
</LinkExternal>
</Flex>
) }
direction="row"
availableSoon
>
<Flex
flex={ 1 }
px={{ base: 4, md: 6 }}
py={{ base: 4, md: 0 }}
justifyContent="space-between"
gap={ 2 }
>
{ Array(5).fill(null).map((_, index) => (
<Image
key={ index }
display={{ base: index > 2 ? 'none' : 'block', sm: 'block' }}
src={ `/static/badges/badge_${ index + 1 }.svg` }
alt={ `Badge ${ index + 1 }` }
w={{ base: 'calc((100% - 16px) / 3)', sm: 'calc((100% - 32px) / 5)' }}
maxW={{ base: '80px', md: '100px' }}
maxH={{ base: '80px', md: '100px' }}
fallback={ (
<Skeleton
display={{ base: index > 2 ? 'none' : 'block', sm: 'block' }}
w={{ base: 'calc((100% - 16px) / 3)', sm: 'calc((100% - 32px) / 5)' }}
maxW={{ base: '80px', md: '100px' }}
maxH={{ base: '80px', md: '100px' }}
aspectRatio={ 1 }
/>
) }
/>
)) }
</Flex>
</RewardsDashboardCard>
</Flex>
</>
);
};
export default RewardsDashboard;
import { Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
// This icon doesn't work properly when it is in the sprite
// Probably because of the gradient
// eslint-disable-next-line no-restricted-imports
import meritsIcon from 'icons/merits_colored.svg';
type Props = {
className?: string;
}
const MeritsIcon = ({ className }: Props) => {
const shadow = useColorModeValue('drop-shadow(0px 4px 2px rgba(141, 179, 204, 0.25))', 'none');
return (
<Icon as={ meritsIcon } className={ className } filter={ shadow }/>
);
};
export default chakra(MeritsIcon);
import type { ButtonProps } from '@chakra-ui/react';
import { Button, chakra, Tooltip } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import { route } from 'nextjs-routes';
import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
type Props = {
size?: ButtonProps['size'];
variant?: ButtonProps['variant'];
};
const RewardsButton = ({ variant = 'header', size }: Props) => {
const { isInitialized, apiToken, openLoginModal, dailyRewardQuery, balancesQuery } = useRewardsContext();
const isMobile = useIsMobile();
const isLoading = !isInitialized || dailyRewardQuery.isLoading || balancesQuery.isLoading;
const handleFocus = useCallback((e: React.FocusEvent<HTMLButtonElement>) => {
e.preventDefault();
}, []);
return (
<Tooltip
label="Earn Merits for using Blockscout"
textAlign="center"
padding={ 2 }
openDelay={ 500 }
isDisabled={ isMobile || isLoading || Boolean(apiToken) }
width="150px"
>
<Button
variant={ variant }
data-selected={ !isLoading && Boolean(apiToken) }
flexShrink={ 0 }
as={ apiToken ? LinkInternal : 'button' }
{ ...(apiToken ? { href: route({ pathname: '/account/rewards' }) } : {}) }
onClick={ apiToken ? undefined : openLoginModal }
onFocus={ handleFocus }
fontSize="sm"
size={ size }
px={ !isLoading && Boolean(apiToken) ? 2.5 : 4 }
isLoading={ isLoading }
loadingText={ isMobile ? undefined : 'Merits' }
_hover={{
textDecoration: 'none',
}}
>
<IconSvg
name={ dailyRewardQuery.data?.available ? 'merits_with_dot_slim' : 'merits_slim' }
boxSize={ variant === 'hero' ? 6 : 5 }
flexShrink={ 0 }
/>
<chakra.span
display={{ base: 'none', md: 'inline' }}
ml={ 2 }
fontWeight={ apiToken ? '700' : '600' }
>
{ apiToken ? (balancesQuery.data?.total || 'N/A') : 'Merits' }
</chakra.span>
</Button>
</Tooltip>
);
};
export default RewardsButton;
import { FormControl, Input, InputGroup, InputRightElement, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
type Props = {
label: string;
value: string;
className?: string;
isLoading?: boolean;
};
const RewardsReadOnlyInputWithCopy = ({ label, value, className, isLoading }: Props) => (
<FormControl variant="floating" id={ label } className={ className }>
<Skeleton isLoaded={ !isLoading }>
<InputGroup>
<Input
readOnly
fontWeight="500"
value={ value }
overflow="hidden"
textOverflow="ellipsis"
sx={{
'&:not(:placeholder-shown)': {
pr: '40px',
},
}}
/>
<FormInputPlaceholder text={ label }/>
<InputRightElement w="40px" display="flex" justifyContent="flex-end" pr={ 2 }>
<CopyToClipboard text={ value }/>
</InputRightElement>
</InputGroup>
</Skeleton>
</FormControl>
);
export default chakra(RewardsReadOnlyInputWithCopy);
import { Button, useBoolean, Flex, useColorModeValue } from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { SECOND } from 'lib/consts';
import { useRewardsContext } from 'lib/contexts/rewards';
import splitSecondsInPeriods from 'ui/blockCountdown/splitSecondsInPeriods';
const DailyRewardClaimButton = () => {
const { balancesQuery, dailyRewardQuery, claim } = useRewardsContext();
const [ isClaiming, setIsClaiming ] = useBoolean(false);
const [ timeLeft, setTimeLeft ] = React.useState<string>('');
const dailyRewardValue = useMemo(() =>
dailyRewardQuery.data ?
Number((Number(dailyRewardQuery.data.daily_reward) + Number(dailyRewardQuery.data.pending_referral_rewards)).toFixed(2)) :
0,
[ dailyRewardQuery.data ]);
const handleClaim = useCallback(async() => {
setIsClaiming.on();
try {
await claim();
await Promise.all([
balancesQuery.refetch(),
dailyRewardQuery.refetch(),
]);
} catch (error) {}
setIsClaiming.off();
}, [ claim, setIsClaiming, balancesQuery, dailyRewardQuery ]);
useEffect(() => {
if (!dailyRewardQuery.data?.reset_at) {
return;
}
// format the date to be compatible with the Date constructor
const formattedDate = dailyRewardQuery.data.reset_at.replace(' ', 'T').replace(' UTC', 'Z');
const target = new Date(formattedDate).getTime();
let interval = 0;
const updateCountdown = (target: number) => {
const now = new Date().getTime();
const difference = target - now;
if (difference > 0) {
const { hours, minutes, seconds } = splitSecondsInPeriods(Math.floor(difference / SECOND));
setTimeLeft(`${ hours }:${ minutes }:${ seconds }`);
} else {
setTimeLeft('00:00:00');
dailyRewardQuery.refetch();
clearInterval(interval);
}
};
updateCountdown(target);
interval = window.setInterval(() => {
updateCountdown(target);
}, SECOND);
return () => clearInterval(interval);
}, [ dailyRewardQuery ]);
const isLoading = isClaiming || dailyRewardQuery.isPending || dailyRewardQuery.isFetching;
const timerBgColor = useColorModeValue('gray.200', 'gray.800');
return !isLoading && !dailyRewardQuery.data?.available ? (
<Flex
h="40px"
alignItems="center"
justifyContent="center"
borderRadius="base"
color="gray.500"
bgColor={ timerBgColor }
fontSize="md"
fontWeight="600"
cursor="default"
>
Next claim in { timeLeft || 'N/A' }
</Flex>
) : (
<Button onClick={ handleClaim } isLoading={ isLoading }>
Claim { dailyRewardValue } Merits
</Button>
);
};
export default DailyRewardClaimButton;
import { Flex, Text, useColorModeValue, Tag } from '@chakra-ui/react';
import React from 'react';
type Props = {
title?: string;
description: string | React.ReactNode;
availableSoon?: boolean;
blurFilter?: boolean;
contentAfter?: React.ReactNode;
direction?: 'column' | 'column-reverse' | 'row';
reverse?: boolean;
children?: React.ReactNode;
};
const RewardsDashboardCard = ({
title, description, availableSoon, contentAfter,
direction = 'column', children, blurFilter,
}: Props) => {
return (
<Flex
flexDirection={{ base: direction === 'row' ? 'column' : direction, md: direction }}
justifyContent={ direction === 'column-reverse' ? 'flex-end' : 'flex-start' }
p={{ base: 1.5, md: 2 }}
border="1px solid"
borderColor={ useColorModeValue('gray.200', 'whiteAlpha.200') }
borderRadius="lg"
gap={{ base: 1, md: direction === 'row' ? 10 : 1 }}
w={ direction === 'row' ? 'full' : 'auto' }
flex={ direction !== 'row' ? 1 : '0 1 auto' }
>
<Flex
flexDirection="column"
gap={ 2 }
p={{ base: 1.5, md: 3 }}
w={{ base: 'full', md: direction === 'row' ? '340px' : 'full' }}
>
{ title && (
<Flex alignItems="center" gap={ 2 }>
<Text fontSize={{ base: 'md', md: 'lg' }} fontWeight="500">{ title }</Text>
{ availableSoon && <Tag colorScheme="blue">Available soon</Tag> }
</Flex>
) }
<Text as="div" fontSize="sm">
{ description }
</Text>
{ contentAfter }
</Flex>
<Flex
alignItems="center"
justifyContent="space-around"
borderRadius={{ base: 'lg', md: '8px' }}
backgroundColor={ useColorModeValue('gray.50', 'whiteAlpha.50') }
h={{ base: '80px', md: direction === 'row' ? 'auto' : '128px' }}
filter="auto"
blur={ blurFilter ? '4px' : '0' }
flex={ direction === 'row' ? 1 : '0 1 auto' }
>
{ children }
</Flex>
</Flex>
);
};
export default RewardsDashboardCard;
import { Flex, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import HintPopover from 'ui/shared/HintPopover';
import MeritsIcon from '../MeritsIcon';
type Props = {
label: string;
value: number | string | undefined;
withIcon?: boolean;
hint?: string | React.ReactNode;
isLoading?: boolean;
}
const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading }: Props) => (
<Flex key={ label } flexDirection="column" alignItems="center" gap={ 2 }>
<Flex alignItems="center" gap={ 1 }>
{ hint && (
<HintPopover
label={ hint }
popoverContentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '210px' } }}
popoverBodyProps={{ textAlign: 'center' }}
/>
) }
<Text fontSize="xs" fontWeight="500" variant="secondary">
{ label }
</Text>
</Flex>
<Skeleton
isLoaded={ !isLoading }
display="flex"
alignItems="center"
justifyContent="center"
gap={ 2 }
minW="100px"
>
{ withIcon && <MeritsIcon boxSize={ 8 }/> }
<Text fontSize={{ base: '24px', md: '32px' }} lineHeight={{ base: '24px', md: 1.5 }} fontWeight="500">
{ value }
</Text>
</Skeleton>
</Flex>
);
export default RewardsDashboardCard;
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWallet from 'lib/web3/useWallet';
import CongratsStepContent from './steps/CongratsStepContent';
import LoginStepContent from './steps/LoginStepContent';
const RewardsLoginModal = () => {
const { isOpen: isWalletModalOpen } = useWallet({ source: 'Merits' });
const isMobile = useIsMobile();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
const [ isReferral, setIsReferral ] = useBoolean(false);
useEffect(() => {
if (!isLoginModalOpen) {
setIsLoginStep.on();
setIsReferral.off();
}
}, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]);
const goNext = useCallback((isReferral: boolean) => {
if (isReferral) {
setIsReferral.on();
}
setIsLoginStep.off();
}, [ setIsLoginStep, setIsReferral ]);
return (
<Modal
isOpen={ isLoginModalOpen && !isWalletModalOpen }
onClose={ closeLoginModal }
size={ isMobile ? 'full' : 'sm' }
isCentered
>
<ModalOverlay/>
<ModalContent width={ isLoginStep ? '400px' : '560px' } p={ 6 }>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 3 }>
{ isLoginStep ? 'Login' : 'Congratulations' }
</ModalHeader>
<ModalCloseButton top={ 6 } right={ 6 }/>
<ModalBody mb={ 0 }>
{ isLoginStep ?
<LoginStepContent goNext={ goNext } closeModal={ closeLoginModal }/> :
<CongratsStepContent isReferral={ isReferral }/>
}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default RewardsLoginModal;
import { Text, Box, Flex, Button, Skeleton, useColorModeValue, Tag } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import { useRewardsContext } from 'lib/contexts/rewards';
import IconSvg from 'ui/shared/IconSvg';
import MeritsIcon from '../../MeritsIcon';
import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
type Props = {
isReferral: boolean;
}
const CongratsStepContent = ({ isReferral }: Props) => {
const { referralsQuery, rewardsConfigQuery } = useRewardsContext();
const registrationReward = rewardsConfigQuery.data?.rewards.registration;
const registrationWithReferralReward = rewardsConfigQuery.data?.rewards.registration_with_referral;
const referralReward = Number(registrationWithReferralReward) - Number(registrationReward);
const refLink = referralsQuery.data?.link || 'N/A';
const shareText = `I joined the @blockscoutcom Merits Program and got my first ${ registrationReward || 'N/A' } #Merits! Use this link for a sign-up bonus and start earning rewards with @blockscoutcom block explorer.\n\n${ refLink }`; // eslint-disable-line max-len
const textColor = useColorModeValue('blue.700', 'blue.100');
const dividerColor = useColorModeValue('whiteAlpha.800', 'whiteAlpha.100');
return (
<>
<Flex
alignItems="center"
background={ useColorModeValue('linear-gradient(254.96deg, #9CD8FF 9.09%, #D0EFFF 88.45%)', 'linear-gradient(255deg, #1B253B 9.09%, #222C3F 88.45%)') }
borderRadius="md"
p={ 2 }
pl={{ base: isReferral ? 4 : 8, md: 8 }}
mb={ 8 }
h="90px"
>
<MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<Text fontSize={{ base: isReferral ? '24px' : '30px', md: '30px' }} fontWeight="700" color={ textColor }>
+{ rewardsConfigQuery.data?.rewards[ isReferral ? 'registration_with_referral' : 'registration' ] || 'N/A' }
</Text>
</Skeleton>
{ isReferral && (
<Flex alignItems="center" h="56px">
<Box w="1px" h="full" bgColor={ dividerColor } mx={{ base: 3, md: 8 }}/>
<Flex flexDirection="column" justifyContent="space-between" gap={ 2 }>
{ [
{
title: 'Registration',
value: registrationReward || 'N/A',
},
{
title: 'Referral program',
value: referralReward || 'N/A',
},
].map(({ title, value }) => (
<Flex key={ title } alignItems="center" gap={{ base: 1, md: 2 }}>
<MeritsIcon boxSize={{ base: 5, md: 6 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<Text fontSize="sm" fontWeight="700" color={ textColor }>
+{ value }
</Text>
</Skeleton>
<Text fontSize="sm" color={ textColor }>
{ title }
</Text>
</Flex>
)) }
</Flex>
</Flex>
) }
</Flex>
<Flex flexDirection="column" alignItems="flex-start" px={ 3 } mb={ 8 }>
<Flex alignItems="center" gap={ 2 }>
<Tag colorScheme="blue" w={ 8 } h={ 8 } display="flex" alignItems="center" justifyContent="center" borderRadius="8px">
<IconSvg name="profile" boxSize={ 5 }/>
</Tag>
<Text fontSize="lg" fontWeight="500">
Referral program
</Text>
</Flex>
<Text fontSize="md" mt={ 2 }>
Receive a{ ' ' }
<Skeleton as="span" isLoaded={ !rewardsConfigQuery.isLoading }>
{ rewardsConfigQuery.data?.rewards.referral_share ?
`${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` :
'N/A'
}
</Skeleton>
{ ' ' }bonus on all Merits earned by your referrals
</Text>
<RewardsReadOnlyInputWithCopy
label="Referral link"
value={ refLink }
isLoading={ referralsQuery.isLoading }
mt={ 3 }
/>
<Button
as="a"
target="_blank"
mt={ 6 }
isLoading={ referralsQuery.isLoading }
href={ `https://x.com/intent/tweet?text=${ encodeURIComponent(shareText) }` }
>
Share on <IconSvg name="social/twitter" boxSize={ 6 } ml={ 1 }/>
</Button>
</Flex>
<Flex flexDirection="column" alignItems="flex-start" px={ 3 }>
<Flex alignItems="center" gap={ 2 }>
<Tag colorScheme="blue" w={ 8 } h={ 8 } display="flex" alignItems="center" justifyContent="center" borderRadius="8px">
<IconSvg name="stats" boxSize={ 6 }/>
</Tag>
<Text fontSize="lg" fontWeight="500">
Dashboard
</Text>
</Flex>
<Text fontSize="md" mt={ 2 }>
Explore your current Merits balance, find activities to boost your Merits,
and view your capybara NFT badge collection on the dashboard
</Text>
<Button mt={ 3 } as="a" href={ route({ pathname: '/account/rewards' }) }>
Open
</Button>
</Flex>
</>
);
};
export default CongratsStepContent;
import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl, Alert, Skeleton, Divider } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards';
import * as cookies from 'lib/cookies';
import { apos } from 'lib/html-entities';
import useWallet from 'lib/web3/useWallet';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import LinkExternal from 'ui/shared/links/LinkExternal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import useSignInWithWallet from 'ui/snippets/auth/useSignInWithWallet';
type Props = {
goNext: (isReferral: boolean) => void;
closeModal: () => void;
};
const LoginStepContent = ({ goNext, closeModal }: Props) => {
const router = useRouter();
const { connect, isConnected, address } = useWallet({ source: 'Merits' });
const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE);
const [ isRefCodeUsed, setIsRefCodeUsed ] = useBoolean(Boolean(savedRefCode));
const [ isLoading, setIsLoading ] = useBoolean(false);
const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false);
const { login, checkUserQuery } = useRewardsContext();
const profileQuery = useProfileQuery();
const isAddressMismatch = useMemo(() =>
Boolean(address) &&
Boolean(profileQuery.data?.address_hash) &&
profileQuery.data?.address_hash !== address,
[ address, profileQuery.data ]);
const isSignUp = useMemo(() =>
isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists,
[ isConnected, isAddressMismatch, checkUserQuery ]);
const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value);
}, []);
const loginToRewardsProgram = useCallback(async() => {
try {
setRefCodeError.off();
setIsLoading.on();
const { isNewUser, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : '');
if (invalidRefCodeError) {
setRefCodeError.on();
} else {
if (isNewUser) {
goNext(Boolean(refCode));
} else {
closeModal();
router.push({ pathname: '/account/rewards' }, undefined, { shallow: true });
}
}
} catch (error) {}
setIsLoading.off();
}, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError, isRefCodeUsed, isSignUp ]);
useEffect(() => {
if (isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6) {
setRefCodeError.on();
} else {
setRefCodeError.off();
}
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps
const { start: loginToAccount } = useSignInWithWallet({
isAuth: Boolean(!profileQuery.isLoading && profileQuery.data?.email),
onSuccess: loginToRewardsProgram,
onError: setIsLoading.off,
});
const handleLogin = useCallback(async() => {
if (!profileQuery.isLoading && !profileQuery.data?.address_hash) {
setIsLoading.on();
loginToAccount();
return;
}
loginToRewardsProgram();
}, [ loginToAccount, loginToRewardsProgram, profileQuery, setIsLoading ]);
return (
<>
<Image
src="/static/merits_program.png"
alt="Merits program"
mb={ 3 }
fallback={ <Skeleton w="full" h="120px" mb={ 3 }/> }
/>
<Box mb={ 6 }>
Merits are awarded for a variety of different Blockscout activities. Connect a wallet to get started.
<LinkExternal href="https://docs.blockscout.com/using-blockscout/merits" ml={ 1 } fontWeight="500">
More about Blockscout Merits
</LinkExternal>
</Box>
{ isSignUp && (
<Box mb={ 6 }>
<Divider bgColor="divider" mb={ 6 }/>
<Flex w="full" alignItems="center" justifyContent="space-between">
I have a referral code
<Switch
colorScheme="blue"
size="md"
isChecked={ isRefCodeUsed }
onChange={ setIsRefCodeUsed.toggle }
aria-label="Referral code switch"
/>
</Flex>
{ isRefCodeUsed && (
<>
<FormControl variant="floating" id="referral-code" mt={ 3 }>
<Input
fontWeight="500"
value={ refCode }
onChange={ handleRefCodeChange }
isInvalid={ refCodeError }
/>
<FormInputPlaceholder text="Code"/>
</FormControl>
<Text fontSize="sm" variant="secondary" mt={ 1 } color={ refCodeError ? 'red.500' : undefined }>
{ refCodeError ? 'Incorrect code or format' : 'The code should be in format XXXXXX' }
</Text>
</>
) }
</Box>
) }
{ isAddressMismatch && (
<Alert status="warning" mb={ 4 }>
Your wallet address doesn{ apos }t match the one in your Blockscout account. Please connect the correct wallet.
</Alert>
) }
<Button
variant="solid"
colorScheme="blue"
w="full"
whiteSpace="normal"
mb={ 4 }
onClick={ isConnected ? handleLogin : connect }
isLoading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching }
loadingText={ isLoading ? 'Sign message in your wallet' : undefined }
isDisabled={ isAddressMismatch || refCodeError }
>
{ isConnected ? 'Get started' : 'Connect wallet' }
</Button>
<Text fontSize="sm" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') } textAlign="center">
Already registered for Blockscout Merits on another network or chain? Connect the same wallet here.
</Text>
</>
);
};
export default LoginStepContent;
...@@ -140,6 +140,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -140,6 +140,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
<AuthModalScreenSuccessEmail <AuthModalScreenSuccessEmail
email={ currentStep.email } email={ currentStep.email }
onConnectWallet={ onNextStep } onConnectWallet={ onNextStep }
onClose={ onModalClose }
isAuth={ currentStep.isAuth } isAuth={ currentStep.isAuth }
profile={ currentStep.profile } profile={ currentStep.profile }
/> />
...@@ -149,6 +150,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -149,6 +150,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
<AuthModalScreenSuccessWallet <AuthModalScreenSuccessWallet
address={ currentStep.address } address={ currentStep.address }
onAddEmail={ onNextStep } onAddEmail={ onNextStep }
onClose={ onModalClose }
isAuth={ currentStep.isAuth } isAuth={ currentStep.isAuth }
profile={ currentStep.profile } profile={ currentStep.profile }
/> />
......
...@@ -9,22 +9,32 @@ import config from 'configs/app'; ...@@ -9,22 +9,32 @@ import config from 'configs/app';
interface Props { interface Props {
email: string; email: string;
onConnectWallet: (screen: Screen) => void; onConnectWallet: (screen: Screen) => void;
onClose: () => void;
isAuth?: boolean; isAuth?: boolean;
profile: UserInfo | undefined; profile: UserInfo | undefined;
} }
const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, isAuth, profile }: Props) => { const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth, profile }: Props) => {
const handleConnectWalletClick = React.useCallback(() => { const handleConnectWalletClick = React.useCallback(() => {
onConnectWallet({ type: 'connect_wallet', isAuth: true }); onConnectWallet({ type: 'connect_wallet', isAuth: true });
}, [ onConnectWallet ]); }, [ onConnectWallet ]);
if (isAuth) { if (isAuth) {
return ( return (
<Text> <Box>
Your account was linked to{ ' ' } <Text>
<chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' } Your account was linked to{ ' ' }
email. Use for the next login. <chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' }
</Text> email. Use for the next login.
</Text>
<Button
mt={ 6 }
variant="outline"
onClick={ onClose }
>
Got it!
</Button>
</Box>
); );
} }
...@@ -34,11 +44,19 @@ const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, isAuth, profile } ...@@ -34,11 +44,19 @@ const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, isAuth, profile }
<chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' } <chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' }
email has been successfully used to log in to your Blockscout account. email has been successfully used to log in to your Blockscout account.
</Text> </Text>
{ !profile?.address_hash && config.features.blockchainInteraction.isEnabled && ( { !profile?.address_hash && config.features.blockchainInteraction.isEnabled ? (
<> <>
<Text mt={ 6 }>Add your web3 wallet to safely interact with smart contracts and dapps inside Blockscout.</Text> <Text mt={ 6 }>Add your web3 wallet to safely interact with smart contracts and dapps inside Blockscout.</Text>
<Button mt={ 6 } onClick={ handleConnectWalletClick }>Connect wallet</Button> <Button mt={ 6 } onClick={ handleConnectWalletClick }>Connect wallet</Button>
</> </>
) : (
<Button
variant="outline"
mt={ 6 }
onClick={ onClose }
>
Got it!
</Button>
) } ) }
</Box> </Box>
); );
......
...@@ -9,22 +9,32 @@ import shortenString from 'lib/shortenString'; ...@@ -9,22 +9,32 @@ import shortenString from 'lib/shortenString';
interface Props { interface Props {
address: string; address: string;
onAddEmail: (screen: Screen) => void; onAddEmail: (screen: Screen) => void;
onClose: () => void;
isAuth?: boolean; isAuth?: boolean;
profile: UserInfo | undefined; profile: UserInfo | undefined;
} }
const AuthModalScreenSuccessWallet = ({ address, onAddEmail, isAuth, profile }: Props) => { const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, profile }: Props) => {
const handleAddEmailClick = React.useCallback(() => { const handleAddEmailClick = React.useCallback(() => {
onAddEmail({ type: 'email', isAuth: true }); onAddEmail({ type: 'email', isAuth: true });
}, [ onAddEmail ]); }, [ onAddEmail ]);
if (isAuth) { if (isAuth) {
return ( return (
<Text> <Box>
Your account was linked to{ ' ' } <Text>
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' } Your account was linked to{ ' ' }
wallet. Use for the next login. <chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
</Text> wallet. Use for the next login.
</Text>
<Button
mt={ 6 }
variant="outline"
onClick={ onClose }
>
Got it!
</Button>
</Box>
); );
} }
...@@ -35,11 +45,19 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, isAuth, profile }: ...@@ -35,11 +45,19 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, isAuth, profile }:
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' } <chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
has been successfully used to log in to your Blockscout account. has been successfully used to log in to your Blockscout account.
</Text> </Text>
{ !profile?.email && ( { !profile?.email ? (
<> <>
<Text mt={ 6 }>Add your email to receive notifications about addresses in your watch list.</Text> <Text mt={ 6 }>Add your email to receive notifications about addresses in your watch list.</Text>
<Button mt={ 6 } onClick={ handleAddEmailClick }>Add email</Button> <Button mt={ 6 } onClick={ handleAddEmailClick }>Add email</Button>
</> </>
) : (
<Button
mt={ 6 }
variant="outline"
onClick={ onClose }
>
Got it!
</Button>
) } ) }
</Box> </Box>
); );
......
import React from 'react'; import React from 'react';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import HeaderDesktop from './HeaderDesktop'; import HeaderDesktop from './HeaderDesktop';
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs([
...ENVS_MAP.rewardsService,
]);
});
test('default view +@dark-mode', async({ render }) => { test('default view +@dark-mode', async({ render }) => {
const component = await render(<HeaderDesktop/>); const component = await render(<HeaderDesktop/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
...@@ -2,19 +2,16 @@ import { HStack, Box } from '@chakra-ui/react'; ...@@ -2,19 +2,16 @@ import { HStack, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import RewardsButton from 'ui/rewards/RewardsButton';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop'; import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop'; import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
import Burger from './Burger';
type Props = { type Props = {
renderSearchBar?: () => React.ReactNode; renderSearchBar?: () => React.ReactNode;
isMarketplaceAppPage?: boolean;
} }
const HeaderDesktop = ({ renderSearchBar, isMarketplaceAppPage }: Props) => { const HeaderDesktop = ({ renderSearchBar }: Props) => {
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>; const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
...@@ -25,19 +22,14 @@ const HeaderDesktop = ({ renderSearchBar, isMarketplaceAppPage }: Props) => { ...@@ -25,19 +22,14 @@ const HeaderDesktop = ({ renderSearchBar, isMarketplaceAppPage }: Props) => {
width="100%" width="100%"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
gap={ 12 } gap={ 6 }
> >
{ isMarketplaceAppPage && (
<Box display="flex" alignItems="center" gap={ 3 }>
<Burger isMarketplaceAppPage/>
<NetworkLogo isCollapsed/>
</Box>
) }
<Box width="100%"> <Box width="100%">
{ searchBar } { searchBar }
</Box> </Box>
{ config.UI.navigation.layout === 'vertical' && ( { config.UI.navigation.layout === 'vertical' && (
<Box display="flex" flexShrink={ 0 }> <Box display="flex" gap={ 2 } flexShrink={ 0 }>
{ config.features.rewards.isEnabled && <RewardsButton/> }
{ {
(config.features.account.isEnabled && <UserProfileDesktop/>) || (config.features.account.isEnabled && <UserProfileDesktop/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop/>) (config.features.blockchainInteraction.isEnabled && <UserWalletDesktop/>)
......
import React from 'react'; import React from 'react';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib'; import { test, expect, devices } from 'playwright/lib';
import HeaderMobile from './HeaderMobile'; import HeaderMobile from './HeaderMobile';
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs([
...ENVS_MAP.rewardsService,
]);
});
test('default view +@dark-mode', async({ render, page }) => { test('default view +@dark-mode', async({ render, page }) => {
await render(<HeaderMobile/>); await render(<HeaderMobile/>);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } });
......
...@@ -4,6 +4,7 @@ import { useInView } from 'react-intersection-observer'; ...@@ -4,6 +4,7 @@ import { useInView } from 'react-intersection-observer';
import config from 'configs/app'; import config from 'configs/app';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import RewardsButton from 'ui/rewards/RewardsButton';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import UserProfileMobile from 'ui/snippets/user/profile/UserProfileMobile'; import UserProfileMobile from 'ui/snippets/user/profile/UserProfileMobile';
...@@ -48,6 +49,7 @@ const HeaderMobile = ({ hideSearchBar, renderSearchBar }: Props) => { ...@@ -48,6 +49,7 @@ const HeaderMobile = ({ hideSearchBar, renderSearchBar }: Props) => {
<Burger/> <Burger/>
<NetworkLogo ml={ 2 } mr="auto"/> <NetworkLogo ml={ 2 } mr="auto"/>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
{ config.features.rewards.isEnabled && <RewardsButton/> }
{ {
(config.features.account.isEnabled && <UserProfileMobile/>) || (config.features.account.isEnabled && <UserProfileMobile/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletMobile/>) || (config.features.blockchainInteraction.isEnabled && <UserWalletMobile/>) ||
......
import type { BrowserContext } from '@playwright/test'; import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as rewardsBalanceMock from 'mocks/rewards/balance';
import * as dailyRewardMock from 'mocks/rewards/dailyReward';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import { contextWithAuth } from 'playwright/fixtures/auth'; import { contextWithAuth } from 'playwright/fixtures/auth';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { contextWithRewards } from 'playwright/fixtures/rewards';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import NavigationDesktop from './NavigationDesktop'; import NavigationDesktop from './NavigationDesktop';
const testWithAuth = test.extend<{ context: BrowserContext }>({ const testWithAuth = test.extend<{ context: BrowserContext }>({
context: contextWithAuth, context: contextWithAuth,
}).extend<{ context: BrowserContext }>({
context: contextWithRewards,
}); });
testWithAuth('base view +@dark-mode', async({ render, mockApiResponse, mockEnvs, page }) => { testWithAuth('base view +@dark-mode', async({ render, mockApiResponse, mockEnvs, page }) => {
...@@ -20,10 +25,13 @@ testWithAuth('base view +@dark-mode', async({ render, mockApiResponse, mockEnvs, ...@@ -20,10 +25,13 @@ testWithAuth('base view +@dark-mode', async({ render, mockApiResponse, mockEnvs,
}, },
}; };
await mockApiResponse('user_info', profileMock.base); await mockApiResponse('user_info', profileMock.withEmailAndWallet);
await mockApiResponse('rewards_user_balances', rewardsBalanceMock.base);
await mockApiResponse('rewards_user_daily_check', dailyRewardMock.base);
await mockEnvs([ await mockEnvs([
...ENVS_MAP.userOps, ...ENVS_MAP.userOps,
...ENVS_MAP.nameService, ...ENVS_MAP.nameService,
...ENVS_MAP.rewardsService,
[ 'NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES', '["/blocks","/apps"]' ], [ 'NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES', '["/blocks","/apps"]' ],
]); ]);
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import RewardsButton from 'ui/rewards/RewardsButton';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils'; import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop'; import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
...@@ -38,10 +39,13 @@ const NavigationDesktop = () => { ...@@ -38,10 +39,13 @@ const NavigationDesktop = () => {
}) } }) }
</Flex> </Flex>
</chakra.nav> </chakra.nav>
{ <Flex gap={ 2 }>
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) || { config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>) {
} (config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
}
</Flex>
</Flex> </Flex>
</Box> </Box>
); );
......
...@@ -7,6 +7,7 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -7,6 +7,7 @@ import IconSvg from 'ui/shared/IconSvg';
import useIsAuth from 'ui/snippets/auth/useIsAuth'; import useIsAuth from 'ui/snippets/auth/useIsAuth';
import NavLink from '../vertical/NavLink'; import NavLink from '../vertical/NavLink';
import NavLinkRewards from '../vertical/NavLinkRewards';
import NavLinkGroup from './NavLinkGroup'; import NavLinkGroup from './NavLinkGroup';
const DRAWER_WIDTH = 330; const DRAWER_WIDTH = 330;
...@@ -82,6 +83,7 @@ const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => { ...@@ -82,6 +83,7 @@ const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => {
borderColor="divider" borderColor="divider"
> >
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
<NavLinkRewards onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>
{ accountNavItems.map((item) => <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
...@@ -120,8 +122,11 @@ const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => { ...@@ -120,8 +122,11 @@ const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => {
> >
{ item.map(subItem => <NavLink key={ subItem.text } item={ subItem } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) } { item.map(subItem => <NavLink key={ subItem.text } item={ subItem } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
</Box> </Box>
) : ) : (
<NavLink key={ item.text } item={ item } mb={ 1 } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>, <Box key={ item.text } mb={ 1 }>
<NavLink item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>
</Box>
),
) } ) }
</Box> </Box>
</Box> </Box>
......
import { Link, Text, HStack, Tooltip, Box, useBreakpointValue, chakra, shouldForwardProp } from '@chakra-ui/react'; import { Link, Text, HStack, Tooltip, Box, useBreakpointValue } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
...@@ -18,23 +18,22 @@ import { checkRouteHighlight } from '../utils'; ...@@ -18,23 +18,22 @@ import { checkRouteHighlight } from '../utils';
type Props = { type Props = {
item: NavItem; item: NavItem;
onClick?: (e: React.MouseEvent) => void;
isCollapsed?: boolean; isCollapsed?: boolean;
px?: string | number; isDisabled?: boolean;
className?: string;
onClick?: () => void;
disableActiveState?: boolean;
} }
const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState }: Props) => { const NavLink = ({ item, onClick, isCollapsed, isDisabled }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false;
const isInternalLink = isInternalItem(item); const isInternalLink = isInternalItem(item);
const href = isInternalLink ? route(item.nextRoute) : item.url;
const isExpanded = isCollapsed === false;
const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive && !disableActiveState }); const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive });
const isXLScreen = useBreakpointValue({ base: false, xl: true }); const isXLScreen = useBreakpointValue({ base: false, xl: true });
const href = isInternalLink ? route(item.nextRoute) : item.url;
const isHighlighted = checkRouteHighlight(item); const isHighlighted = checkRouteHighlight(item);
...@@ -46,13 +45,13 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -46,13 +45,13 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
w={{ base: '100%', lg: isExpanded ? '100%' : '60px', xl: isCollapsed ? '60px' : '100%' }} w={{ base: '100%', lg: isExpanded ? '100%' : '60px', xl: isCollapsed ? '60px' : '100%' }}
display="flex" display="flex"
position="relative" position="relative"
px={ px || { base: 2, lg: isExpanded ? 2 : '15px', xl: isCollapsed ? '15px' : 2 } } px={{ base: 2, lg: isExpanded ? 2 : '15px', xl: isCollapsed ? '15px' : 2 }}
aria-label={ `${ item.text } link` } aria-label={ `${ item.text } link` }
whiteSpace="nowrap" whiteSpace="nowrap"
onClick={ onClick } onClick={ onClick }
_hover={{ _hover={{
[`& *:not(.${ LIGHTNING_LABEL_CLASS_NAME }, .${ LIGHTNING_LABEL_CLASS_NAME } *)`]: { [`& *:not(.${ LIGHTNING_LABEL_CLASS_NAME }, .${ LIGHTNING_LABEL_CLASS_NAME } *)`]: {
color: 'link_hovered', color: isDisabled ? 'inherit' : 'link_hovered',
}, },
}} }}
> >
...@@ -81,7 +80,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -81,7 +80,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
); );
return ( return (
<Box as="li" listStyleType="none" w="100%" className={ className }> <Box as="li" listStyleType="none" w="100%">
{ isInternalLink ? ( { isInternalLink ? (
<NextLink href={ item.nextRoute } passHref legacyBehavior> <NextLink href={ item.nextRoute } passHref legacyBehavior>
{ content } { content }
...@@ -91,16 +90,4 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -91,16 +90,4 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
); );
}; };
const NavLinkChakra = chakra(NavLink, { export default React.memo(NavLink);
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
if (isChakraProp && prop !== 'px') {
return false;
}
return true;
},
});
export default React.memo(NavLinkChakra);
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import type { Route } from 'nextjs-routes';
import config from 'configs/app';
import { useRewardsContext } from 'lib/contexts/rewards';
import NavLink from './NavLink';
type Props = {
isCollapsed?: boolean;
onClick?: () => void;
}
const NavLinkRewards = ({ isCollapsed, onClick }: Props) => {
const router = useRouter();
const { openLoginModal, dailyRewardQuery, apiToken, isInitialized } = useRewardsContext();
const pathname = '/account/rewards';
const nextRoute = { pathname } as Route;
const handleClick = useCallback((e: React.MouseEvent) => {
if (isInitialized && !apiToken) {
e.preventDefault();
openLoginModal();
}
onClick?.();
}, [ onClick, isInitialized, apiToken, openLoginModal ]);
if (!config.features.rewards.isEnabled) {
return null;
}
return (
<NavLink
item={{
text: 'Merits',
icon: dailyRewardQuery.data?.available ? 'merits_with_dot' : 'merits',
nextRoute: nextRoute,
isActive: router.pathname === pathname,
}}
onClick={ handleClick }
isCollapsed={ isCollapsed }
isDisabled={ !isInitialized }
/>
);
};
export default React.memo(NavLinkRewards);
...@@ -24,6 +24,7 @@ const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; ...@@ -24,6 +24,7 @@ const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
test.beforeEach(async({ mockEnvs, mockConfigResponse }) => { test.beforeEach(async({ mockEnvs, mockConfigResponse }) => {
await mockEnvs([ await mockEnvs([
...ENVS_MAP.rewardsService,
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ], [ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
]); ]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS_MOCK); await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS_MOCK);
......
...@@ -14,6 +14,7 @@ import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu'; ...@@ -14,6 +14,7 @@ import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import TestnetBadge from '../TestnetBadge'; import TestnetBadge from '../TestnetBadge';
import NavLink from './NavLink'; import NavLink from './NavLink';
import NavLinkGroup from './NavLinkGroup'; import NavLinkGroup from './NavLinkGroup';
import NavLinkRewards from './NavLinkRewards';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const appProps = useAppContext(); const appProps = useAppContext();
...@@ -100,6 +101,7 @@ const NavigationDesktop = () => { ...@@ -100,6 +101,7 @@ const NavigationDesktop = () => {
{ isAuth && ( { isAuth && (
<Box as="nav" borderTopWidth="1px" borderColor="divider" w="100%" mt={ 3 } pt={ 3 }> <Box as="nav" borderTopWidth="1px" borderColor="divider" w="100%" mt={ 3 } pt={ 3 }>
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
<NavLinkRewards isCollapsed={ isCollapsed }/>
{ accountNavItems.map((item) => <NavLink key={ item.text } item={ item } isCollapsed={ isCollapsed }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } item={ item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
......
import type { ButtonProps } from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react';
import { Button, Skeleton, Tooltip, Box, HStack } from '@chakra-ui/react'; import { Button, Tooltip, Box, HStack } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React, { useCallback, useState } from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
...@@ -22,8 +22,8 @@ interface Props { ...@@ -22,8 +22,8 @@ interface Props {
isPending?: boolean; isPending?: boolean;
} }
const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }: Props, ref: React.ForwardedRef<HTMLDivElement>) => { const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const [ isFetched, setIsFetched ] = React.useState(false); const [ isFetched, setIsFetched ] = useState(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { data, isLoading } = profileQuery; const { data, isLoading } = profileQuery;
...@@ -36,6 +36,10 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }: ...@@ -36,6 +36,10 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }:
} }
}, [ isLoading ]); }, [ isLoading ]);
const handleFocus = useCallback((e: React.FocusEvent<HTMLButtonElement>) => {
e.preventDefault();
}, []);
const content = (() => { const content = (() => {
if (web3AccountWithDomain.address) { if (web3AccountWithDomain.address) {
return ( return (
...@@ -60,31 +64,34 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }: ...@@ -60,31 +64,34 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }:
); );
})(); })();
const isButtonLoading = isPending || !isFetched;
const dataExists = !isButtonLoading && (Boolean(data) || Boolean(web3AccountWithDomain.address));
return ( return (
<Tooltip <Tooltip
label={ <span>Sign in to My Account to add tags,<br/>create watchlists, access API keys and more</span> } label={ <span>Sign in to My Account to add tags,<br/>create watchlists, access API keys and more</span> }
textAlign="center" textAlign="center"
padding={ 2 } padding={ 2 }
isDisabled={ isMobile || isFetched || Boolean(data) } isDisabled={ isMobile || isLoading || Boolean(data) }
openDelay={ 500 } openDelay={ 500 }
> >
<Skeleton isLoaded={ isFetched } borderRadius="base" ref={ ref } w="fit-content"> <Button
<Button ref={ ref }
size={ size } size={ size }
variant={ variant } variant={ variant }
onClick={ onClick } onClick={ onClick }
data-selected={ Boolean(data) || Boolean(web3AccountWithDomain.address) } onFocus={ handleFocus }
data-warning={ isAutoConnectDisabled } data-selected={ dataExists }
fontSize="sm" data-warning={ isAutoConnectDisabled }
lineHeight={ 5 } fontSize="sm"
px={ data || web3AccountWithDomain.address ? 2.5 : 4 } lineHeight={ 5 }
fontWeight={ data || web3AccountWithDomain.address ? 700 : 600 } px={ dataExists ? 2.5 : 4 }
isLoading={ isPending } fontWeight={ dataExists ? 700 : 600 }
loadingText={ isMobile ? undefined : 'Connecting' } isLoading={ isButtonLoading }
> loadingText={ isMobile ? undefined : 'Log in' }
{ content } >
</Button> { content }
</Skeleton> </Button>
</Tooltip> </Tooltip>
); );
}; };
......
...@@ -92,14 +92,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -92,14 +92,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Flex p={ 2 } borderColor="divider" borderBottomWidth="1px"> <Flex p={ 2 } borderColor="divider" borderBottomWidth="1px">
<Box>Address</Box> <Box>Address</Box>
<Hint <Hint
label="This wallet address is linked to your Blockscout account. It can be used to login and is used for merit program participation" label={ `This wallet address is linked to your Blockscout account. It can be used to login ${ config.features.rewards.isEnabled ? 'and is used for Merits Program participation' : '' }` } // eslint-disable-line max-len
boxSize={ 4 } boxSize={ 4 }
ml={ 1 } ml={ 1 }
mr="auto" mr="auto"
/> />
{ data?.address_hash ? { data?.address_hash ?
<Box>{ shortenString(data?.address_hash) }</Box> : <Box>{ shortenString(data?.address_hash) }</Box> :
<Link onClick={ onAddAddress } color="icon_info" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add address</Link> <Link onClick={ onAddAddress } _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add address</Link>
} }
</Flex> </Flex>
) } ) }
...@@ -107,7 +107,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -107,7 +107,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Box mr="auto">Email</Box> <Box mr="auto">Email</Box>
{ data?.email ? { data?.email ?
<TruncatedValue value={ data.email }/> : <TruncatedValue value={ data.email }/> :
<Link onClick={ onAddEmail } color="icon_info" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link> <Link onClick={ onAddEmail } _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link>
} }
</Flex> </Flex>
</Box> </Box>
......
...@@ -41,7 +41,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -41,7 +41,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
isTooltipDisabled isTooltipDisabled
truncation="dynamic" truncation="dynamic"
fontSize="sm" fontSize="sm"
fontWeight={ 700 } fontWeight={ 500 }
/> />
<IconButton <IconButton
aria-label="Open wallet" aria-label="Open wallet"
......
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