Skip to content

Commit

Permalink
fix: update popup data along with icon
Browse files Browse the repository at this point in the history
refactor: add all popup data related to tab under tabKey
  - tabUrl -> tab.url
  - isSiteMonetized -> tab.walletAddresses.length > 0
  - hasAllSessionsInvalid -> tab.cannotMonetizeReason === 'all_sessions_invalid'

refactor(monetization): extract `getPopupTabData` to tabState service
refactor(tabEvents): use getPopupTabData instead of multiple params

Paves way for showing more useful messages with tab.cannotMonetizeReason
  (e.g. new tab, internal extension pages, non-https pages)

Allows showing in popup which wallet addresses are being paid (if needed)
  • Loading branch information
sidvishnoi committed Sep 19, 2024
1 parent ac4c96c commit 1e282c1
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 86 deletions.
15 changes: 15 additions & 0 deletions src/background/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// cSpell:ignore newtab, webui, startpage
export const DEFAULT_SCALE = 2;
export const DEFAULT_INTERVAL_MS = 3_600_000;

Expand All @@ -7,3 +8,17 @@ export const MAX_RATE_OF_PAY = '100';

export const EXCHANGE_RATES_URL =
'https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json';

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',
];
4 changes: 2 additions & 2 deletions src/background/services/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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) =>
Expand Down
24 changes: 1 addition & 23 deletions src/background/services/monetization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/background/services/paymentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ export class PaymentSession {
return this.isInvalid;
}

get info() {
const { id, assetCode, assetScale } = this.receiver;
return {
id,
assetCode,
assetScale,
};
}

disable() {
this.isDisabled = true;
this.stop();
Expand Down
66 changes: 25 additions & 41 deletions src/background/services/tabEvents.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
}
};

Expand All @@ -108,54 +108,38 @@ 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) => {
if (!tab.id) return;
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 (
Expand Down Expand Up @@ -187,26 +171,30 @@ 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 ||
(tabInfo.cannotMonetizeReason &&
tabInfo.cannotMonetizeReason !== 'all_sessions_invalid')
) {
// use defaults
} else if (!isOkState(state) || hasTabAllSessionsInvalid) {
} else if (
!isOkState(state) ||
tabInfo.cannotMonetizeReason === 'all_sessions_invalid'
) {
iconData = enabled ? ICONS.enabled_warn : ICONS.disabled_warn;
const tabStateText = this.t('icon_state_actionRequired');
title = `${title} - ${tabStateText}`;
} else {
const isTabMonetized = tabInfo.walletAddresses.length > 0;
if (enabled) {
iconData = isTabMonetized
? ICONS.enabled_hasLinks
Expand All @@ -222,10 +210,6 @@ export class TabEvents {
title = `${title} - ${tabStateText}`;
}

return {
path: iconData,
isMonetized: isTabMonetized,
title,
};
return { path: iconData, title };
}
}
49 changes: 48 additions & 1 deletion src/background/services/tabState.ts
Original file line number Diff line number Diff line change
@@ -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 { INTERNAL_PAGE_URL_PROTOCOLS, NEW_TAB_PAGES } from '../config';

type State = {
monetizationEvent: MonetizationEventDetails;
Expand Down Expand Up @@ -119,6 +123,49 @@ export class TabState {
return [...this.sessions.values()].flatMap((s) => [...s.values()]);
}

getPopupTabData(tab: Pick<Tabs.Tab, 'id' | 'url'>): 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 cannotMonetizeReason: PopupTabInfo['cannotMonetizeReason'] = null;
if (!tabUrl) {
cannotMonetizeReason = 'unsupported_scheme';
} else if (!ALLOWED_PROTOCOLS.includes(tabUrl.protocol)) {
if (tabUrl && INTERNAL_PAGE_URL_PROTOCOLS.has(tabUrl.protocol)) {
if (NEW_TAB_PAGES.some((url) => tabUrl.href.startsWith(url))) {
cannotMonetizeReason = 'new_tab';
} else {
cannotMonetizeReason = 'internal_page';
}
} else {
cannotMonetizeReason = 'unsupported_scheme';
}
} else if (this.tabHasAllSessionsInvalid(tab.id)) {
cannotMonetizeReason = 'all_sessions_invalid';
}

return {
tabId: tab.id,
url,
cannotMonetizeReason,
walletAddresses: this.getPayableSessions(tab.id).map((e) => e.info),
};
}

getIcon(tabId: TabId) {
return this.currentIcon.get(tabId);
}
Expand Down
4 changes: 2 additions & 2 deletions src/popup/components/PayWebsiteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<keyof typeof BUTTON_STATE>('idle');
Expand Down Expand Up @@ -84,7 +84,7 @@ export const PayWebsiteForm = () => {
addOn={getCurrencySymbol(walletAddress.assetCode)}
label={
<p className="overflow-hidden text-ellipsis whitespace-nowrap">
Pay <span className="text-ellipsis text-primary">{url}</span>
Pay <span className="text-ellipsis text-primary">{tab.url}</span>
</p>
}
placeholder="0.00"
Expand Down
9 changes: 3 additions & 6 deletions src/popup/lib/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
});
Expand Down
10 changes: 4 additions & 6 deletions src/popup/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ export const Component = () => {
const {
state: {
enabled,
isSiteMonetized,
rateOfPay,
minRateOfPay,
maxRateOfPay,
balance,
walletAddress,
url,
hasAllSessionsInvalid,
tab,
},
dispatch,
} = usePopupState();
Expand Down Expand Up @@ -65,11 +63,11 @@ export const Component = () => {
dispatch({ type: 'TOGGLE_WM', data: {} });
};

if (!isSiteMonetized) {
if (!tab.walletAddresses.length) {
return <SiteNotMonetized />;
}

if (hasAllSessionsInvalid) {
if (tab.cannotMonetizeReason === 'all_sessions_invalid') {
return <AllSessionsInvalid />;
}

Expand Down Expand Up @@ -113,7 +111,7 @@ export const Component = () => {

<hr />

{url ? <PayWebsiteForm /> : null}
{tab.url ? <PayWebsiteForm /> : null}
</div>
);
};
3 changes: 1 addition & 2 deletions src/shared/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading

0 comments on commit 1e282c1

Please sign in to comment.