Support new API for dapp ratings (#2916); Remove dapp security score (#2918)

This commit is contained in:
Max Alekseenko
2025-08-08 14:49:14 +02:00
committed by GitHub
parent 11c001864d
commit c24d92193f
76 changed files with 275 additions and 2447 deletions
+1 -2
View File
@@ -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
View File
@@ -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,
};
+1 -2
View File
@@ -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']
+1 -3
View File
@@ -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
-2
View File
@@ -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
+1 -3
View File
@@ -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
+1 -3
View File
@@ -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
+1 -3
View File
@@ -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
+1 -3
View File
@@ -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']
+1 -2
View File
@@ -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']
+1 -2
View File
@@ -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']
+1 -3
View File
@@ -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
+1 -2
View File
@@ -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
-1
View File
@@ -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"
-1
View File
@@ -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',
+2 -87
View File
@@ -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
+3
View File
@@ -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 |
-3
View File
@@ -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
+2 -2
View File
@@ -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

+7 -3
View File
@@ -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 */
+2 -5
View File
@@ -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;
+2
View File
@@ -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',
-13
View File
@@ -1,13 +0,0 @@
import { apps } from './apps';
export const ratings = {
records: [
{
fields: {
appId: apps[0].id,
rating: 4.3,
count: 15,
},
},
],
};
-60
View File
@@ -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',
},
},
},
],
},
},
},
];
-1
View File
@@ -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 -4
View File
@@ -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)) {
+2 -2
View File
@@ -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',
+1 -1
View File
@@ -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>
);
}) }
+11 -48
View File
@@ -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;
};
};
-127
View File
@@ -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;
+2 -2
View File
@@ -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 -2
View File
@@ -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 -2
View File
@@ -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;
-101
View File
@@ -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;
-67
View File
@@ -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;
+31 -64
View File
@@ -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>
);
+2 -2
View File
@@ -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'>> = [
+2 -16
View File
@@ -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();
+8 -73
View File
@@ -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>
+43 -74
View File
@@ -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>
);
};
+5 -18
View File
@@ -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] }
/>
)) }
+50 -17
View File
@@ -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"
/>
</>
);
+15 -22
View File
@@ -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>
+3 -7
View File
@@ -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 (
-110
View File
@@ -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);
});
-190
View File
@@ -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,
};
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+5 -34
View File
@@ -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,
]);
}
+21 -30
View File
@@ -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,
]);
}
+2 -2
View File
@@ -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>>({
-35
View File
@@ -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),
});
}
+3 -4
View File
@@ -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) {
+3 -16
View File
@@ -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 }) => {
-37
View File
@@ -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 }
/>
) }
</>
);
};
+2 -16
View File
@@ -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">
+15 -14
View File
@@ -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;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+3 -3
View File
@@ -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;