From 303136528c9f036bf4dd1dc847c6a73ab019d4b5 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:42:11 +0530 Subject: [PATCH] fix: update popup data along with icon - refactor(PopupState): add all popup data related to tab under `tab` - `tabUrl` -> `tab.url` - `isSiteMonetized` -> `tab.status === 'monetized'` - `hasAllSessionsInvalid` -> `tab.status === 'all_sessions_invalid'` - refactor(monetization): extract `getPopupTabData` to tabState service - refactor(tabEvents): use `getPopupTabData` instead of multiple params Paves way to show more useful messages with `tab.status` (e.g. new tab, internal extension pages, non-https pages) --- src/background/constants.ts | 15 ++++++ src/background/services/background.ts | 4 +- src/background/services/monetization.ts | 24 +--------- src/background/services/tabEvents.ts | 64 +++++++++---------------- src/background/services/tabState.ts | 46 +++++++++++++++++- src/background/utils.ts | 9 ++++ src/popup/components/PayWebsiteForm.tsx | 4 +- src/popup/lib/context.tsx | 9 ++-- src/popup/pages/Home.tsx | 18 ++++--- src/shared/messages.ts | 3 +- src/shared/types.ts | 27 +++++++++-- 11 files changed, 133 insertions(+), 90 deletions(-) create mode 100644 src/background/constants.ts diff --git a/src/background/constants.ts b/src/background/constants.ts new file mode 100644 index 00000000..14e648f8 --- /dev/null +++ b/src/background/constants.ts @@ -0,0 +1,15 @@ +// cSpell:ignore newtab, webui, startpage + +export const INTERNAL_PAGE_URL_PROTOCOLS = new Set([ + 'chrome:', + 'about:', + 'edge:', +]); + +export const NEW_TAB_PAGES = [ + 'about:blank', + 'chrome://newtab', + 'about:newtab', + 'edge://newtab', + 'chrome://vivaldi-webui/startpage', +]; diff --git a/src/background/services/background.ts b/src/background/services/background.ts index c2cc48a4..a215adf4 100644 --- a/src/background/services/background.ts +++ b/src/background/services/background.ts @@ -291,7 +291,7 @@ export class Background { private async updateVisualIndicatorsForCurrentTab() { const activeTab = await this.windowState.getCurrentTab(); if (activeTab?.id) { - void this.tabEvents.updateVisualIndicators(activeTab.id, activeTab.url); + void this.tabEvents.updateVisualIndicators(activeTab); } } @@ -308,7 +308,7 @@ export class Background { this.events.on('monetization.state_update', async (tabId) => { const tab = await this.browser.tabs.get(tabId); - void this.tabEvents.updateVisualIndicators(tabId, tab?.url); + void this.tabEvents.updateVisualIndicators(tab); }); this.events.on('storage.balance_update', (balance) => diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index 55a90ee8..2d0b0dc6 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -13,7 +13,6 @@ import { } from '../utils'; import { isOutOfBalanceError } from './openPayments'; import { isOkState, removeQueryParams } from '@/shared/helpers'; -import { ALLOWED_PROTOCOLS } from '@/shared/defines'; import type { AmountValue, PopupStore, Storage } from '@/shared/types'; import type { Cradle } from '../container'; @@ -388,35 +387,14 @@ export class MonetizationService { const { oneTimeGrant, recurringGrant, ...dataFromStorage } = storedData; - const tabId = tab.id; - if (!tabId) { - throw new Error('Tab ID not found'); - } - let url; - if (tab && tab.url) { - try { - const tabUrl = new URL(tab.url); - if (ALLOWED_PROTOCOLS.includes(tabUrl.protocol)) { - // Do not include search params - url = `${tabUrl.origin}${tabUrl.pathname}`; - } - } catch { - // noop - } - } - const isSiteMonetized = this.tabState.isTabMonetized(tabId); - const hasAllSessionsInvalid = this.tabState.tabHasAllSessionsInvalid(tabId); - return { ...dataFromStorage, balance: balance.total.toString(), - url, + tab: this.tabState.getPopupTabData(tab), grants: { oneTime: oneTimeGrant?.amount, recurring: recurringGrant?.amount, }, - isSiteMonetized, - hasAllSessionsInvalid, }; } diff --git a/src/background/services/tabEvents.ts b/src/background/services/tabEvents.ts index bc25f8e4..e87b5ce9 100644 --- a/src/background/services/tabEvents.ts +++ b/src/background/services/tabEvents.ts @@ -1,6 +1,5 @@ import { isOkState, removeQueryParams } from '@/shared/helpers'; -import { ALLOWED_PROTOCOLS } from '@/shared/defines'; -import type { Storage, TabId } from '@/shared/types'; +import type { PopupTabInfo, Storage, TabId } from '@/shared/types'; import type { Browser, Tabs } from 'webextension-polyfill'; import type { Cradle } from '@/background/container'; @@ -93,7 +92,8 @@ export class TabEvents { if (clearOverpaying) { this.tabState.clearOverpayingByTabId(tabId); } - void this.updateVisualIndicators(tabId, url); + if (!tab.id) return; + void this.updateVisualIndicators(tab); } }; @@ -108,13 +108,13 @@ export class TabEvents { const updated = this.windowState.setCurrentTabId(info.windowId, info.tabId); if (!updated) return; const tab = await this.browser.tabs.get(info.tabId); - await this.updateVisualIndicators(info.tabId, tab?.url); + await this.updateVisualIndicators(tab); }; onCreatedTab: CallbackTab<'onCreated'> = async (tab) => { if (!tab.id) return; this.windowState.addTab(tab.id, tab.windowId); - await this.updateVisualIndicators(tab.id, tab.url); + await this.updateVisualIndicators(tab); }; onFocussedTab = async (tab: Tabs.Tab) => { @@ -122,40 +122,24 @@ export class TabEvents { this.windowState.addTab(tab.id, tab.windowId); const updated = this.windowState.setCurrentTabId(tab.windowId!, tab.id); if (!updated) return; - const tabUrl = tab.url ?? (await this.browser.tabs.get(tab.id)).url; - await this.updateVisualIndicators(tab.id, tabUrl); + await this.updateVisualIndicators(tab); }; - updateVisualIndicators = async ( - tabId: TabId, - tabUrl?: string, - isTabMonetized: boolean = tabId - ? this.tabState.isTabMonetized(tabId) - : false, - hasTabAllSessionsInvalid: boolean = tabId - ? this.tabState.tabHasAllSessionsInvalid(tabId) - : false, - ) => { - const canMonetizeTab = ALLOWED_PROTOCOLS.some((scheme) => - tabUrl?.startsWith(scheme), - ); + updateVisualIndicators = async (tab: Tabs.Tab) => { + const tabInfo = this.tabState.getPopupTabData(tab); + this.sendToPopup.send('SET_TAB_DATA', tabInfo); const { enabled, connected, state } = await this.storage.get([ 'enabled', 'connected', 'state', ]); - const { path, title, isMonetized } = this.getIconAndTooltip({ + const { path, title } = this.getIconAndTooltip({ enabled, connected, state, - canMonetizeTab, - isTabMonetized, - hasTabAllSessionsInvalid, + tabInfo, }); - - this.sendToPopup.send('SET_IS_MONETIZED', isMonetized); - this.sendToPopup.send('SET_ALL_SESSIONS_INVALID', hasTabAllSessionsInvalid); - await this.setIconAndTooltip(tabId, path, title); + await this.setIconAndTooltip(tabInfo.tabId, path, title); }; private setIconAndTooltip = async ( @@ -187,26 +171,28 @@ export class TabEvents { enabled, connected, state, - canMonetizeTab, - isTabMonetized, - hasTabAllSessionsInvalid, + tabInfo, }: { enabled: Storage['enabled']; connected: Storage['connected']; state: Storage['state']; - canMonetizeTab: boolean; - isTabMonetized: boolean; - hasTabAllSessionsInvalid: boolean; + tabInfo: PopupTabInfo; }) { let title = this.t('appName'); let iconData = ICONS.default; - if (!connected || !canMonetizeTab) { + if (!connected) { // use defaults - } else if (!isOkState(state) || hasTabAllSessionsInvalid) { + } else if (!isOkState(state) || tabInfo.status === 'all_sessions_invalid') { iconData = enabled ? ICONS.enabled_warn : ICONS.disabled_warn; const tabStateText = this.t('icon_state_actionRequired'); title = `${title} - ${tabStateText}`; + } else if ( + tabInfo.status !== 'monetized' && + tabInfo.status !== 'no_monetization_links' + ) { + // use defaults } else { + const isTabMonetized = tabInfo.status === 'monetized'; if (enabled) { iconData = isTabMonetized ? ICONS.enabled_hasLinks @@ -222,10 +208,6 @@ export class TabEvents { title = `${title} - ${tabStateText}`; } - return { - path: iconData, - isMonetized: isTabMonetized, - title, - }; + return { path: iconData, title }; } } diff --git a/src/background/services/tabState.ts b/src/background/services/tabState.ts index b218cb1e..27831e9d 100644 --- a/src/background/services/tabState.ts +++ b/src/background/services/tabState.ts @@ -1,7 +1,11 @@ +import type { Tabs } from 'webextension-polyfill'; import type { MonetizationEventDetails } from '@/shared/messages'; -import type { TabId } from '@/shared/types'; +import type { PopupTabInfo, TabId } from '@/shared/types'; import type { PaymentSession } from './paymentSession'; import type { Cradle } from '@/background/container'; +import { removeQueryParams } from '@/shared/helpers'; +import { ALLOWED_PROTOCOLS } from '@/shared/defines'; +import { isBrowserInternalPage, isBrowserNewTabPage } from '@/background/utils'; type State = { monetizationEvent: MonetizationEventDetails; @@ -119,6 +123,46 @@ export class TabState { return [...this.sessions.values()].flatMap((s) => [...s.values()]); } + getPopupTabData(tab: Pick): PopupTabInfo { + if (!tab.id) { + throw new Error('Tab does not have an ID'); + } + + let tabUrl: URL | null = null; + try { + tabUrl = new URL(tab.url ?? ''); + } catch { + // noop + } + + let url = ''; + if (tabUrl && ALLOWED_PROTOCOLS.includes(tabUrl.protocol)) { + // Do not include search params + url = removeQueryParams(tabUrl.href); + } + + let status: PopupTabInfo['status'] = 'no_monetization_links'; + if (!tabUrl) { + status = 'unsupported_scheme'; + } else if (!ALLOWED_PROTOCOLS.includes(tabUrl.protocol)) { + if (tabUrl && isBrowserInternalPage(tabUrl)) { + if (isBrowserNewTabPage(tabUrl)) { + status = 'new_tab'; + } else { + status = 'internal_page'; + } + } else { + status = 'unsupported_scheme'; + } + } else if (this.tabHasAllSessionsInvalid(tab.id)) { + status = 'all_sessions_invalid'; + } else if (this.isTabMonetized(tab.id)) { + status = 'monetized'; + } + + return { tabId: tab.id, url, status }; + } + getIcon(tabId: TabId) { return this.currentIcon.get(tabId); } diff --git a/src/background/utils.ts b/src/background/utils.ts index dbe19c82..0e08fc05 100644 --- a/src/background/utils.ts +++ b/src/background/utils.ts @@ -6,6 +6,7 @@ import type { } from '@/shared/types'; import type { Browser, Runtime } from 'webextension-polyfill'; import { DEFAULT_SCALE, EXCHANGE_RATES_URL } from './config'; +import { INTERNAL_PAGE_URL_PROTOCOLS, NEW_TAB_PAGES } from './constants'; import { notNullOrUndef } from '@/shared/helpers'; export const getCurrentActiveTab = async (browser: Browser) => { @@ -97,6 +98,14 @@ export const getSender = (sender: Runtime.MessageSender) => { return { tabId, frameId, url: sender.url }; }; +export const isBrowserInternalPage = (url: URL) => { + return INTERNAL_PAGE_URL_PROTOCOLS.has(url.protocol); +}; + +export const isBrowserNewTabPage = (url: URL) => { + return NEW_TAB_PAGES.some((e) => url.href.startsWith(e)); +}; + export const computeRate = (rate: string, sessionsCount: number): AmountValue => (BigInt(rate) / BigInt(sessionsCount)).toString(); diff --git a/src/popup/components/PayWebsiteForm.tsx b/src/popup/components/PayWebsiteForm.tsx index 3be8cef7..312f9330 100644 --- a/src/popup/components/PayWebsiteForm.tsx +++ b/src/popup/components/PayWebsiteForm.tsx @@ -26,7 +26,7 @@ const BUTTON_STATE = { export const PayWebsiteForm = () => { const message = useMessage(); const { - state: { walletAddress, url }, + state: { walletAddress, tab }, } = usePopupState(); const [buttonState, setButtonState] = React.useState('idle'); @@ -84,7 +84,7 @@ export const PayWebsiteForm = () => { addOn={getCurrencySymbol(walletAddress.assetCode)} label={

