@@ -6,8 +6,7 @@ NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
|
||||
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=xxx
|
||||
|
||||
## DEPRECATED
|
||||
NEXT_PUBLIC_SENTRY_DSN=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
|
||||
|
||||
@@ -10,12 +10,9 @@ const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL')
|
||||
const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
|
||||
const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
|
||||
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
|
||||
const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
|
||||
const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP');
|
||||
const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL');
|
||||
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
|
||||
const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY');
|
||||
const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID');
|
||||
const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL');
|
||||
|
||||
const title = 'Marketplace';
|
||||
@@ -27,10 +24,8 @@ const config: Feature<(
|
||||
submitFormUrl: string;
|
||||
categoriesUrl: string | undefined;
|
||||
suggestIdeasFormUrl: string | undefined;
|
||||
securityReportsUrl: string | undefined;
|
||||
featuredApp: string | undefined;
|
||||
banner: { contentUrl: string; linkUrl: string } | undefined;
|
||||
rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
|
||||
graphLinksUrl: string | undefined;
|
||||
}> = (() => {
|
||||
if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
|
||||
@@ -38,16 +33,11 @@ const config: Feature<(
|
||||
submitFormUrl,
|
||||
categoriesUrl,
|
||||
suggestIdeasFormUrl,
|
||||
securityReportsUrl,
|
||||
featuredApp,
|
||||
banner: bannerContentUrl && bannerLinkUrl ? {
|
||||
contentUrl: bannerContentUrl,
|
||||
linkUrl: bannerLinkUrl,
|
||||
} : undefined,
|
||||
rating: ratingAirtableApiKey && ratingAirtableBaseId ? {
|
||||
airtableApiKey: ratingAirtableApiKey,
|
||||
airtableBaseId: ratingAirtableBaseId,
|
||||
} : undefined,
|
||||
graphLinksUrl,
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Meri
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://arbitrum.blockscout.com/apps/revokescout?chainId=42161
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -64,4 +63,4 @@ NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker']
|
||||
|
||||
@@ -36,8 +36,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://base.blockscout.com/apps/swapscout
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -71,4 +69,4 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'Rarible','collection_url':'https://rarible.com/collection/base/{hash}/items','instance_url':'https://rarible.com/token/base/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'OpenSea','collection_url':'https://opensea.io/assets/base/{hash}','instance_url':'https://opensea.io/assets/base/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'}, {'name':'MagicEden','collection_url':'https://magiceden.io/collections/base/{hash}','instance_url':'https://magiceden.io/item-details/base/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}]
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
|
||||
@@ -39,8 +39,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://eth.blockscout.com/apps/revokescout?chainId=1
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
|
||||
@@ -38,8 +38,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://revoke.blockscout.com?utm_source=blockscout&utm_medium=eth-sepolia,
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -72,4 +70,4 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
|
||||
@@ -41,8 +41,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://gnosis.blockscout.com/apps/revok
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/marketplace-subgraph-links/subgraph-links.json
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -73,4 +71,4 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
|
||||
@@ -35,8 +35,6 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/test-configs/marketplace-security-report-mock.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
|
||||
@@ -72,4 +70,4 @@ NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=nouns
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer-test.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
|
||||
@@ -38,8 +38,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://optimism.blockscout.com/apps/swapscout
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -76,4 +74,4 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker']
|
||||
|
||||
@@ -35,7 +35,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://polygon.blockscout.com/apps/swapscout
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -60,4 +59,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://polygon.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
|
||||
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
|
||||
|
||||
@@ -39,7 +39,6 @@ NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=
|
||||
NEXT_PUBLIC_APP_ENV=testing
|
||||
NEXT_PUBLIC_APP_INSTANCE=pw
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://localhost:3000/marketplace-suggest-ideas-form
|
||||
NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
|
||||
@@ -60,4 +59,4 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom
|
||||
NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false
|
||||
NEXT_PUBLIC_GAS_TRACKER_UNITS=['usd','gwei']
|
||||
NEXT_PUBLIC_GAS_TRACKER_UNITS=['usd','gwei']
|
||||
|
||||
@@ -36,8 +36,6 @@ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0x
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.shibariumscan.io/apps/revokescout?chainId=109
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -65,4 +63,4 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['miner']
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
|
||||
@@ -34,7 +34,6 @@ NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Meri
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://explorer.zora.energy/apps/revokescout?chainId=7777777
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
@@ -62,4 +61,4 @@ NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://explorer.zora.energy
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
|
||||
@@ -16,7 +16,6 @@ ASSETS_DIR="$1"
|
||||
ASSETS_ENVS=(
|
||||
"NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
|
||||
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
|
||||
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
|
||||
"NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL"
|
||||
"NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL"
|
||||
"NEXT_PUBLIC_FEATURED_NETWORKS"
|
||||
|
||||
@@ -39,7 +39,6 @@ async function validateEnvs(appEnvs: Record<string, string>) {
|
||||
'NEXT_PUBLIC_FEATURED_NETWORKS',
|
||||
'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL',
|
||||
'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL',
|
||||
'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL',
|
||||
'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL',
|
||||
'NEXT_PUBLIC_FOOTER_LINKS',
|
||||
'NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL',
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
|
||||
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
|
||||
import { GAS_UNITS } from '../../../types/client/gasTracker';
|
||||
import type { GasUnit } from '../../../types/client/gasTracker';
|
||||
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
|
||||
import type { MarketplaceAppBase, MarketplaceAppSocialInfo } from '../../../types/client/marketplace';
|
||||
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
|
||||
import type { ApiDocsTabId } from '../../../types/views/apiDocs';
|
||||
import { API_DOCS_TABS } from '../../../types/views/apiDocs';
|
||||
@@ -80,7 +80,7 @@ const getYupValidationErrorMessage = (error: unknown) =>
|
||||
error.errors.join(', ') :
|
||||
'';
|
||||
|
||||
const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = yup
|
||||
const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppBase & MarketplaceAppSocialInfo> = yup
|
||||
.object({
|
||||
id: yup.string().required(),
|
||||
external: yup.boolean(),
|
||||
@@ -105,65 +105,6 @@ const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = yup
|
||||
priority: yup.number(),
|
||||
});
|
||||
|
||||
const issueSeverityDistributionSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['overallInfo']['issueSeverityDistribution']> = yup
|
||||
.object({
|
||||
critical: yup.number().required(),
|
||||
gas: yup.number().required(),
|
||||
high: yup.number().required(),
|
||||
informational: yup.number().required(),
|
||||
low: yup.number().required(),
|
||||
medium: yup.number().required(),
|
||||
});
|
||||
|
||||
const solidityscanReportSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['contractsData'][number]['solidityScanReport']> = yup
|
||||
.object({
|
||||
contractname: yup.string().required(),
|
||||
scan_status: yup.string().required(),
|
||||
scan_summary: yup
|
||||
.object({
|
||||
issue_severity_distribution: issueSeverityDistributionSchema.required(),
|
||||
lines_analyzed_count: yup.number().required(),
|
||||
scan_time_taken: yup.number().required(),
|
||||
score: yup.string().required(),
|
||||
score_v2: yup.string().required(),
|
||||
threat_score: yup.string().required(),
|
||||
})
|
||||
.required(),
|
||||
scanner_reference_url: yup.string().test(urlTest).required(),
|
||||
});
|
||||
|
||||
const contractDataSchema: yup.ObjectSchema<MarketplaceAppSecurityReport['contractsData'][number]> = yup
|
||||
.object({
|
||||
address: yup.string().required(),
|
||||
isVerified: yup.boolean().required(),
|
||||
solidityScanReport: solidityscanReportSchema.nullable().notRequired(),
|
||||
});
|
||||
|
||||
const chainsDataSchema = yup.lazy((objValue) => {
|
||||
let schema = yup.object();
|
||||
Object.keys(objValue).forEach((key) => {
|
||||
schema = schema.shape({
|
||||
[key]: yup.object({
|
||||
overallInfo: yup.object({
|
||||
verifiedNumber: yup.number().required(),
|
||||
totalContractsNumber: yup.number().required(),
|
||||
solidityScanContractsNumber: yup.number().required(),
|
||||
securityScore: yup.number().required(),
|
||||
issueSeverityDistribution: issueSeverityDistributionSchema.required(),
|
||||
}).required(),
|
||||
contractsData: yup.array().of(contractDataSchema).required(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
return schema;
|
||||
});
|
||||
|
||||
const securityReportSchema: yup.ObjectSchema<MarketplaceAppSecurityReportRaw> = yup
|
||||
.object({
|
||||
appName: yup.string().required(),
|
||||
chainsData: chainsDataSchema,
|
||||
});
|
||||
|
||||
const marketplaceSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
@@ -204,16 +145,6 @@ const marketplaceSchema = yup
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(securityReportSchema)
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
@@ -238,22 +169,6 @@ const marketplaceSchema = yup
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema,
|
||||
// eslint-disable-next-line max-len
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
|
||||
}),
|
||||
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
|
||||
|
||||
@@ -3,10 +3,7 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com
|
||||
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com
|
||||
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=test
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=test
|
||||
|
||||
@@ -14,3 +14,6 @@
|
||||
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS |
|
||||
| NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.0+ | v2.2.0 | Removed; configuration done on the API side |
|
||||
| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array<LinkId>` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | v1.16.0+ | v2.3.0 | Use NEXT_PUBLIC_API_DOCS_TABS instead to hide tabs on the API docs page. |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key for dapp ratings | - | - | - | v1.33.0+ | v2.3.0 | Removed; now the ratings come from the API |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ | v2.3.0 | Removed; now the ratings come from the API |
|
||||
| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | v1.28.0+ | v2.3.0 | Removed; we don't support this feature anymore |
|
||||
|
||||
@@ -569,12 +569,9 @@ Ads are enabled by default on all self-hosted instances. If you would like to di
|
||||
| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | v1.24.0+ |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | v1.23.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | v1.28.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ |
|
||||
| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ |
|
||||
|
||||
#### Marketplace app configuration properties
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 15 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.502 11.242a2.491 2.491 0 0 0-1.802.77L4.919 8.758c.112-.42.112-.863 0-1.284L10.7 4.222a2.492 2.492 0 1 0-.614-1.088L4.303 6.386a2.5 2.5 0 1 0 0 3.461l5.781 3.253a2.5 2.5 0 1 0 2.417-1.858Z" fill="currentColor"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20" fill="none">
|
||||
<path d="M15.521 13.459a2.76 2.76 0 0 0-1.996.853l-6.407-3.605a2.764 2.764 0 0 0 0-1.423l6.407-3.605a2.762 2.762 0 1 0-.681-1.206L6.437 8.078a2.77 2.77 0 1 0 0 3.835l6.407 3.605a2.77 2.77 0 1 0 2.678-2.06Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 311 B |
@@ -1,6 +1,6 @@
|
||||
import type { ApiResource } from '../types';
|
||||
import type { TokenInfoApplicationConfig, TokenInfoApplications } from 'types/api/account';
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
export const ADMIN_API_RESOURCES = {
|
||||
public_tag_application: {
|
||||
@@ -23,6 +23,10 @@ export const ADMIN_API_RESOURCES = {
|
||||
path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId',
|
||||
pathParams: [ 'chainId' as const, 'dappId' as const ],
|
||||
},
|
||||
marketplace_rate_dapp: {
|
||||
path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId/ratings',
|
||||
pathParams: [ 'chainId' as const, 'dappId' as const ],
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type AdminApiResourceName = `admin:${ keyof typeof ADMIN_API_RESOURCES }`;
|
||||
@@ -31,7 +35,7 @@ export type AdminApiResourceName = `admin:${ keyof typeof ADMIN_API_RESOURCES }`
|
||||
export type AdminApiResourcePayload<R extends AdminApiResourceName> =
|
||||
R extends 'admin:token_info_applications_config' ? TokenInfoApplicationConfig :
|
||||
R extends 'admin:token_info_applications' ? TokenInfoApplications :
|
||||
R extends 'admin:marketplace_dapps' ? Array<MarketplaceAppOverview> :
|
||||
R extends 'admin:marketplace_dapp' ? MarketplaceAppOverview :
|
||||
R extends 'admin:marketplace_dapps' ? Array<MarketplaceApp> :
|
||||
R extends 'admin:marketplace_dapp' ? MarketplaceApp :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
@@ -125,12 +125,9 @@ Type extends EventTypes.PAGE_WIDGET ? (
|
||||
{
|
||||
Type: 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
|
||||
} | {
|
||||
Type: 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts';
|
||||
Type: 'Favorite app' | 'More button';
|
||||
Info: string;
|
||||
Source: 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner';
|
||||
} | {
|
||||
Type: 'Security score';
|
||||
Source: 'Analyzed contracts popup';
|
||||
Source: 'Discovery view' | 'App modal' | 'App page' | 'Banner';
|
||||
} | {
|
||||
Type: 'Action button';
|
||||
Info: string;
|
||||
|
||||
@@ -14,6 +14,8 @@ export const apps = [
|
||||
github: [ 'https://github.com/hop-protocol/hop', 'https://github.com/hop-protocol/hop-ui' ],
|
||||
discord: 'https://discord.gg/hopprotocol',
|
||||
twitter: 'https://twitter.com/HopProtocol',
|
||||
rating: 4.3,
|
||||
ratingsTotalCount: 15,
|
||||
},
|
||||
{
|
||||
author: 'Blockscout',
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { apps } from './apps';
|
||||
|
||||
export const ratings = {
|
||||
records: [
|
||||
{
|
||||
fields: {
|
||||
appId: apps[0].id,
|
||||
rating: 4.3,
|
||||
count: 15,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { apps } from './apps';
|
||||
|
||||
export const securityReports = [
|
||||
{
|
||||
appName: apps[0].id,
|
||||
doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet',
|
||||
chainsData: {
|
||||
'1': {
|
||||
overallInfo: {
|
||||
verifiedNumber: 1,
|
||||
totalContractsNumber: 1,
|
||||
solidityScanContractsNumber: 1,
|
||||
securityScore: 87.5,
|
||||
issueSeverityDistribution: {
|
||||
critical: 4,
|
||||
gas: 1,
|
||||
high: 0,
|
||||
informational: 4,
|
||||
low: 2,
|
||||
medium: 0,
|
||||
},
|
||||
},
|
||||
contractsData: [
|
||||
{
|
||||
address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
|
||||
isVerified: true,
|
||||
solidityScanReport: {
|
||||
connection_id: '',
|
||||
contract_address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
|
||||
contract_chain: 'optimism',
|
||||
contract_platform: 'blockscout',
|
||||
contract_url: 'http://optimism.blockscout.com/address/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE',
|
||||
contractname: 'LiFiDiamond',
|
||||
is_quick_scan: true,
|
||||
node_reference_id: null,
|
||||
request_type: 'threat_scan',
|
||||
scanner_reference_url: 'http://solidityscan.com/quickscan/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE/blockscout/eth?ref=blockscout',
|
||||
scan_status: 'scan_done',
|
||||
scan_summary: {
|
||||
issue_severity_distribution: {
|
||||
critical: 0,
|
||||
gas: 1,
|
||||
high: 0,
|
||||
informational: 4,
|
||||
low: 2,
|
||||
medium: 0,
|
||||
},
|
||||
lines_analyzed_count: 72,
|
||||
scan_time_taken: 1,
|
||||
score: '4.38',
|
||||
score_v2: '87.50',
|
||||
threat_score: '100.00',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -12,7 +12,6 @@ export function marketplace(): CspDev.DirectiveDescriptor {
|
||||
return {
|
||||
'connect-src': [
|
||||
'api' in feature ? feature.api.endpoint : '',
|
||||
feature.rating ? 'https://api.airtable.com' : '',
|
||||
],
|
||||
|
||||
'frame-src': [
|
||||
|
||||
@@ -4,7 +4,7 @@ import fetch from 'node-fetch';
|
||||
import React from 'react';
|
||||
|
||||
import type { NextPageWithLayout } from 'nextjs/types';
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import * as gSSP from 'nextjs/getServerSideProps';
|
||||
@@ -17,7 +17,7 @@ import config from 'configs/app';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import LayoutApp from 'ui/shared/layout/LayoutApp';
|
||||
|
||||
const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false });
|
||||
const MarketplaceAppPage = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false });
|
||||
|
||||
const pathname: Route['pathname'] = '/apps/[id]';
|
||||
const feature = config.features.marketplace;
|
||||
@@ -25,7 +25,7 @@ const feature = config.features.marketplace;
|
||||
const Page: NextPageWithLayout<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
|
||||
return (
|
||||
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
|
||||
<MarketplaceApp/>
|
||||
<MarketplaceAppPage/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
|
||||
|
||||
try {
|
||||
const response = await fetch(feature.configUrl, { signal: controller.signal });
|
||||
const appList = await response.json() as Array<MarketplaceAppOverview>;
|
||||
const appList = await response.json() as Array<MarketplaceApp>;
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (appList && Array.isArray(appList)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable max-len */
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
export const MARKETPLACE_APP: MarketplaceAppOverview = {
|
||||
export const MARKETPLACE_APP: MarketplaceApp = {
|
||||
author: 'StubApp Inc.',
|
||||
id: 'stub-app',
|
||||
title: 'My cool app name',
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
||||
|
||||
return (
|
||||
<RatingGroup.Item key={ index } index={ index + 1 }>
|
||||
<RatingGroup.ItemIndicator icon={ icon }/>
|
||||
<RatingGroup.ItemIndicator icon={ icon } cursor={ readOnly ? 'default' : 'pointer' }/>
|
||||
</RatingGroup.Item>
|
||||
);
|
||||
}) }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { SolidityScanReport, SolidityScanReportSeverityDistribution } from 'lib/solidityScan/schema';
|
||||
|
||||
export type MarketplaceAppPreview = {
|
||||
export type MarketplaceAppBase = {
|
||||
id: string;
|
||||
author: string;
|
||||
site?: string;
|
||||
external?: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
logoDarkMode?: string;
|
||||
shortDescription: string;
|
||||
@@ -13,6 +14,12 @@ export type MarketplaceAppPreview = {
|
||||
priority?: number;
|
||||
};
|
||||
|
||||
export type MarketplaceAppRating = {
|
||||
rating?: number;
|
||||
ratingsTotalCount?: number;
|
||||
userRating?: number;
|
||||
};
|
||||
|
||||
export type MarketplaceAppSocialInfo = {
|
||||
twitter?: string;
|
||||
telegram?: string;
|
||||
@@ -20,53 +27,9 @@ export type MarketplaceAppSocialInfo = {
|
||||
discord?: string;
|
||||
};
|
||||
|
||||
export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocialInfo & {
|
||||
author: string;
|
||||
description: string;
|
||||
site?: string;
|
||||
};
|
||||
|
||||
export type AppRating = {
|
||||
recordId: string;
|
||||
value: number | undefined;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & {
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
rating?: AppRating;
|
||||
};
|
||||
export type MarketplaceApp = MarketplaceAppBase & MarketplaceAppSocialInfo & MarketplaceAppRating;
|
||||
|
||||
export enum MarketplaceCategory {
|
||||
ALL = 'All',
|
||||
FAVORITES = 'Favorites',
|
||||
}
|
||||
|
||||
export enum ContractListTypes {
|
||||
ANALYZED = 'Analyzed',
|
||||
ALL = 'All',
|
||||
VERIFIED = 'Verified',
|
||||
}
|
||||
|
||||
export type MarketplaceAppSecurityReport = {
|
||||
overallInfo: {
|
||||
verifiedNumber: number;
|
||||
totalContractsNumber: number;
|
||||
solidityScanContractsNumber: number;
|
||||
securityScore: number;
|
||||
totalIssues?: number;
|
||||
issueSeverityDistribution: SolidityScanReportSeverityDistribution;
|
||||
};
|
||||
contractsData: Array<{
|
||||
address: string;
|
||||
isVerified: boolean;
|
||||
solidityScanReport?: SolidityScanReport['scan_report'] | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type MarketplaceAppSecurityReportRaw = {
|
||||
appName: string;
|
||||
chainsData: {
|
||||
[chainId: string]: MarketplaceAppSecurityReport;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import type { BoxProps, ButtonProps } from '@chakra-ui/react';
|
||||
import { Box, Text, Flex, Separator, Icon } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
// 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 solidityScanIcon from 'icons/brands/solidity_scan.svg';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import type { PopoverContentProps } from 'toolkit/chakra/popover';
|
||||
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import { apos } from 'toolkit/utils/htmlEntities';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
|
||||
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
|
||||
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
isLoading?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
source: 'Discovery view' | 'App modal' | 'App page';
|
||||
popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left';
|
||||
buttonProps?: ButtonProps;
|
||||
triggerWrapperProps?: BoxProps;
|
||||
popoverContentProps?: PopoverContentProps;
|
||||
};
|
||||
|
||||
const AppSecurityReport = ({
|
||||
id,
|
||||
securityReport,
|
||||
showContractList,
|
||||
isLoading,
|
||||
onlyIcon,
|
||||
source,
|
||||
triggerWrapperProps,
|
||||
buttonProps,
|
||||
popoverPlacement = 'bottom-start',
|
||||
popoverContentProps,
|
||||
}: Props) => {
|
||||
|
||||
const { open, onOpenChange } = useDisclosure();
|
||||
|
||||
const handleButtonClick = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Info: id, Source: source });
|
||||
}, [ id, source ]);
|
||||
|
||||
const showAnalyzedContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' });
|
||||
showContractList(id, ContractListTypes.ANALYZED);
|
||||
onOpenChange({ open: false });
|
||||
}, [ showContractList, id, onOpenChange ]);
|
||||
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security score popup' });
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
onOpenChange({ open: false });
|
||||
}, [ showContractList, id, onOpenChange ]);
|
||||
|
||||
const {
|
||||
securityScore = 0,
|
||||
solidityScanContractsNumber = 0,
|
||||
issueSeverityDistribution = {} as MarketplaceAppSecurityReport['overallInfo']['issueSeverityDistribution'],
|
||||
totalIssues = 0,
|
||||
} = securityReport?.overallInfo || {};
|
||||
|
||||
if ((!securityReport || !securityScore) && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverRoot open={ open } onOpenChange={ onOpenChange } positioning={{ placement: popoverPlacement }}>
|
||||
<SolidityscanReportButton
|
||||
score={ securityScore }
|
||||
isLoading={ isLoading }
|
||||
onClick={ handleButtonClick }
|
||||
onlyIcon={ onlyIcon }
|
||||
label={ <>The security score is based on analysis<br/>of a DApp{ apos }s smart contracts.</> }
|
||||
wrapperProps={ triggerWrapperProps }
|
||||
{ ...buttonProps }
|
||||
/>
|
||||
<PopoverContent w={{ base: 'calc(100vw - 48px)', lg: '328px' }} mx={{ base: 3, lg: 0 }} overflowY="auto" { ...popoverContentProps }>
|
||||
<PopoverBody px="26px" py="20px" textStyle="sm">
|
||||
<Text fontWeight="500" textStyle="xs" mb={ 2 } color="text.secondary">Smart contracts info</Text>
|
||||
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
|
||||
<Flex alignItems="center">
|
||||
<IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500" mr={ 1 }/>
|
||||
<Text>Verified contracts</Text>
|
||||
</Flex>
|
||||
<Link textStyle="sm" fontWeight="500" onClick={ showAllContracts }>
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</Link>
|
||||
</Flex>
|
||||
<Separator my={ 3 }/>
|
||||
<Box mb={ 5 }>
|
||||
{ solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine
|
||||
this protocol{ apos }s overall security score on the { config.chain.name } network by { ' ' }
|
||||
<Box>
|
||||
<Icon as={ solidityScanIcon } mr={ 1 } w="23px" h="20px" display="inline-block" verticalAlign="middle"/>
|
||||
<Text fontWeight={ 600 } display="inline-block">SolidityScan</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SolidityscanReportScore score={ securityScore } mb={ 5 }/>
|
||||
{ issueSeverityDistribution && totalIssues > 0 && (
|
||||
<Box mb={ 5 }>
|
||||
<Text py="7px" color="text.secondary" textStyle="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
|
||||
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
|
||||
</Box>
|
||||
) }
|
||||
<Link onClick={ showAnalyzedContracts } display="inline-flex" alignItems="center">
|
||||
Analyzed contracts
|
||||
</Link>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSecurityReport;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
@@ -12,7 +12,7 @@ import IframeBanner from './Banner/IframeBanner';
|
||||
const feature = config.features.marketplace;
|
||||
|
||||
type BannerProps = {
|
||||
apps: Array<MarketplaceAppPreview> | undefined;
|
||||
apps: Array<MarketplaceApp> | undefined;
|
||||
favoriteApps: Array<string>;
|
||||
isLoading: boolean;
|
||||
onInfoClick: (id: string) => void;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex, Text } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -19,7 +19,7 @@ import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
import FeaturedAppMobile from './FeaturedAppMobile';
|
||||
|
||||
type FeaturedAppProps = {
|
||||
app: MarketplaceAppPreview;
|
||||
app: MarketplaceApp;
|
||||
isFavorite: boolean;
|
||||
isLoading: boolean;
|
||||
onInfoClick: (id: string) => void;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex, Text } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
@@ -14,7 +14,7 @@ import FavoriteIcon from '../FavoriteIcon';
|
||||
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
|
||||
interface Props extends MarketplaceAppPreview {
|
||||
interface Props extends MarketplaceApp {
|
||||
onInfoClick: (event: MouseEvent) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: () => void;
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
|
||||
import { apos } from 'toolkit/utils/htmlEntities';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
import ContractSecurityReport from './ContractSecurityReport';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onBack?: () => void;
|
||||
type: ContractListTypes;
|
||||
contracts?: MarketplaceAppSecurityReport['contractsData'];
|
||||
};
|
||||
|
||||
const titles = {
|
||||
[ContractListTypes.ALL]: `All app${ apos }s smart contracts`,
|
||||
[ContractListTypes.ANALYZED]: 'Analyzed contracts',
|
||||
[ContractListTypes.VERIFIED]: 'Verified contracts',
|
||||
};
|
||||
|
||||
const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
|
||||
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}, [ onClose ]);
|
||||
|
||||
const displayedContracts = React.useMemo(() => {
|
||||
if (!contracts) {
|
||||
return [];
|
||||
}
|
||||
switch (type) {
|
||||
default:
|
||||
case ContractListTypes.ALL:
|
||||
return contracts.sort((a) => a.isVerified ? -1 : 1);
|
||||
case ContractListTypes.ANALYZED:
|
||||
return contracts
|
||||
.filter((contract) => Boolean(contract.solidityScanReport))
|
||||
.sort((a, b) =>
|
||||
(parseFloat(b.solidityScanReport?.scan_summary.score_v2 ?? '0')) - (parseFloat(a.solidityScanReport?.scan_summary.score_v2 ?? '0')),
|
||||
);
|
||||
case ContractListTypes.VERIFIED:
|
||||
return contracts.filter((contract) => contract.isVerified);
|
||||
}
|
||||
}, [ contracts, type ]);
|
||||
|
||||
if (!contracts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogRoot
|
||||
open={ Boolean(type) }
|
||||
onOpenChange={ handleOpenChange }
|
||||
size={{ lgDown: 'full', lg: 'md' }}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader display="flex" alignItems="center" mb={ 4 } onBackToClick={ onBack }>
|
||||
{ titles[type] }
|
||||
</DialogHeader>
|
||||
<DialogBody
|
||||
maxH={{ base: 'max-content', lg: '352px' }}
|
||||
overflow="scroll"
|
||||
mb={ 0 }
|
||||
display="grid"
|
||||
gridTemplateColumns="max-content 1fr"
|
||||
rowGap={ 2 }
|
||||
columnGap={ type === ContractListTypes.ANALYZED ? 4 : 0 }
|
||||
>
|
||||
{ displayedContracts.map((contract) => (
|
||||
<React.Fragment key={ contract.address }>
|
||||
{ type === ContractListTypes.ANALYZED && (
|
||||
<Box gridColumn={ 1 }>
|
||||
<ContractSecurityReport securityReport={ contract.solidityScanReport }/>
|
||||
</Box>
|
||||
) }
|
||||
<AddressEntity
|
||||
address={{
|
||||
hash: contract.address,
|
||||
name: contract.solidityScanReport?.contractname,
|
||||
is_contract: true,
|
||||
is_verified: contract.isVerified,
|
||||
}}
|
||||
noCopy
|
||||
gridColumn={ 2 }
|
||||
height="32px"
|
||||
/>
|
||||
</React.Fragment>
|
||||
)) }
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractListModal;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Box, Text, Icon } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
// 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 solidityScanIcon from 'icons/brands/solidity_scan.svg';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import type { SolidityScanReport } from 'lib/solidityScan/schema';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
|
||||
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
|
||||
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
|
||||
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
|
||||
|
||||
type Props = {
|
||||
securityReport?: SolidityScanReport['scan_report'] | null;
|
||||
};
|
||||
|
||||
const ContractSecurityReport = ({ securityReport }: Props) => {
|
||||
const handleClick = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Source: 'Analyzed contracts popup' });
|
||||
}, [ ]);
|
||||
|
||||
if (!securityReport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = securityReport.scanner_reference_url;
|
||||
const {
|
||||
score_v2: securityScore,
|
||||
issue_severity_distribution: issueSeverityDistribution,
|
||||
} = securityReport.scan_summary;
|
||||
|
||||
const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0);
|
||||
|
||||
return (
|
||||
<PopoverRoot>
|
||||
<SolidityscanReportButton
|
||||
score={ parseFloat(securityScore) }
|
||||
onClick={ handleClick }
|
||||
/>
|
||||
<PopoverContent zIndex="modal2" w={{ base: 'calc(100vw - 48px)', lg: '328px' }} overflowY="auto">
|
||||
<PopoverBody>
|
||||
<Box mb={ 5 }>
|
||||
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network by { ' ' }
|
||||
<Box>
|
||||
<Icon as={ solidityScanIcon } mr={ 1 } w="23px" h="20px" display="inline-block" verticalAlign="middle"/>
|
||||
<Text fontWeight={ 600 } display="inline-block">SolidityScan</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SolidityscanReportScore score={ parseFloat(securityScore) } mb={ 5 }/>
|
||||
{ issueSeverityDistribution && totalIssues > 0 && (
|
||||
<Box mb={ 5 }>
|
||||
<Text py="7px" color="text.secondary" textStyle="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
|
||||
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
|
||||
</Box>
|
||||
) }
|
||||
<Link external href={ url }>View full report</Link>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractSecurityReport;
|
||||
@@ -2,9 +2,8 @@ import { chakra, Flex, Text } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
@@ -13,27 +12,19 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { isBrowser } from 'toolkit/utils/isBrowser';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import FavoriteIcon from './FavoriteIcon';
|
||||
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
|
||||
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
|
||||
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
|
||||
import Rating from './Rating/Rating';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
interface Props extends MarketplaceAppWithSecurityReport {
|
||||
interface Props extends MarketplaceApp {
|
||||
onInfoClick: (id: string) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
isLoading: boolean;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
className?: string;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
userRating?: AppRating;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
graphLinks?: Array<{ title: string; url: string }>;
|
||||
}
|
||||
|
||||
@@ -52,18 +43,12 @@ const MarketplaceAppCard = ({
|
||||
isLoading,
|
||||
internalWallet,
|
||||
onAppClick,
|
||||
securityReport,
|
||||
className,
|
||||
showContractList,
|
||||
rating,
|
||||
ratingsTotalCount,
|
||||
userRating,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
graphLinks,
|
||||
}: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const categoriesLabel = categories.join(', ');
|
||||
|
||||
const handleInfoClick = useCallback((event: MouseEvent) => {
|
||||
@@ -173,69 +158,51 @@ const MarketplaceAppCard = ({
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginTop="auto"
|
||||
h="30px"
|
||||
>
|
||||
<Link
|
||||
textStyle="sm"
|
||||
fontWeight="500"
|
||||
paddingRight={{ md: 2 }}
|
||||
paddingRight={ 3 }
|
||||
h="full"
|
||||
href="#"
|
||||
onClick={ handleInfoClick }
|
||||
>
|
||||
More info
|
||||
Info
|
||||
</Link>
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
<Rating
|
||||
appId={ id }
|
||||
rating={ rating }
|
||||
ratingsTotalCount={ ratingsTotalCount }
|
||||
userRating={ userRating }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
isLoading={ isLoading }
|
||||
source="Discovery"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
onClick={ handleFavoriteClick }
|
||||
selected={ isFavorite }
|
||||
>
|
||||
<FavoriteIcon isFavorite={ isFavorite }/>
|
||||
</IconButton>
|
||||
<CopyToClipboard
|
||||
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
|
||||
type="share"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
borderRadius="none"
|
||||
ml={ 0 }
|
||||
boxSize={ 8 }
|
||||
/>
|
||||
<Flex gap={ 2 }>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
onClick={ handleFavoriteClick }
|
||||
selected={ isFavorite }
|
||||
>
|
||||
<FavoriteIcon isFavorite={ isFavorite }/>
|
||||
</IconButton>
|
||||
<CopyToClipboard
|
||||
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
|
||||
type="share"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
borderRadius="none"
|
||||
ml={ 0 }
|
||||
boxSize={ 8 }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) }
|
||||
|
||||
{ securityReport && (
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showContractList }
|
||||
isLoading={ isLoading }
|
||||
source="Discovery view"
|
||||
popoverPlacement={ isMobile ? 'bottom-end' : 'left' }
|
||||
triggerWrapperProps={{
|
||||
position: 'absolute',
|
||||
right: { base: 3, md: 5 },
|
||||
top: { base: '10px', md: 5 },
|
||||
}}
|
||||
buttonProps={{
|
||||
border: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
</LinkBox>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import InfoButton from 'ui/shared/InfoButton';
|
||||
|
||||
import Content from './MarketplaceAppInfo/Content';
|
||||
|
||||
interface Props {
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
data: MarketplaceApp | undefined;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Flex, Text, Grid } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import SocialLink from './SocialLink';
|
||||
import type { Props as SocialLinkProps } from './SocialLink';
|
||||
import WebsiteLink from './WebsiteLink';
|
||||
|
||||
interface Props {
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
data: MarketplaceApp | undefined;
|
||||
}
|
||||
|
||||
const SOCIAL_LINKS: Array<Omit<SocialLinkProps, 'href'>> = [
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import type { TestFnArgs } from 'playwright/lib';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
@@ -13,14 +10,7 @@ const props = {
|
||||
onClose: () => {},
|
||||
onFavoriteClick: () => {},
|
||||
showContractList: () => {},
|
||||
data: {
|
||||
...appsMock[0],
|
||||
securityReport: securityReportsMock[0].chainsData['1'],
|
||||
rating: {
|
||||
recordId: 'test',
|
||||
value: 4.3,
|
||||
},
|
||||
} as MarketplaceAppWithSecurityReport,
|
||||
data: appsMock[0],
|
||||
isFavorite: false,
|
||||
userRating: undefined,
|
||||
rateApp: () => {},
|
||||
@@ -29,11 +19,7 @@ const props = {
|
||||
canRate: undefined,
|
||||
};
|
||||
|
||||
const testFn = async({ render, page, mockAssetResponse, mockEnvs }: TestFnArgs) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
const testFn = async({ render, page, mockAssetResponse }: TestFnArgs) => {
|
||||
await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg');
|
||||
await render(<MarketplaceAppModal { ...props }/>);
|
||||
await page.getByText('Launch app').focus();
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { Badge } from 'toolkit/chakra/badge';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
@@ -24,27 +21,19 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import type { IconName } from 'ui/shared/IconSvg';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import FavoriteIcon from './FavoriteIcon';
|
||||
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
|
||||
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
|
||||
import Rating from './Rating/Rating';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const isRatingEnabled = feature.isEnabled && feature.rating;
|
||||
const isRatingEnabled = feature.isEnabled && 'api' in feature;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'App modal') => void;
|
||||
data: MarketplaceAppWithSecurityReport;
|
||||
showContractList: (id: string, type: ContractListTypes, hasPreviousStep: boolean) => void;
|
||||
userRating?: AppRating;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
data: MarketplaceApp;
|
||||
graphLinks?: Array<{ title: string; url: string }>;
|
||||
};
|
||||
|
||||
@@ -53,12 +42,6 @@ const MarketplaceAppModal = ({
|
||||
isFavorite,
|
||||
onFavoriteClick,
|
||||
data,
|
||||
showContractList: showContractListProp,
|
||||
userRating,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
graphLinks,
|
||||
}: Props) => {
|
||||
const {
|
||||
@@ -76,8 +59,9 @@ const MarketplaceAppModal = ({
|
||||
logo,
|
||||
logoDarkMode,
|
||||
categories,
|
||||
securityReport,
|
||||
rating,
|
||||
ratingsTotalCount,
|
||||
userRating,
|
||||
internalWallet,
|
||||
} = data;
|
||||
|
||||
@@ -114,20 +98,6 @@ const MarketplaceAppModal = ({
|
||||
onFavoriteClick(id, isFavorite, 'App modal');
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
const showContractList = useCallback((id: string, type: ContractListTypes) => {
|
||||
onClose();
|
||||
// FIXME: This is a workaround to avoid the dialog closing before the modal is opened
|
||||
window.setTimeout(() => {
|
||||
showContractListProp(id, type, true);
|
||||
}, 100);
|
||||
}, [ showContractListProp, onClose ]);
|
||||
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' });
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
|
||||
|
||||
return (
|
||||
@@ -189,12 +159,9 @@ const MarketplaceAppModal = ({
|
||||
<Rating
|
||||
appId={ id }
|
||||
rating={ rating }
|
||||
ratingsTotalCount={ ratingsTotalCount }
|
||||
userRating={ userRating }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
fullView
|
||||
canRate={ canRate }
|
||||
source="App modal"
|
||||
popoverContentProps={{ zIndex: 'modal' }}
|
||||
/>
|
||||
@@ -206,8 +173,8 @@ const MarketplaceAppModal = ({
|
||||
marginTop={{ base: 6, md: 3 }}
|
||||
>
|
||||
<Flex flexWrap="wrap" gap={ 6 }>
|
||||
<Flex width={{ base: '100%', md: 'auto' }}>
|
||||
<Link href={ external ? url : route({ pathname: '/apps/[id]', query: { id: data.id } }) } external={ external } noIcon mr={ 2 }>
|
||||
<Flex width={{ base: '100%', md: 'auto' }} gap={ 2 }>
|
||||
<Link href={ external ? url : route({ pathname: '/apps/[id]', query: { id: data.id } }) } external={ external } noIcon>
|
||||
<Button size="sm">
|
||||
Launch app
|
||||
</Button>
|
||||
@@ -229,7 +196,6 @@ const MarketplaceAppModal = ({
|
||||
type="share"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
borderRadius="none"
|
||||
ml={ 0 }
|
||||
boxSize={ 8 }
|
||||
/>
|
||||
@@ -239,37 +205,6 @@ const MarketplaceAppModal = ({
|
||||
</Box>
|
||||
|
||||
<DialogBody mb={ 6 }>
|
||||
{ securityReport && (
|
||||
<Flex
|
||||
direction={{ base: 'column', md: 'row' }}
|
||||
justifyContent={{ base: 'flex-start', md: 'space-between' }}
|
||||
gap={ 3 }
|
||||
fontSize="sm"
|
||||
mb={ 6 }
|
||||
>
|
||||
<Flex alignItems="center" gap={ 2 } flexWrap="wrap">
|
||||
<IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500"/>
|
||||
<Text>Verified contracts</Text>
|
||||
<Text fontWeight="500">
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</Text>
|
||||
<Link onClick={ showAllContracts } ml={ 1 }>
|
||||
View all contracts
|
||||
</Link>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Text>Security level</Text>
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showContractList }
|
||||
source="App modal"
|
||||
popoverPlacement={ isMobile ? 'bottom-start' : 'left' }
|
||||
popoverContentProps={{ zIndex: 'modal' }}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) }
|
||||
<Text>{ description }</Text>
|
||||
</DialogBody>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { chakra, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview, MarketplaceAppSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -17,26 +17,19 @@ import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
|
||||
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import ContractListModal from './ContractListModal';
|
||||
import MarketplaceAppInfo from './MarketplaceAppInfo';
|
||||
import Rating from './Rating/Rating';
|
||||
import useRatings from './Rating/useRatings';
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
data: MarketplaceApp | undefined;
|
||||
isLoading: boolean;
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
};
|
||||
|
||||
const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) => {
|
||||
const [ contractListType, setContractListType ] = React.useState<ContractListTypes>();
|
||||
const MarketplaceAppTopBar = ({ appId, data, isLoading }: Props) => {
|
||||
const appProps = useAppContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
|
||||
|
||||
const goBackUrl = React.useMemo(() => {
|
||||
if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) {
|
||||
return appProps.referrer;
|
||||
@@ -44,76 +37,52 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
|
||||
return route({ pathname: '/apps' });
|
||||
}, [ appProps.referrer ]);
|
||||
|
||||
const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []);
|
||||
const hideContractList = React.useCallback(() => setContractListType(undefined), []);
|
||||
|
||||
const handleBackToClick = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Back to', Source: mixpanel.PAGE_TYPE_DICT['/apps/[id]'] });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
|
||||
{ !isMobile && <NetworkLogo isCollapsed mr={ 4 }/> }
|
||||
<BackToButton
|
||||
href={ goBackUrl }
|
||||
hint="Back to dApps list"
|
||||
loading={ isLoading }
|
||||
onClick={ handleBackToClick }
|
||||
/>
|
||||
<Link
|
||||
external
|
||||
href={ data?.url }
|
||||
variant="underlaid"
|
||||
textStyle="sm"
|
||||
minW={ 0 }
|
||||
maxW={{ base: 'calc(100% - 114px)', md: 'auto' }}
|
||||
display="flex"
|
||||
loading={ isLoading }
|
||||
>
|
||||
<chakra.span truncate>
|
||||
{ makePrettyLink(data?.url)?.domain }
|
||||
</chakra.span>
|
||||
</Link>
|
||||
<MarketplaceAppInfo data={ data } isLoading={ isLoading }/>
|
||||
{ (securityReport || isLoading) && (
|
||||
<AppSecurityReport
|
||||
id={ data?.id || '' }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showContractList }
|
||||
isLoading={ isLoading }
|
||||
onlyIcon={ isMobile }
|
||||
source="App page"
|
||||
/>
|
||||
) }
|
||||
<Rating
|
||||
appId={ appId }
|
||||
rating={ ratings[appId] }
|
||||
userRating={ userRatings[appId] }
|
||||
rate={ rateApp }
|
||||
isSending={ isRatingSending }
|
||||
isLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
source="App page"
|
||||
/>
|
||||
{ !isMobile && (
|
||||
<Flex ml="auto" gap={ 2 }>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
|
||||
}
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
{ contractListType && (
|
||||
<ContractListModal
|
||||
type={ contractListType }
|
||||
contracts={ securityReport?.contractsData }
|
||||
onClose={ hideContractList }
|
||||
/>
|
||||
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
|
||||
{ !isMobile && <NetworkLogo isCollapsed mr={ 4 }/> }
|
||||
<BackToButton
|
||||
href={ goBackUrl }
|
||||
hint="Back to dApps list"
|
||||
loading={ isLoading }
|
||||
onClick={ handleBackToClick }
|
||||
/>
|
||||
<Link
|
||||
external
|
||||
href={ data?.url }
|
||||
variant="underlaid"
|
||||
textStyle="sm"
|
||||
minW={ 0 }
|
||||
maxW={{ base: 'calc(100% - 114px)', md: 'auto' }}
|
||||
display="flex"
|
||||
loading={ isLoading }
|
||||
>
|
||||
<chakra.span truncate>
|
||||
{ makePrettyLink(data?.url)?.domain }
|
||||
</chakra.span>
|
||||
</Link>
|
||||
<MarketplaceAppInfo data={ data } isLoading={ isLoading }/>
|
||||
<Rating
|
||||
appId={ appId }
|
||||
rating={ data?.rating }
|
||||
ratingsTotalCount={ data?.ratingsTotalCount }
|
||||
userRating={ data?.userRating }
|
||||
isLoading={ isLoading }
|
||||
source="App page"
|
||||
/>
|
||||
{ !isMobile && (
|
||||
<Flex ml="auto" gap={ 2 }>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
|
||||
}
|
||||
</Flex>
|
||||
) }
|
||||
</>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,36 +3,28 @@ import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
import EmptySearchResult from './EmptySearchResult';
|
||||
import MarketplaceAppCard from './MarketplaceAppCard';
|
||||
import type { RateFunction } from './Rating/useRatings';
|
||||
|
||||
type Props = {
|
||||
apps: Array<MarketplaceAppWithSecurityReport>;
|
||||
apps: Array<MarketplaceApp>;
|
||||
showAppInfo: (id: string) => void;
|
||||
favoriteApps: Array<string>;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void;
|
||||
isLoading: boolean;
|
||||
selectedCategoryId?: string;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
userRatings: Record<string, AppRating>;
|
||||
rateApp: RateFunction;
|
||||
isRatingSending: boolean;
|
||||
isRatingLoading: boolean;
|
||||
canRate: boolean | undefined;
|
||||
graphLinksQuery: UseQueryResult<Record<string, Array<{ title: string; url: string }>>, unknown>;
|
||||
};
|
||||
|
||||
const MarketplaceList = ({
|
||||
apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId,
|
||||
onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate,
|
||||
graphLinksQuery,
|
||||
onAppClick, graphLinksQuery,
|
||||
}: Props) => {
|
||||
const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16);
|
||||
|
||||
@@ -72,14 +64,9 @@ const MarketplaceList = ({
|
||||
isLoading={ isLoading }
|
||||
internalWallet={ app.internalWallet }
|
||||
onAppClick={ onAppClick }
|
||||
securityReport={ app.securityReport }
|
||||
showContractList={ showContractList }
|
||||
rating={ app.rating }
|
||||
userRating={ userRatings[app.id] }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
ratingsTotalCount={ app.ratingsTotalCount }
|
||||
userRating={ app.userRating }
|
||||
graphLinks={ graphLinksQuery.data?.[app.id] }
|
||||
/>
|
||||
)) }
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
import { Text, Flex, Spinner } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { Rating } from 'toolkit/chakra/rating';
|
||||
import { toaster } from 'toolkit/chakra/toaster';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import type { RateFunction } from './useRatings';
|
||||
|
||||
const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ];
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
rating?: AppRating;
|
||||
userRating?: AppRating;
|
||||
rate: RateFunction;
|
||||
isSending?: boolean;
|
||||
userRating?: number;
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
|
||||
};
|
||||
|
||||
const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => {
|
||||
const handleValueChange = React.useCallback(({ value }: { value: number }) => {
|
||||
rate(appId, rating?.recordId, userRating?.recordId, value, source);
|
||||
}, [ appId, rating, rate, userRating, source ]);
|
||||
const PopoverContent = ({ appId, userRating, source }: Props) => {
|
||||
const apiFetch = useApiFetch();
|
||||
const [ isSending, setIsSending ] = React.useState(false);
|
||||
const [ ratingValue, setRatingValue ] = React.useState(userRating);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleValueChange = React.useCallback(async({ value }: { value: number }) => {
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
await apiFetch('admin:marketplace_rate_dapp', {
|
||||
pathParams: { chainId: config.chain.id, dappId: appId },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { rating: value },
|
||||
},
|
||||
});
|
||||
|
||||
setRatingValue(value);
|
||||
queryClient.invalidateQueries({ queryKey: [ 'marketplace-dapps' ] });
|
||||
|
||||
toaster.success({
|
||||
title: 'Awesome! Thank you 💜',
|
||||
description: 'Your rating improves the service',
|
||||
});
|
||||
|
||||
mixpanel.logEvent(
|
||||
mixpanel.EventTypes.APP_FEEDBACK,
|
||||
{ Action: 'Rating', Source: source, AppId: appId, Score: value },
|
||||
);
|
||||
} catch (error) {
|
||||
toaster.error({
|
||||
title: 'Ooops! Something went wrong',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
|
||||
setIsSending(false);
|
||||
}, [ appId, source, apiFetch, queryClient ]);
|
||||
|
||||
if (isSending) {
|
||||
return (
|
||||
@@ -36,19 +69,19 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }:
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center">
|
||||
{ userRating && (
|
||||
<Flex alignItems="center" h="30px">
|
||||
{ ratingValue && (
|
||||
<IconSvg name="verified" color="green.400" boxSize="30px" mr={ 1 } ml="-5px"/>
|
||||
) }
|
||||
<Text fontWeight="500" textStyle="xs" color="text.secondary">
|
||||
{ userRating ? 'App is already rated by you' : 'How was your experience?' }
|
||||
{ ratingValue ? 'App is already rated by you' : 'How was your experience?' }
|
||||
</Text>
|
||||
</Flex>
|
||||
<Rating
|
||||
defaultValue={ userRating?.value }
|
||||
defaultValue={ ratingValue }
|
||||
onValueChange={ handleValueChange }
|
||||
label={ ratingDescriptions }
|
||||
mt={ 1 }
|
||||
h="32px"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import type { PopoverContentProps } from 'toolkit/chakra/popover';
|
||||
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
|
||||
import { Rating } from 'toolkit/chakra/rating';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
|
||||
import Content from './PopoverContent';
|
||||
import TriggerButton from './TriggerButton';
|
||||
import type { RateFunction } from './useRatings';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const isEnabled = feature.isEnabled && feature.rating;
|
||||
const isEnabled = feature.isEnabled && 'api' in feature;
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
rating?: AppRating;
|
||||
userRating?: AppRating;
|
||||
rate: RateFunction;
|
||||
isSending?: boolean;
|
||||
rating?: number;
|
||||
ratingsTotalCount?: number;
|
||||
userRating?: number;
|
||||
isLoading?: boolean;
|
||||
fullView?: boolean;
|
||||
canRate: boolean | undefined;
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
|
||||
popoverContentProps?: PopoverContentProps;
|
||||
};
|
||||
|
||||
const MarketplaceRating = ({
|
||||
appId, rating, userRating, rate,
|
||||
isSending, isLoading, fullView, canRate, source,
|
||||
popoverContentProps,
|
||||
appId, rating, ratingsTotalCount, userRating,
|
||||
isLoading, fullView, source, popoverContentProps,
|
||||
}: Props) => {
|
||||
const isAuth = useIsAuth();
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
@@ -49,28 +45,25 @@ const MarketplaceRating = ({
|
||||
>
|
||||
{ fullView && (
|
||||
<>
|
||||
<Rating defaultValue={ Math.floor(rating?.value || 0) } readOnly key={ rating?.value }/>
|
||||
<Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
|
||||
{ rating?.count && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ rating?.count })</Text> }
|
||||
<Rating defaultValue={ Math.floor(rating || 0) } readOnly key={ rating }/>
|
||||
{ rating && <Text fontSize="md" ml={ 2 }>{ rating }</Text> }
|
||||
{ ratingsTotalCount && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ ratingsTotalCount })</Text> }
|
||||
</>
|
||||
) }
|
||||
<PopoverRoot positioning={{ placement: 'bottom' }}>
|
||||
|
||||
<TriggerButton
|
||||
rating={ rating?.value }
|
||||
count={ rating?.count }
|
||||
rating={ rating }
|
||||
count={ ratingsTotalCount }
|
||||
fullView={ fullView }
|
||||
canRate={ canRate }
|
||||
canRate={ isAuth }
|
||||
/>
|
||||
{ canRate ? (
|
||||
{ isAuth ? (
|
||||
<PopoverContent w="250px" { ...popoverContentProps }>
|
||||
<PopoverBody>
|
||||
<Content
|
||||
appId={ appId }
|
||||
rating={ rating }
|
||||
userRating={ userRating }
|
||||
rate={ rate }
|
||||
isSending={ isSending }
|
||||
source={ source }
|
||||
/>
|
||||
</PopoverBody>
|
||||
|
||||
@@ -13,15 +13,12 @@ interface Props extends ButtonProps {
|
||||
rating?: number;
|
||||
count?: number;
|
||||
fullView?: boolean;
|
||||
canRate: boolean | undefined;
|
||||
canRate: boolean;
|
||||
};
|
||||
|
||||
const getTooltipText = (canRate: boolean | undefined) => {
|
||||
if (canRate === undefined) {
|
||||
return <>Please connect your wallet to Blockscout to rate this DApp.<br/>Only wallets with 5+ transactions are eligible</>;
|
||||
}
|
||||
const getTooltipText = (canRate: boolean) => {
|
||||
if (!canRate) {
|
||||
return <>Brand new wallets cannot leave ratings.<br/>Please connect a wallet with 5 or more transactions on this chain</>;
|
||||
return <>Please log in to Blockscout to rate this DApp.</>;
|
||||
}
|
||||
return <>Ratings come from verified users.<br/>Click here to rate!</>;
|
||||
};
|
||||
@@ -31,7 +28,6 @@ const TriggerButton = (
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
const onFocusCapture = usePreventFocusAfterModalClosing();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { act } from 'react';
|
||||
|
||||
import { renderHook, wrapper } from 'jest/lib';
|
||||
|
||||
import useRatings from './useRatings';
|
||||
|
||||
const useAccount = jest.fn();
|
||||
const useApiQuery = jest.fn();
|
||||
|
||||
jest.mock('toolkit/chakra/toaster', () => ({
|
||||
toaster: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('wagmi', () => ({ useAccount: () => useAccount() }));
|
||||
jest.mock('lib/api/useApiQuery', () => () => useApiQuery());
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set canRate to true if address is defined and transactions_count is 5 or more', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(true);
|
||||
});
|
||||
|
||||
it('should set canRate to undefined if address is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: undefined });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should set canRate to false if transactions_count is less than 5', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: { transactions_count: 4 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if isPlaceholderData is true', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: true,
|
||||
data: { transactions_count: 5 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if data is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
|
||||
it('should set canRate to false if transactions_count is undefined', async() => {
|
||||
useAccount.mockReturnValue({ address: '0x123' });
|
||||
useApiQuery.mockReturnValue({
|
||||
isPlaceholderData: false,
|
||||
data: {},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRatings(), { wrapper });
|
||||
await act(async() => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.canRate).toBe(false);
|
||||
});
|
||||
@@ -1,190 +0,0 @@
|
||||
import Airtable from 'airtable';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
import type { AppRating } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { ADDRESS_COUNTERS } from 'stubs/address';
|
||||
import { toaster } from 'toolkit/chakra/toaster';
|
||||
|
||||
const MIN_TRANSACTION_COUNT = 5;
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const airtable = (feature.isEnabled && feature.rating) ?
|
||||
new Airtable({ apiKey: feature.rating.airtableApiKey }).base(feature.rating.airtableBaseId) :
|
||||
undefined;
|
||||
|
||||
export type RateFunction = (
|
||||
appId: string,
|
||||
appRecordId: string | undefined,
|
||||
userRecordId: string | undefined,
|
||||
rating: number,
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'],
|
||||
) => void;
|
||||
|
||||
function formatRatings(data: Airtable.Records<Airtable.FieldSet>) {
|
||||
return data.reduce((acc: Record<string, AppRating>, record) => {
|
||||
const fields = record.fields as { appId: string | Array<string>; rating: number | undefined; count?: number };
|
||||
const appId = Array.isArray(fields.appId) ? fields.appId[0] : fields.appId;
|
||||
acc[appId] = {
|
||||
recordId: record.id,
|
||||
value: fields.rating,
|
||||
count: fields.count,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default function useRatings() {
|
||||
const { address } = useAccount();
|
||||
|
||||
const addressCountersQuery = useApiQuery<'general:address_counters', { status: number }>('general:address_counters', {
|
||||
pathParams: { hash: address },
|
||||
queryOptions: {
|
||||
enabled: Boolean(address),
|
||||
placeholderData: ADDRESS_COUNTERS,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [ ratings, setRatings ] = useState<Record<string, AppRating>>({});
|
||||
const [ userRatings, setUserRatings ] = useState<Record<string, AppRating>>({});
|
||||
const [ isRatingLoading, setIsRatingLoading ] = useState<boolean>(false);
|
||||
const [ isUserRatingLoading, setIsUserRatingLoading ] = useState<boolean>(false);
|
||||
const [ isSending, setIsSending ] = useState<boolean>(false);
|
||||
const [ canRate, setCanRate ] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const fetchRatings = useCallback(async() => {
|
||||
if (!airtable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await airtable('apps_ratings').select({ fields: [ 'appId', 'rating', 'count' ] }).all();
|
||||
const ratings = formatRatings(data);
|
||||
setRatings(ratings);
|
||||
} catch (error) {
|
||||
toaster.error({
|
||||
title: 'Error loading ratings',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetch() {
|
||||
setIsRatingLoading(true);
|
||||
await fetchRatings();
|
||||
setIsRatingLoading(false);
|
||||
}
|
||||
fetch();
|
||||
}, [ fetchRatings ]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUserRatings() {
|
||||
setIsUserRatingLoading(true);
|
||||
let userRatings = {} as Record<string, AppRating>;
|
||||
if (address && airtable) {
|
||||
try {
|
||||
const data = await airtable('users_ratings').select({
|
||||
filterByFormula: `address = "${ address }"`,
|
||||
fields: [ 'appId', 'rating' ],
|
||||
}).all();
|
||||
userRatings = formatRatings(data);
|
||||
} catch (error) {
|
||||
toaster.error({
|
||||
title: 'Error loading user ratings',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
}
|
||||
setUserRatings(userRatings);
|
||||
setIsUserRatingLoading(false);
|
||||
}
|
||||
fetchUserRatings();
|
||||
}, [ address ]);
|
||||
|
||||
useEffect(() => {
|
||||
const isPlaceholderData = addressCountersQuery?.isPlaceholderData;
|
||||
const transactionsCount = addressCountersQuery?.data?.transactions_count;
|
||||
const canRate = address && !isPlaceholderData && Number(transactionsCount || 0) >= MIN_TRANSACTION_COUNT;
|
||||
setCanRate(canRate);
|
||||
}, [ address, addressCountersQuery?.isPlaceholderData, addressCountersQuery?.data?.transactions_count ]);
|
||||
|
||||
const rateApp = useCallback(async(
|
||||
appId: string,
|
||||
appRecordId: string | undefined,
|
||||
userRecordId: string | undefined,
|
||||
rating: number,
|
||||
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'],
|
||||
) => {
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
if (!address || !airtable) {
|
||||
throw new Error('Address is missing');
|
||||
}
|
||||
|
||||
if (!appRecordId) {
|
||||
const records = await airtable('apps_ratings').create([ { fields: { appId } } ]);
|
||||
appRecordId = records[0].id;
|
||||
if (!appRecordId) {
|
||||
throw new Error('Record ID is missing');
|
||||
}
|
||||
}
|
||||
|
||||
if (!userRecordId) {
|
||||
const userRecords = await airtable('users_ratings').create([
|
||||
{
|
||||
fields: {
|
||||
address,
|
||||
appRecordId: [ appRecordId ],
|
||||
rating,
|
||||
},
|
||||
},
|
||||
]);
|
||||
userRecordId = userRecords[0].id;
|
||||
} else {
|
||||
await airtable('users_ratings').update(userRecordId, { rating });
|
||||
}
|
||||
|
||||
setUserRatings({
|
||||
...userRatings,
|
||||
[appId]: {
|
||||
recordId: userRecordId,
|
||||
value: rating,
|
||||
},
|
||||
});
|
||||
fetchRatings();
|
||||
|
||||
toaster.success({
|
||||
title: 'Awesome! Thank you 💜',
|
||||
description: 'Your rating improves the service',
|
||||
});
|
||||
mixpanel.logEvent(
|
||||
mixpanel.EventTypes.APP_FEEDBACK,
|
||||
{ Action: 'Rating', Source: source, AppId: appId, Score: rating },
|
||||
);
|
||||
} catch (error) {
|
||||
toaster.error({
|
||||
title: 'Ooops! Something went wrong',
|
||||
description: 'Please try again later',
|
||||
});
|
||||
}
|
||||
|
||||
setIsSending(false);
|
||||
}, [ address, userRatings, fetchRatings ]);
|
||||
|
||||
return {
|
||||
ratings,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending: isSending,
|
||||
isRatingLoading,
|
||||
isUserRatingLoading,
|
||||
canRate,
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,7 +1,6 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { ContractListTypes } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
@@ -9,7 +8,6 @@ import * as mixpanel from 'lib/mixpanel/index';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import removeQueryParam from 'lib/router/removeQueryParam';
|
||||
|
||||
import useRatings from './Rating/useRatings';
|
||||
import useMarketplaceApps from './useMarketplaceApps';
|
||||
import useMarketplaceCategories from './useMarketplaceCategories';
|
||||
|
||||
@@ -35,10 +33,8 @@ export default function useMarketplace() {
|
||||
const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false);
|
||||
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
|
||||
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
|
||||
const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null);
|
||||
const [ hasPreviousStep, setHasPreviousStep ] = React.useState<boolean>(false);
|
||||
|
||||
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'Security view' | 'App modal' | 'Banner') => {
|
||||
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean, source: 'Discovery view' | 'App modal' | 'Banner') => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id, Source: source });
|
||||
|
||||
const favoriteApps = getFavoriteApps();
|
||||
@@ -64,21 +60,11 @@ export default function useMarketplace() {
|
||||
setIsDisclaimerModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const showContractList = React.useCallback((id: string, type: ContractListTypes, hasPreviousStep?: boolean) => {
|
||||
setSelectedAppId(id);
|
||||
setContractListModalType(type);
|
||||
if (hasPreviousStep) {
|
||||
setHasPreviousStep(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedFilterQuery = useDebounce(filterQuery, 500);
|
||||
const clearSelectedAppId = React.useCallback(() => {
|
||||
setSelectedAppId(null);
|
||||
setIsAppInfoModalOpen(false);
|
||||
setIsDisclaimerModalOpen(false);
|
||||
setContractListModalType(null);
|
||||
setHasPreviousStep(false);
|
||||
}, []);
|
||||
|
||||
const handleCategoryChange = React.useCallback((newCategory: string) => {
|
||||
@@ -86,10 +72,9 @@ export default function useMarketplace() {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
|
||||
const {
|
||||
isPlaceholderData, isError, error, data, displayedApps, setSorting,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded, ratings);
|
||||
isPlaceholderData, isError, error, data, displayedApps, setSorting, refetch,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
|
||||
const {
|
||||
isPlaceholderData: isCategoriesPlaceholderData, data: categories,
|
||||
} = useMarketplaceCategories(data, isPlaceholderData);
|
||||
@@ -171,15 +156,8 @@ export default function useMarketplace() {
|
||||
showDisclaimer,
|
||||
appsTotal: data?.length || 0,
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
refetch,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
@@ -199,14 +177,7 @@ export default function useMarketplace() {
|
||||
isDisclaimerModalOpen,
|
||||
showDisclaimer,
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
refetch,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -9,23 +9,23 @@ import type { ResourceError } from 'lib/api/resources';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { MARKETPLACE_APP } from 'stubs/marketplace';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
|
||||
import useSecurityReports from './useSecurityReports';
|
||||
import type { SortValue } from './utils';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
|
||||
function isAppNameMatches(q: string, app: MarketplaceAppWithSecurityReport) {
|
||||
function isAppNameMatches(q: string, app: MarketplaceApp) {
|
||||
return app.title.toLowerCase().includes(q.toLowerCase());
|
||||
}
|
||||
|
||||
function isAppCategoryMatches(category: string, app: MarketplaceAppWithSecurityReport, favoriteApps: Array<string> = []) {
|
||||
function isAppCategoryMatches(category: string, app: MarketplaceApp, favoriteApps: Array<string> = []) {
|
||||
return category === MarketplaceCategory.ALL ||
|
||||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
|
||||
app.categories.includes(category);
|
||||
}
|
||||
|
||||
function sortApps(apps: Array<MarketplaceAppWithSecurityReport>, favoriteApps: Array<string> = []) {
|
||||
function sortApps(apps: Array<MarketplaceApp>, favoriteApps: Array<string> = []) {
|
||||
return apps.sort((a, b) => {
|
||||
const priorityA = a.priority || 0;
|
||||
const priorityB = b.priority || 0;
|
||||
@@ -55,12 +55,10 @@ export default function useMarketplaceApps(
|
||||
selectedCategoryId: string = MarketplaceCategory.ALL,
|
||||
favoriteApps: Array<string> | undefined = undefined,
|
||||
isFavoriteAppsLoaded: boolean = false,
|
||||
ratings: Record<string, AppRating> | undefined = undefined,
|
||||
) {
|
||||
const fetch = useFetch();
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
const { data: securityReports, isPlaceholderData: isSecurityReportsPlaceholderData } = useSecurityReports();
|
||||
const isAuth = useIsAuth();
|
||||
|
||||
const [ sorting, setSorting ] = React.useState<SortValue>();
|
||||
// Set the value only 1 time to avoid unnecessary useQuery calls and re-rendering of all applications
|
||||
@@ -75,54 +73,45 @@ export default function useMarketplaceApps(
|
||||
}, [ isFavoriteAppsLoaded, favoriteApps ]);
|
||||
|
||||
const {
|
||||
isPlaceholderData: isAppsPlaceholderData, isError, error, data,
|
||||
} = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppWithSecurityReport>>({
|
||||
queryKey: [ 'marketplace-dapps', snapshotFavoriteApps ],
|
||||
isPlaceholderData, isError, error, data, refetch,
|
||||
} = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceApp>>({
|
||||
queryKey: [ 'marketplace-dapps' ],
|
||||
queryFn: async() => {
|
||||
if (!feature.isEnabled) {
|
||||
return [];
|
||||
} else if ('configUrl' in feature) {
|
||||
return fetch<Array<MarketplaceAppWithSecurityReport>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
|
||||
return fetch<Array<MarketplaceApp>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
|
||||
} else {
|
||||
return apiFetch('admin:marketplace_dapps', { pathParams: { chainId: config.chain.id } });
|
||||
}
|
||||
},
|
||||
select: (data) => sortApps(data as Array<MarketplaceAppWithSecurityReport>, snapshotFavoriteApps),
|
||||
select: (data) => sortApps(data as Array<MarketplaceApp>, snapshotFavoriteApps),
|
||||
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
|
||||
staleTime: Infinity,
|
||||
enabled: feature.isEnabled && Boolean(snapshotFavoriteApps),
|
||||
});
|
||||
|
||||
const isPlaceholderData = isAppsPlaceholderData || isSecurityReportsPlaceholderData;
|
||||
|
||||
const appsWithSecurityReportsAndRating = React.useMemo(() =>
|
||||
data?.map((app) => ({
|
||||
...app,
|
||||
securityReport: securityReports?.[app.id],
|
||||
rating: ratings?.[app.id],
|
||||
})),
|
||||
[ data, securityReports, ratings ]);
|
||||
React.useEffect(() => {
|
||||
refetch();
|
||||
}, [ isAuth, refetch ]);
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
if (isPlaceholderData) {
|
||||
return appsWithSecurityReportsAndRating || [];
|
||||
return data || [];
|
||||
}
|
||||
|
||||
return appsWithSecurityReportsAndRating
|
||||
return data
|
||||
?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps))
|
||||
.sort((a, b) => {
|
||||
if (sorting === 'security_score') {
|
||||
return (b.securityReport?.overallInfo.securityScore || 0) - (a.securityReport?.overallInfo.securityScore || 0);
|
||||
}
|
||||
if (sorting === 'rating_score') {
|
||||
return (b.rating?.value || 0) - (a.rating?.value || 0);
|
||||
return (b.rating || 0) - (a.rating || 0);
|
||||
}
|
||||
if (sorting === 'rating_count') {
|
||||
return (b.rating?.count || 0) - (a.rating?.count || 0);
|
||||
return (b.ratingsTotalCount || 0) - (a.ratingsTotalCount || 0);
|
||||
}
|
||||
return 0;
|
||||
}) || [];
|
||||
}, [ selectedCategoryId, appsWithSecurityReportsAndRating, filter, favoriteApps, sorting, isPlaceholderData ]);
|
||||
}, [ selectedCategoryId, data, filter, favoriteApps, sorting, isPlaceholderData ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
data,
|
||||
@@ -131,6 +120,7 @@ export default function useMarketplaceApps(
|
||||
isError,
|
||||
isPlaceholderData,
|
||||
setSorting,
|
||||
refetch,
|
||||
}), [
|
||||
data,
|
||||
displayedApps,
|
||||
@@ -138,5 +128,6 @@ export default function useMarketplaceApps(
|
||||
isError,
|
||||
isPlaceholderData,
|
||||
setSorting,
|
||||
refetch,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
@@ -11,7 +11,7 @@ import { CATEGORIES } from 'stubs/marketplace';
|
||||
const feature = config.features.marketplace;
|
||||
const categoriesUrl = (feature.isEnabled && feature.categoriesUrl) || '';
|
||||
|
||||
export default function useMarketplaceCategories(apps: Array<MarketplaceAppOverview> | undefined, isAppsPlaceholderData: boolean) {
|
||||
export default function useMarketplaceCategories(apps: Array<MarketplaceApp> | undefined, isAppsPlaceholderData: boolean) {
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
const { isPlaceholderData, data } = useQuery<unknown, ResourceError<unknown>, Array<string>>({
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { MarketplaceAppSecurityReport, MarketplaceAppSecurityReportRaw } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useApiFetch from 'lib/hooks/useFetch';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
const securityReportsUrl = (feature.isEnabled && feature.securityReportsUrl) || '';
|
||||
|
||||
export default function useSecurityReports() {
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
return useQuery<unknown, ResourceError<unknown>, Record<string, MarketplaceAppSecurityReport>>({
|
||||
queryKey: [ 'marketplace-security-reports' ],
|
||||
queryFn: async() => apiFetch(securityReportsUrl, undefined, { resource: 'marketplace-security-reports' }),
|
||||
select: (data) => {
|
||||
const securityReports: Record<string, MarketplaceAppSecurityReport> = {};
|
||||
(data as Array<MarketplaceAppSecurityReportRaw>).forEach((item) => {
|
||||
const report = item.chainsData[config.chain.id || ''];
|
||||
if (report) {
|
||||
const issues: Record<string, number> = report.overallInfo.issueSeverityDistribution;
|
||||
report.overallInfo.totalIssues = Object.values(issues).reduce((acc, val) => acc + val, 0);
|
||||
report.overallInfo.securityScore = Number(report.overallInfo.securityScore.toFixed(2));
|
||||
}
|
||||
securityReports[item.appName] = report;
|
||||
});
|
||||
return securityReports;
|
||||
},
|
||||
placeholderData: securityReportsUrl ? {} : undefined,
|
||||
staleTime: Infinity,
|
||||
enabled: Boolean(securityReportsUrl),
|
||||
});
|
||||
}
|
||||
@@ -7,13 +7,12 @@ import type { SelectOption } from 'toolkit/chakra/select';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
|
||||
export type SortValue = 'default' | 'rating_score' | 'rating_count' | 'security_score';
|
||||
export type SortValue = 'default' | 'rating_score' | 'rating_count';
|
||||
|
||||
export const SORT_OPTIONS: Array<SelectOption<SortValue>> = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
(feature.isEnabled && feature.rating) && { label: 'Top rated', value: 'rating_score' },
|
||||
(feature.isEnabled && feature.rating) && { label: 'Most rated', value: 'rating_count' },
|
||||
(feature.isEnabled && feature.securityReportsUrl) && { label: 'Security score', value: 'security_score' },
|
||||
(feature.isEnabled && 'api' in feature) && { label: 'Top rated', value: 'rating_score' },
|
||||
(feature.isEnabled && 'api' in feature) && { label: 'Most rated', value: 'rating_count' },
|
||||
].filter(Boolean) as Array<SelectOption<SortValue>>;
|
||||
|
||||
export function getAppUrl(url: string | undefined, router: NextRouter) {
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { ratings as ratingsMock } from 'mocks/apps/ratings';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
import Marketplace from './Marketplace';
|
||||
|
||||
const MARKETPLACE_CONFIG_URL = 'http://localhost:4000/marketplace-config.json';
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://localhost:4000/marketplace-security-reports.json';
|
||||
|
||||
test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse, page }) => {
|
||||
test.beforeEach(async({ mockEnvs, mockAssetResponse, mockApiResponse }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, appsMock);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, securityReportsMock);
|
||||
await mockApiResponse('admin:marketplace_dapps', appsMock, { pathParams: { chainId: config.chain.id } });
|
||||
await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg')));
|
||||
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating&fields%5B%5D=count', (route) => route.fulfill({
|
||||
status: 200,
|
||||
json: ratingsMock,
|
||||
}));
|
||||
});
|
||||
|
||||
test('base view +@dark-mode', async({ render, page }) => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from 'toolkit/chakra/men
|
||||
import AdaptiveTabs from 'toolkit/components/AdaptiveTabs/AdaptiveTabs';
|
||||
import { FilterInput } from 'toolkit/components/filters/FilterInput';
|
||||
import Banner from 'ui/marketplace/Banner';
|
||||
import ContractListModal from 'ui/marketplace/ContractListModal';
|
||||
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
|
||||
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
|
||||
import MarketplaceList from 'ui/marketplace/MarketplaceList';
|
||||
@@ -73,15 +72,7 @@ const Marketplace = () => {
|
||||
showDisclaimer,
|
||||
appsTotal,
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
userRatings,
|
||||
rateApp,
|
||||
isRatingSending,
|
||||
isRatingLoading,
|
||||
canRate,
|
||||
} = useMarketplace();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -135,13 +126,6 @@ const Marketplace = () => {
|
||||
}
|
||||
}, [ showDisclaimer ]);
|
||||
|
||||
const handleGoBackInContractListModal = React.useCallback(() => {
|
||||
clearSelectedAppId();
|
||||
if (selectedApp) {
|
||||
showAppInfo(selectedApp.id);
|
||||
}
|
||||
}, [ clearSelectedAppId, showAppInfo, selectedApp ]);
|
||||
|
||||
const handleSortChange = React.useCallback(({ value }: { value: Array<string> }) => {
|
||||
setSorting(value[0] as SortValue);
|
||||
}, [ setSorting ]);
|
||||
@@ -248,12 +232,6 @@ const Marketplace = () => {
|
||||
isLoading={ isPlaceholderData }
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onAppClick={ handleAppClick }
|
||||
showContractList={ showContractList }
|
||||
userRatings={ userRatings }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
graphLinksQuery={ graphLinksQuery }
|
||||
/>
|
||||
|
||||
@@ -263,12 +241,6 @@ const Marketplace = () => {
|
||||
isFavorite={ favoriteApps.includes(selectedApp.id) }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
data={ selectedApp }
|
||||
showContractList={ showContractList }
|
||||
userRating={ userRatings[selectedApp.id] }
|
||||
rateApp={ rateApp }
|
||||
isRatingSending={ isRatingSending }
|
||||
isRatingLoading={ isRatingLoading }
|
||||
canRate={ canRate }
|
||||
graphLinks={ graphLinksQuery.data?.[selectedApp.id] }
|
||||
/>
|
||||
) }
|
||||
@@ -280,15 +252,6 @@ const Marketplace = () => {
|
||||
appId={ selectedApp.id }
|
||||
/>
|
||||
) }
|
||||
|
||||
{ (selectedApp && contractListModalType) && (
|
||||
<ContractListModal
|
||||
type={ contractListModalType }
|
||||
contracts={ selectedApp?.securityReport?.contractsData }
|
||||
onClose={ clearSelectedAppId }
|
||||
onBack={ hasPreviousStep ? handleGoBackInContractListModal : undefined }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ import { numberToHex } from 'viem';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { ratings as ratingsMock } from 'mocks/apps/ratings';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import type { TestFnArgs } from 'playwright/lib';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
@@ -18,28 +16,16 @@ const hooksConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const MARKETPLACE_CONFIG_URL = 'http://localhost:4000/marketplace-config.json';
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'http://localhost:4000/marketplace-security-reports.json';
|
||||
|
||||
const testFn = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs, mockRpcResponse, page }: TestFnArgs) => {
|
||||
const testFn = async({ render, mockAssetResponse, mockEnvs, mockRpcResponse, mockApiResponse }: TestFnArgs) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, appsMock);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, securityReportsMock);
|
||||
await mockApiResponse('admin:marketplace_dapp', appsMock[0], { pathParams: { chainId: config.chain.id, dappId: appsMock[0].id } });
|
||||
await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html');
|
||||
await mockRpcResponse({
|
||||
Method: 'eth_chainId',
|
||||
ReturnType: numberToHex(Number(config.chain.id)),
|
||||
});
|
||||
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating&fields%5B%5D=count', (route) => route.fulfill({
|
||||
status: 200,
|
||||
json: ratingsMock,
|
||||
}));
|
||||
|
||||
const component = await render(
|
||||
<Flex flexDirection="column" mx={{ base: 4, lg: 6 }} h="100vh">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -18,11 +18,11 @@ import * as metadata from 'lib/metadata';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { useColorMode } from 'toolkit/chakra/color-mode';
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
|
||||
import MarketplaceAppTopBar from '../marketplace/MarketplaceAppTopBar';
|
||||
import useAutoConnectWallet from '../marketplace/useAutoConnectWallet';
|
||||
import useMarketplaceWallet from '../marketplace/useMarketplaceWallet';
|
||||
import useSecurityReports from '../marketplace/useSecurityReports';
|
||||
import { getAppUrl } from '../marketplace/utils';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
@@ -36,7 +36,7 @@ const IFRAME_ALLOW_ATTRIBUTE = 'clipboard-read; clipboard-write;';
|
||||
|
||||
type Props = {
|
||||
address: string | undefined;
|
||||
data: MarketplaceAppOverview | undefined;
|
||||
data: MarketplaceApp | undefined;
|
||||
isPending: boolean;
|
||||
appUrl?: string;
|
||||
};
|
||||
@@ -100,27 +100,26 @@ const MarketplaceAppContent = ({ address, data, isPending, appUrl }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const MarketplaceApp = () => {
|
||||
export default function MarketplaceApp() {
|
||||
const fetch = useFetch();
|
||||
const apiFetch = useApiFetch();
|
||||
const router = useRouter();
|
||||
const id = getQueryParamString(router.query.id);
|
||||
const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet(id);
|
||||
const isAuth = useIsAuth();
|
||||
useAutoConnectWallet();
|
||||
|
||||
const { data: securityReports, isLoading: isSecurityReportsLoading } = useSecurityReports();
|
||||
|
||||
const query = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
|
||||
const query = useQuery<unknown, ResourceError<unknown>, MarketplaceApp>({
|
||||
queryKey: [ 'marketplace-dapps', id ],
|
||||
queryFn: async() => {
|
||||
if (!feature.isEnabled) {
|
||||
return null;
|
||||
} else if ('configUrl' in feature) {
|
||||
const result = await fetch<Array<MarketplaceAppOverview>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
|
||||
const result = await fetch<Array<MarketplaceApp>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
|
||||
if (!Array.isArray(result)) {
|
||||
throw result;
|
||||
}
|
||||
const item = result.find((app: MarketplaceAppOverview) => app.id === id);
|
||||
const item = result.find((app: MarketplaceApp) => app.id === id);
|
||||
if (!item) {
|
||||
throw { status: 404 };
|
||||
}
|
||||
@@ -131,7 +130,12 @@ const MarketplaceApp = () => {
|
||||
},
|
||||
enabled: feature.isEnabled,
|
||||
});
|
||||
const { data, isPending } = query;
|
||||
const { data, isPending, refetch } = query;
|
||||
|
||||
React.useEffect(() => {
|
||||
refetch();
|
||||
}, [ isAuth, refetch ]);
|
||||
|
||||
const { setIsAutoConnectDisabled } = useMarketplaceContext();
|
||||
|
||||
const appUrl = useMemo(() => getAppUrl(data?.url, router), [ data?.url, router ]);
|
||||
@@ -153,8 +157,7 @@ const MarketplaceApp = () => {
|
||||
<MarketplaceAppTopBar
|
||||
appId={ id }
|
||||
data={ data }
|
||||
isLoading={ isPending || isSecurityReportsLoading }
|
||||
securityReport={ securityReports?.[id] }
|
||||
isLoading={ isPending }
|
||||
/>
|
||||
<DappscoutIframeProvider
|
||||
address={ address }
|
||||
@@ -169,5 +172,3 @@ const MarketplaceApp = () => {
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceApp;
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,4 +1,4 @@
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
import type { SearchResultItem } from 'types/client/search';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -8,11 +8,11 @@ export type Category = ApiCategory | 'app';
|
||||
|
||||
export type ItemsCategoriesMap =
|
||||
Record<ApiCategory, Array<SearchResultItem>> &
|
||||
Record<'app', Array<MarketplaceAppOverview>>;
|
||||
Record<'app', Array<MarketplaceApp>>;
|
||||
|
||||
export type SearchResultAppItem = {
|
||||
type: 'app';
|
||||
app: MarketplaceAppOverview;
|
||||
app: MarketplaceApp;
|
||||
};
|
||||
|
||||
export const searchCategories: Array<{ id: Category; title: string }> = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Spinner } from '@chakra-ui/react';
|
||||
import { Spinner, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
|
||||
@@ -14,21 +13,16 @@ import useScoreLevelAndColor from './useScoreLevelAndColor';
|
||||
interface Props extends ButtonProps {
|
||||
score: number;
|
||||
isLoading?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
label?: string | React.ReactElement;
|
||||
wrapperProps?: BoxProps;
|
||||
}
|
||||
|
||||
const SolidityscanReportButton = (
|
||||
{ score, isLoading, onlyIcon, label = 'Security score', wrapperProps, ...rest }: Props,
|
||||
) => {
|
||||
const SolidityscanReportButton = ({ score, isLoading, ...rest }: Props) => {
|
||||
const { scoreColor } = useScoreLevelAndColor(score);
|
||||
const colorLoading = { _light: 'gray.300', _dark: 'gray.600' };
|
||||
const onFocusCapture = usePreventFocusAfterModalClosing();
|
||||
|
||||
return (
|
||||
<Tooltip content={ label } disableOnMobile>
|
||||
<Box { ...wrapperProps }>
|
||||
<Tooltip content="Security score" disableOnMobile>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
color={ isLoading ? colorLoading : scoreColor }
|
||||
@@ -51,7 +45,7 @@ const SolidityscanReportButton = (
|
||||
>
|
||||
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 }/>
|
||||
{ isLoading && <Spinner size="sm"/> }
|
||||
{ !isLoading && (onlyIcon ? null : score) }
|
||||
{ !isLoading && score }
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex, Text } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import type { MarketplaceApp } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -13,7 +13,7 @@ import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import SearchBarSuggestItemLink from './SearchBarSuggestItemLink';
|
||||
interface Props {
|
||||
data: MarketplaceAppOverview;
|
||||
data: MarketplaceApp;
|
||||
isMobile: boolean | undefined;
|
||||
searchTerm: string;
|
||||
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
|
||||