diff --git a/.browserslistrc b/.browserslistrc index ca521ad75..c517611c3 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,5 +1,5 @@ [production] -chrome >= 49 +chrome >= 103 firefox >= 52 opera >= 36 safari >= 14 diff --git a/package.json b/package.json index 546a87bdf..093722ba3 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,6 @@ "webextension-polyfill": "^0.10.0", "webpack": "^5.74.0", "webpack-cli": "^5", - "webpack-ext-reloader": "^1.1.9", "webpack-ext-reloader-mv3": "^2.1.1", "webpack-target-webextension": "^1.0.4", "webpackbar": "^5", diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 805443675..66e9c8bba 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,7 +1,21 @@ export enum ContentScriptType { - ExternalLinksActivity = 'ExternalLinksActivity' + ExternalLinksActivity = 'ExternalLinksActivity', + ExternalAdsActivity = 'ExternalAdsActivity' +} + +export enum AdType { + EtherscanBuiltin = 'etherscan-builtin', + Bitmedia = 'bitmedia', + Coinzilla = 'coinzilla', + Cointraffic = 'cointraffic' } export const WEBSITES_ANALYTICS_ENABLED = 'WEBSITES_ANALYTICS_ENABLED'; export const ACCOUNT_PKH_STORAGE_KEY = 'account_publickeyhash'; + +export const ETHERSCAN_BUILTIN_ADS_WEBSITES = [ + 'https://etherscan.io', + 'https://bscscan.com', + 'https://polygonscan.com' +]; diff --git a/src/lib/slise/get-ads-containers.ts b/src/lib/slise/get-ads-containers.ts index 3f9b0aa1a..e8a7b7931 100644 --- a/src/lib/slise/get-ads-containers.ts +++ b/src/lib/slise/get-ads-containers.ts @@ -1,41 +1,81 @@ +import { AdType, ETHERSCAN_BUILTIN_ADS_WEBSITES } from 'lib/constants'; + interface AdContainerProps { element: HTMLElement; width: number; + height: number; + type: AdType; } -const getFinalWidth = (element: Element) => { +const getFinalSize = (element: Element) => { const elementStyle = getComputedStyle(element); - const rawWidthFromStyle = elementStyle.width; - const rawWidthFromAttribute = element.getAttribute('width'); + const size = { width: 0, height: 0 }; + const dimensions = ['width', 'height'] as const; + + for (const dimension of dimensions) { + const rawDimensionFromStyle = elementStyle[dimension]; + const rawDimensionFromAttribute = element.getAttribute(dimension); + const rawDimension = rawDimensionFromAttribute || rawDimensionFromStyle; + + if (/\d+px/.test(rawDimension)) { + size[dimension] = Number(rawDimension.replace('px', '')); + } else { + size[dimension] = dimension === 'width' ? element.clientWidth : element.clientHeight; + } + } - return Number((rawWidthFromAttribute || rawWidthFromStyle).replace('px', '') || element.clientWidth); + return size; }; +const mapBannersWithType = (banners: NodeListOf, type: AdType) => + [...banners].map(banner => ({ banner, type })); + export const getAdsContainers = () => { - const builtInAdsImages = [...document.querySelectorAll('span + img')].filter(element => { - const { width, height } = element.getBoundingClientRect(); - const label = element.previousElementSibling?.innerHTML ?? ''; + const locationUrl = window.parent.location.href; + const builtInAdsImages = ETHERSCAN_BUILTIN_ADS_WEBSITES.some(urlPrefix => locationUrl.startsWith(urlPrefix)) + ? [...document.querySelectorAll('span + img')].filter(element => { + const { width, height } = element.getBoundingClientRect(); + const label = element.previousElementSibling?.innerHTML ?? ''; - return (width > 0 || height > 0) && ['Featured', 'Ad'].includes(label); - }); - const coinzillaBanners = [...document.querySelectorAll('.coinzilla')]; - const bitmediaBanners = [...document.querySelectorAll('iframe[src*="media.bmcdn"], iframe[src*="cdn.bmcdn"]')]; + return (width > 0 || height > 0) && ['Featured', 'Ad'].includes(label); + }) + : []; + const coinzillaBanners = mapBannersWithType( + document.querySelectorAll('iframe[src*="coinzilla.io"], iframe[src*="czilladx.com"]'), + AdType.Coinzilla + ); + const bitmediaBanners = mapBannersWithType( + document.querySelectorAll('iframe[src*="media.bmcdn"], iframe[src*="cdn.bmcdn"]'), + AdType.Bitmedia + ); + const cointrafficBanners = mapBannersWithType( + document.querySelectorAll('iframe[src*="ctengine.io"]'), + AdType.Cointraffic + ); return builtInAdsImages .map((image): AdContainerProps | null => { const element = image.closest('div'); - return element && { element, width: getFinalWidth(image) }; + return ( + element && { + ...getFinalSize(image), + element, + type: AdType.EtherscanBuiltin + } + ); }) .concat( - [...bitmediaBanners, ...coinzillaBanners].map(banner => { - const parentElement = banner.parentElement; - const closestDiv = parentElement?.closest('div') ?? null; - const element = bitmediaBanners.includes(banner) ? closestDiv : parentElement; - const widthDefinedElement = element?.parentElement ?? parentElement; - const bannerFrame = banner.tagName === 'iframe' ? banner : banner.querySelector('iframe'); - - return element && { element, width: getFinalWidth(bannerFrame || widthDefinedElement!) }; + [...bitmediaBanners, ...coinzillaBanners, ...cointrafficBanners].map(({ banner, type }) => { + const element = banner.parentElement?.closest('div') ?? null; + + return ( + element && { + ...getFinalSize(banner), + element, + type + } + ); }) ) .filter((element): element is AdContainerProps => Boolean(element)); diff --git a/src/lib/slise/get-slot-id.ts b/src/lib/slise/get-slot-id.ts index 6f0e67880..851e95b0d 100644 --- a/src/lib/slise/get-slot-id.ts +++ b/src/lib/slise/get-slot-id.ts @@ -1,7 +1,9 @@ export const getSlotId = () => { - const hostnameParts = window.location.hostname.split('.').filter(part => part !== 'www'); + const hostnameParts = window.parent.location.hostname.split('.').filter(part => part !== 'www'); const serviceId = hostnameParts[0]; - const pathnameParts = window.location.pathname.split('/').filter(part => part !== '' && !/0x[0-9a-f]+/i.test(part)); + const pathnameParts = window.parent.location.pathname + .split('/') + .filter(part => part !== '' && !/^(0x)?[0-9a-f]+$/i.test(part) && !/^[0-9a-z]{30,}$/i.test(part)); return [serviceId, ...pathnameParts].join('-'); }; diff --git a/src/lib/slise/slise-ad.tsx b/src/lib/slise/slise-ad.tsx index f7ea675d4..8efe6c26d 100644 --- a/src/lib/slise/slise-ad.tsx +++ b/src/lib/slise/slise-ad.tsx @@ -4,12 +4,18 @@ import { SliseAd as OriginalSliseAd, SliseAdProps } from '@slise/embed-react'; import { useDidMount } from 'lib/ui/hooks/useDidMount'; +import { getSlotId } from './get-slot-id'; + interface CunningSliseAdProps extends Omit { width: number; height: number; } -export const SliseAd = memo(({ width, height, ...restProps }: CunningSliseAdProps) => { +export const buildSliceAdReactNode = (width: number, height: number) => ( + +); + +const SliseAd = memo(({ width, height, ...restProps }: CunningSliseAdProps) => { useDidMount(() => require('./slise-ad.embed')); return ; diff --git a/src/lib/temple/back/main.ts b/src/lib/temple/back/main.ts index 34e52aab1..e2aae7ac1 100644 --- a/src/lib/temple/back/main.ts +++ b/src/lib/temple/back/main.ts @@ -1,5 +1,6 @@ import browser, { Runtime } from 'webextension-polyfill'; +import { ACCOUNT_PKH_STORAGE_KEY, ContentScriptType } from 'lib/constants'; import { E2eMessageType } from 'lib/e2e/types'; import { BACKGROUND_IS_WORKER } from 'lib/env'; import { encodeMessage, encryptMessage, getSenderId, MessageType, Response } from 'lib/temple/beacon'; @@ -7,7 +8,6 @@ import { clearAsyncStorages } from 'lib/temple/reset'; import { TempleMessageType, TempleRequest, TempleResponse } from 'lib/temple/types'; import { getTrackedUrl } from 'lib/utils/url-track/get-tracked-url'; -import { ACCOUNT_PKH_STORAGE_KEY, ContentScriptType } from '../../constants'; import * as Actions from './actions'; import * as Analytics from './analytics'; import { intercom } from './defaults'; @@ -245,24 +245,30 @@ const processRequest = async (req: TempleRequest, port: Runtime.Port): Promise { - if (msg?.type === ContentScriptType.ExternalLinksActivity) { - const url = getTrackedUrl(msg.url); + switch (msg?.type) { + case ContentScriptType.ExternalLinksActivity: + const url = getTrackedUrl(msg.url); + + if (url) { + browser.storage.local + .get(ACCOUNT_PKH_STORAGE_KEY) + .then(({ [ACCOUNT_PKH_STORAGE_KEY]: accountPkh }) => + Analytics.client.track('External links activity', { url, accountPkh }) + ) + .catch(console.error); + } - if (url) { + break; + case ContentScriptType.ExternalAdsActivity: browser.storage.local .get(ACCOUNT_PKH_STORAGE_KEY) .then(({ [ACCOUNT_PKH_STORAGE_KEY]: accountPkh }) => - Analytics.client.track('External links activity', { url, accountPkh }) + Analytics.client.track('External Ads Activity', { url: msg.url, accountPkh }) ) .catch(console.error); - } - } - - if (msg?.type === E2eMessageType.ResetRequest) { - return new Promise(async resolve => { - await clearAsyncStorages(); - resolve({ type: E2eMessageType.ResetResponse }); - }); + break; + case E2eMessageType.ResetRequest: + return clearAsyncStorages().then(() => ({ type: E2eMessageType.ResetResponse })); } return; diff --git a/src/replaceAds.ts b/src/replaceAds.ts new file mode 100644 index 000000000..5a8bed7d1 --- /dev/null +++ b/src/replaceAds.ts @@ -0,0 +1,81 @@ +import browser from 'webextension-polyfill'; + +import { AdType, ContentScriptType, ETHERSCAN_BUILTIN_ADS_WEBSITES, WEBSITES_ANALYTICS_ENABLED } from 'lib/constants'; +import { getAdsContainers } from 'lib/slise/get-ads-containers'; + +const availableAdsResolutions = [ + { width: 270, height: 90 }, + { width: 728, height: 90 } +]; + +let oldHref = ''; + +let processing = false; + +const replaceAds = async () => { + if (processing) return; + processing = true; + + try { + const adsContainers = getAdsContainers(); + const adsContainersToReplace = adsContainers.filter( + ({ width, height }) => + ((width >= 600 && width <= 900) || (width >= 180 && width <= 430)) && height >= 60 && height <= 120 + ); + + const newHref = window.parent.location.href; + if (oldHref !== newHref && adsContainersToReplace.length > 0) { + oldHref = newHref; + + browser.runtime.sendMessage({ + type: ContentScriptType.ExternalAdsActivity, + url: window.parent.location.origin + }); + } + + if (!adsContainersToReplace.length) { + processing = false; + return; + } + + const ReactDomModule = await import('react-dom/client'); + const SliceAdModule = await import('lib/slise/slise-ad'); + + adsContainersToReplace.forEach(({ element: adContainer, width: containerWidth, type }) => { + let adsResolution = availableAdsResolutions[0]; + for (let i = 1; i < availableAdsResolutions.length; i++) { + const candidate = availableAdsResolutions[i]; + if (candidate.width <= containerWidth && candidate.width > adsResolution.width) { + adsResolution = candidate; + } + } + + if ( + ETHERSCAN_BUILTIN_ADS_WEBSITES.some(urlPrefix => newHref.startsWith(urlPrefix)) && + type === AdType.Coinzilla + ) { + adContainer.style.textAlign = 'left'; + } + + const adRoot = ReactDomModule.createRoot(adContainer); + adRoot.render(SliceAdModule.buildSliceAdReactNode(adsResolution.width, adsResolution.height)); + }); + } catch (error) { + console.error('Replacing Ads error:', error); + } + + processing = false; +}; + +// Prevents the script from running in an Iframe +if (window.frameElement === null) { + browser.storage.local.get(WEBSITES_ANALYTICS_ENABLED).then(storage => { + if (storage[WEBSITES_ANALYTICS_ENABLED]) { + // Replace ads with those from Slise + window.addEventListener('load', () => replaceAds()); + window.addEventListener('ready', () => replaceAds()); + setInterval(() => replaceAds(), 1000); + replaceAds(); + } + }); +} diff --git a/src/replaceAds.tsx b/src/replaceAds.tsx deleted file mode 100644 index eb5a89a4e..000000000 --- a/src/replaceAds.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; - -import debounce from 'debounce'; -import { createRoot } from 'react-dom/client'; -import browser from 'webextension-polyfill'; - -import { WEBSITES_ANALYTICS_ENABLED } from 'lib/constants'; -import { getAdsContainers } from 'lib/slise/get-ads-containers'; -import { getSlotId } from 'lib/slise/get-slot-id'; -import { SliseAd } from 'lib/slise/slise-ad'; - -const availableAdsResolutions = [ - { width: 270, height: 90 }, - { width: 728, height: 90 } -]; - -const replaceAds = debounce( - () => { - try { - const adsContainers = getAdsContainers(); - - adsContainers.forEach(({ element: adContainer, width: containerWidth }) => { - let adsResolution = availableAdsResolutions[0]; - for (let i = 1; i < availableAdsResolutions.length; i++) { - const candidate = availableAdsResolutions[i]; - if (candidate.width <= containerWidth && candidate.width > adsResolution.width) { - adsResolution = candidate; - } - } - - const adRoot = createRoot(adContainer); - adRoot.render( - - ); - }); - } catch {} - }, - 100, - true -); - -// Prevents the script from running in an Iframe -if (window.frameElement === null) { - browser.storage.local.get(WEBSITES_ANALYTICS_ENABLED).then(storage => { - if (storage[WEBSITES_ANALYTICS_ENABLED]) { - // Replace ads with those from Slise - window.addEventListener('load', () => replaceAds()); - window.addEventListener('ready', () => replaceAds()); - setInterval(() => replaceAds(), 1000); - replaceAds(); - } - }); -} diff --git a/webpack.config.ts b/webpack.config.ts index e71aa8003..fe44ff136 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -10,7 +10,6 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import * as Path from 'path'; -import ExtensionReloaderBadlyTyped, { ExtensionReloader as ExtensionReloaderType } from 'webpack-ext-reloader'; import ExtensionReloaderMV3BadlyTyped, { ExtensionReloader as ExtensionReloaderMV3Type } from 'webpack-ext-reloader-mv3'; @@ -32,7 +31,6 @@ import { buildManifest } from './webpack/manifest'; import { PATHS } from './webpack/paths'; import { isTruthy } from './webpack/utils'; -const ExtensionReloader = ExtensionReloaderBadlyTyped as ExtensionReloaderType; const ExtensionReloaderMV3 = ExtensionReloaderMV3BadlyTyped as ExtensionReloaderMV3Type; const PAGES_NAMES = ['popup', 'fullpage', 'confirm', 'options']; @@ -158,9 +156,12 @@ const mainConfig = (() => { const scriptsConfig = (() => { const config = buildBaseConfig(); + // Required for dynamic imports `import()` + config.output!.chunkFormat = 'module'; + config.entry = { contentScript: Path.join(PATHS.SOURCE, 'contentScript.ts'), - replaceAds: Path.join(PATHS.SOURCE, 'replaceAds.tsx') + replaceAds: Path.join(PATHS.SOURCE, 'replaceAds.ts') }; if (BACKGROUND_IS_WORKER) @@ -183,18 +184,7 @@ const scriptsConfig = (() => { cleanOnceBeforeBuildPatterns: ['scripts/**'], cleanStaleWebpackAssets: false, verbose: false - }), - - /* Page reloading in development mode */ - DEVELOPMENT_ENV && - new ExtensionReloader({ - port: RELOADER_PORTS.SCRIPTS, - reloadPage: true, - entries: { - background: '', - contentScript: CONTENT_SCRIPTS - } - }) + }) ].filter(isTruthy) ); diff --git a/webpack/manifest.ts b/webpack/manifest.ts index fa985076d..a7470932c 100644 --- a/webpack/manifest.ts +++ b/webpack/manifest.ts @@ -43,6 +43,14 @@ const buildManifestV3 = (vendor: string): Manifest.WebExtensionManifest => { ...commons, + web_accessible_resources: [ + { + matches: ['https://*/*'], + // Required for dynamic imports `import()` + resources: ['scripts/*.chunk.js'] + } + ], + permissions: PERMISSIONS, host_permissions: HOST_PERMISSIONS, @@ -72,6 +80,9 @@ const buildManifestV2 = (vendor: string): Manifest.WebExtensionManifest => { content_security_policy: "script-src 'self' 'unsafe-eval' blob:; object-src 'self'", + // Required for dynamic imports `import()` + web_accessible_resources: ['scripts/*.chunk.js'], + browser_action: buildBrowserAction(vendor), options_ui: { @@ -154,7 +165,7 @@ const buildManifestCommons = (vendor: string): Omit