- Pay {url} + Pay {tab.url}

} placeholder="0.00" diff --git a/src/popup/lib/context.tsx b/src/popup/lib/context.tsx index a89a840e..685f3ba0 100644 --- a/src/popup/lib/context.tsx +++ b/src/popup/lib/context.tsx @@ -69,12 +69,10 @@ const reducer = (state: PopupState, action: ReducerActions): PopupState => { return { ...state, rateOfPay: action.data.rateOfPay }; case 'SET_STATE': return { ...state, state: action.data.state }; - case 'SET_IS_MONETIZED': - return { ...state, isSiteMonetized: action.data }; + case 'SET_TAB_DATA': + return { ...state, tab: action.data }; case 'SET_BALANCE': return { ...state, balance: action.data.total }; - case 'SET_ALL_SESSIONS_INVALID': - return { ...state, hasAllSessionsInvalid: action.data }; default: return state; } @@ -109,8 +107,7 @@ export function PopupContextProvider({ children }: PopupContextProviderProps) { switch (message.type) { case 'SET_BALANCE': case 'SET_STATE': - case 'SET_IS_MONETIZED': - case 'SET_ALL_SESSIONS_INVALID': + case 'SET_TAB_DATA': return dispatch(message); } }); diff --git a/src/popup/pages/Home.tsx b/src/popup/pages/Home.tsx index 908b0682..b92196a5 100644 --- a/src/popup/pages/Home.tsx +++ b/src/popup/pages/Home.tsx @@ -19,14 +19,12 @@ export const Component = () => { const { state: { enabled, - isSiteMonetized, rateOfPay, minRateOfPay, maxRateOfPay, balance, walletAddress, - url, - hasAllSessionsInvalid, + tab, }, dispatch, } = usePopupState(); @@ -65,12 +63,12 @@ export const Component = () => { dispatch({ type: 'TOGGLE_WM', data: {} }); }; - if (!isSiteMonetized) { - return ; - } - - if (hasAllSessionsInvalid) { - return ; + if (tab.status !== 'monetized') { + if (tab.status === 'all_sessions_invalid') { + return ; + } else { + return ; + } } return ( @@ -113,7 +111,7 @@ export const Component = () => {
- {url ? : null} + {tab.url ? : null} ); }; diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 3de9345b..1f7672b2 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -241,9 +241,8 @@ export const BACKGROUND_TO_POPUP_CONNECTION_NAME = 'popup'; // These methods are fire-and-forget, nothing is returned. export interface BackgroundToPopupMessagesMap { SET_BALANCE: Record<'recurring' | 'oneTime' | 'total', AmountValue>; - SET_IS_MONETIZED: boolean; + SET_TAB_DATA: PopupState['tab']; SET_STATE: { state: Storage['state']; prevState: Storage['state'] }; - SET_ALL_SESSIONS_INVALID: boolean; } export type BackgroundToPopupMessage = { diff --git a/src/shared/types.ts b/src/shared/types.ts index 2baccda2..c34be612 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -85,6 +85,29 @@ export interface Storage { } export type StorageKey = keyof Storage; +export type PopupTabInfo = { + tabId: TabId; + url: string; + status: + | never // just added for code formatting + /** Happy state */ + | 'monetized' + /** No monetization links or all links disabled */ + | 'no_monetization_links' + /** New tab */ + | 'new_tab' + /** Browser internal pages */ + | 'internal_page' + /** Not https:// */ + | 'unsupported_scheme' + /** + * All wallet addresses belong to wallets that are not peered with the + * connected wallet, or cannot receive payments for some other reason. + */ + | 'all_sessions_invalid' + | never; // just added for code formatting +}; + export type PopupStore = Omit< Storage, | 'version' @@ -95,13 +118,11 @@ export type PopupStore = Omit< | 'oneTimeGrant' > & { balance: AmountValue; - isSiteMonetized: boolean; - url: string | undefined; + tab: PopupTabInfo; grants?: Partial<{ oneTime: OneTimeGrant['amount']; recurring: RecurringGrant['amount']; }>; - hasAllSessionsInvalid: boolean; }; export type DeepNonNullable = {