diff --git a/packages/extension-koni/public/images/icons/__error__.png b/packages/extension-koni/public/images/icons/__error__.png new file mode 100644 index 0000000000..401159f480 Binary files /dev/null and b/packages/extension-koni/public/images/icons/__error__.png differ diff --git a/packages/extension-koni/src/content.ts b/packages/extension-koni/src/content.ts index 6dee5d1591..81ec6d6ea5 100644 --- a/packages/extension-koni/src/content.ts +++ b/packages/extension-koni/src/content.ts @@ -1,58 +1,129 @@ // Copyright 2019-2022 @polkadot/extension authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { Message } from '@subwallet/extension-base/types'; + import { TransportRequestMessage } from '@subwallet/extension-base/background/types'; import { MESSAGE_ORIGIN_CONTENT, MESSAGE_ORIGIN_PAGE, PORT_CONTENT } from '@subwallet/extension-base/defaults'; -import { Message } from '@subwallet/extension-base/types'; import { getId } from '@subwallet/extension-base/utils/getId'; +import { addNotificationPopUp } from '@subwallet/extension-koni/helper/PageNotification'; -// connect to the extension -const port = chrome.runtime.connect({ name: PORT_CONTENT }); - -// redirect users if this page is considered as phishing, otherwise return false const handleRedirectPhishing: { id: string, resolve?: (value: (boolean | PromiseLike)) => void, reject?: (e: Error) => void } = { id: 'redirect-phishing-' + getId() }; -const redirectIfPhishingProm = new Promise((resolve, reject) => { - handleRedirectPhishing.resolve = resolve; - handleRedirectPhishing.reject = reject; +function checkForLastError () { + const { lastError } = chrome.runtime; + + if (!lastError) { + return undefined; + } + + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message); +} + +export class ContentHandler { + port?: chrome.runtime.Port; + isShowNotification = false; + isConnected = false; - const transportRequestMessage: TransportRequestMessage<'pub(phishing.redirectIfDenied)'> = { - id: handleRedirectPhishing.id, - message: 'pub(phishing.redirectIfDenied)', - origin: MESSAGE_ORIGIN_PAGE, - request: null - }; + // Get the port to communicate with the background and init handlers + getPort (): chrome.runtime.Port { + if (!this.port) { + const port = chrome.runtime.connect({ name: PORT_CONTENT }); + const onMessageHandler = this.onPortMessageHandler.bind(this); - port.postMessage(transportRequestMessage); -}); + const disconnectHandler = () => { + this.onDisconnectPort(port, onMessageHandler, disconnectHandler); + }; -// send any messages from the extension back to the page -port.onMessage.addListener((data: {id: string, response: any}): void => { - const { id, resolve } = handleRedirectPhishing; + this.port = port; + this.port.onMessage.addListener(onMessageHandler); + this.port.onDisconnect.addListener(disconnectHandler); + } - if (data?.id === id) { - resolve && resolve(Boolean(data.response)); - } else { - window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*'); + return this.port; } -}); -// // all messages from the page, pass them to the extension -window.addEventListener('message', ({ data, source }: Message): void => { - // only allow messages from our window, by the inject - if (source !== window || data.origin !== MESSAGE_ORIGIN_PAGE) { - return; + // Handle messages from the background + onPortMessageHandler (data: {id: string, response: any}): void { + const { id, resolve } = handleRedirectPhishing; + + if (data?.id === id) { + resolve && resolve(Boolean(data.response)); + } else { + window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*'); + } } - port.postMessage(data); -}); + // Handle disconnecting the port from the background + onDisconnectPort (port: chrome.runtime.Port, onMessage: (data: {id: string, response: any}) => void, onDisconnect: () => void): void { + const err = checkForLastError(); + + if (err) { + console.warn(`${err.message}, port is disconnected.`); + } + + port.onMessage.removeListener(onMessage); + port.onDisconnect.removeListener(onDisconnect); -redirectIfPhishingProm.then((gotRedirected) => { - if (!gotRedirected) { - console.log('Check phishing by URL: Passed.'); + this.port = undefined; } -}).catch((e) => { - console.warn(`Unable to determine if the site is in the phishing list: ${(e as Error).message}`); -}); + + // Handle messages from the webpage + onPageMessage ({ data, source }: Message): void { + // only allow messages from our window, by the inject + if (source !== window || data.origin !== MESSAGE_ORIGIN_PAGE) { + return; + } + + try { + this.isConnected = true; + this.getPort().postMessage(data); + } catch (e) { + console.error(e); + + if (!this.isShowNotification) { + console.log('The OpenBit extension is not installed. Please install the extension to use the wallet.'); + addNotificationPopUp(); + this.isShowNotification = true; + + setTimeout(() => { + this.isShowNotification = false; + }, 5000); + } + } + } + + // Detect phishing by URL + redirectIfPhishingProm (): void { + new Promise((resolve, reject) => { + handleRedirectPhishing.resolve = resolve; + handleRedirectPhishing.reject = reject; + + const transportRequestMessage: TransportRequestMessage<'pub(phishing.redirectIfDenied)'> = { + id: handleRedirectPhishing.id, + message: 'pub(phishing.redirectIfDenied)', + origin: MESSAGE_ORIGIN_PAGE, + request: null + }; + + this.getPort().postMessage(transportRequestMessage); + }).then((gotRedirected) => { + if (!gotRedirected) { + console.log('Check phishing by URL: Passed.'); + } + }).catch((e) => { + console.warn(`Unable to determine if the site is in the phishing list: ${(e as Error).message}`); + }); + } + + constructor () { + this.redirectIfPhishingProm(); + window.addEventListener('message', this.onPageMessage.bind(this)); + } +} + +// @ts-ignore +const contentHandler = new ContentHandler(); diff --git a/packages/extension-koni/src/helper/PageNotification.ts b/packages/extension-koni/src/helper/PageNotification.ts new file mode 100644 index 0000000000..43f72bb2f6 --- /dev/null +++ b/packages/extension-koni/src/helper/PageNotification.ts @@ -0,0 +1,73 @@ +// Copyright 2019-2022 @polkadot/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +const unableConnectImageSrc = chrome.runtime.getURL('/images/icons/__error__.png'); + +export function removeNotificationPopup () { + const divContainerExisted = document.getElementById('__notification-container'); + + divContainerExisted && divContainerExisted.remove(); +} + +export function addNotificationPopUp () { + removeNotificationPopup(); + + const divContainer = document.createElement('div'); + const divBox = document.createElement('div'); + const imgElement = document.createElement('img'); + const divContent = document.createElement('div'); + const styleElement = document.createElement('style'); + + const notificationContainerStyles: Partial = { + position: 'fixed', + top: '5%', + zIndex: '10001', + width: '100%', + animation: 'slideDown 5s ease-in-out' + }; + + const notificationBoxStyles: Partial = { + borderRadius: '8px', + margin: 'auto', + width: 'fit-content', + backgroundColor: 'black', + alignItems: 'center', + border: '2px solid #BF1616', + display: 'flex', + gap: '8px', + padding: '8px 16px 8px 16px' + }; + + const notificationContentStyles: Partial = { + fontFamily: 'inherit', + fontSize: '14px', + fontStyle: 'normal', + color: 'rgba(255, 255, 255, 0.85)', + fontWeight: '500', + lineHeight: '22px' + }; + + const keyframes = `@keyframes slideDown { + 0% { transform: translateY(-100%); opacity: 0; } + 20% { transform: translateY(0); opacity: 1; } + 95% { transform: translateY(0); opacity: 1; } + 100% { transform: translateY(-100%); opacity: 0; } + }`; + + Object.assign(divContent.style, notificationContentStyles); + Object.assign(divContainer.style, notificationContainerStyles); + Object.assign(divBox.style, notificationBoxStyles); + + divContainer.id = '__notification-container'; + imgElement.src = unableConnectImageSrc; + divContent.innerText = 'Unable to connect. Reload dApp site and try again.'; + styleElement.innerHTML = keyframes; + + document.head.appendChild(styleElement); + unableConnectImageSrc !== 'chrome-extension://invalid/' && divBox.appendChild(imgElement); + divBox.appendChild(divContent); + divContainer.appendChild(divBox); + document.body.appendChild(divContainer); + + setTimeout(removeNotificationPopup, 5000); +}