Skip to content

Commit

Permalink
Add support for monetization in iframes
Browse files Browse the repository at this point in the history
  • Loading branch information
vezwork committed Dec 30, 2020
1 parent b4cdb93 commit fb18491
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 105 deletions.
18 changes: 13 additions & 5 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,33 @@
},
"permissions":[
"tabs",
"<all_urls>",
"storage"
],
"background": {
"scripts": ["src/background_script.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>", "http://*/*", "https://*/*"],
"matches": ["http://*/*", "https://*/*"],
"js": [
"src/data/WebMonetizationAsset.js",
"src/data/AkitaPaymentPointerData.js",
"src/data/AkitaOriginVisitData.js",
"src/data/AkitaOriginData.js",
"src/data/AkitaOriginStats.js",
"src/data/storage.js",
"src/content_main.js",
"src/main.js"
]
"src/content_scripts/content_general.js"
],
"all_frames":true
},
{
"matches": ["http://*/*", "https://*/*"],
"js": ["src/content_scripts/content_origin.js"]
},
{
"matches": ["http://*/*", "https://*/*"],
"js": ["src/content_scripts/content_iframe.js"],
"all_frames":true
}
],
"browser_action": {
Expand Down
32 changes: 21 additions & 11 deletions src/background_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,29 @@ const UNMONETIZED_ICON_PATH = '../assets/icon_unmonetized.png';
// Listen to messages sent from Content Scripts via webBrowser.runtime.sendMessage
webBrowser.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
// Set browser badge and icon based on requests sent from content_origin.js
if (request.isCurrentlyMonetized) {
webBrowser.browserAction.setBadgeBackgroundColor({ color: '#EF5E92' });
webBrowser.browserAction.setBadgeText({ text: '$' });
webBrowser.browserAction.setIcon({
path: MONETIZED_ICON_PATH,
const isCurrentlyMonetized = request.isCurrentlyMonetized.value;
if (isCurrentlyMonetized) {
webBrowser.browserAction.setBadgeBackgroundColor({ color: '#EF5E92' });
webBrowser.browserAction.setBadgeText({ text: '$' });
webBrowser.browserAction.setIcon({
path: MONETIZED_ICON_PATH,
tabId: sender.tab.id
});
} else {
webBrowser.browserAction.setBadgeText({ text: '' }); // Hide the badge
webBrowser.browserAction.setIcon({
path: UNMONETIZED_ICON_PATH,
tabId: sender.tab.id
});
} else {
webBrowser.browserAction.setBadgeText({ text: '' }); // Hide the badge
webBrowser.browserAction.setIcon({
path: UNMONETIZED_ICON_PATH,
tabId: sender.tab.id
});
});
}
return;
}
// Forward messages from content_iframe.js scripts to content_origin.js
if (request.iframePaymentPointerChange || request.iframeReceivedUuid) {
chrome.tabs.sendMessage(sender.tab.id, request);
return;
}
}
);
Expand Down
54 changes: 54 additions & 0 deletions src/content_scripts/content_general.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/***********************************************************
* content_general.js runs both at the top level page (alongside content_origin.js), and
* in iframes (alongside content_iframe.js); it handles forwarding web monetization events into
* akita events.
***********************************************************/

const NEW_PAYMENT_POINTER_CHECK_RATE_MS = 1000; // 1 seconds
const PAYMENT_POINTER_VALIDATION_RATE_MS = 1000 * 60 * 60; // 1 hour

/**
* Content scripts can only see a "clean version" of the DOM, i.e. a version of the DOM without
* properties which are added by JavaScript, such as document.monetization!
* reference: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#Content_script_environment
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts
* So we must inject some code into the JavaScript context of the current tab in order to
* access the document.monetization object. We inject code using a script element:
*/
const scriptEl = document.createElement('script');
scriptEl.text = `
if (document.monetization) {
document.monetization.addEventListener('monetizationstart', (event) => {
document.dispatchEvent(new CustomEvent('akita_monetizationstart', { detail: event.detail }));
});
document.monetization.addEventListener('monetizationprogress', (event) => {
document.dispatchEvent(new CustomEvent('akita_monetizationprogress', { detail: event.detail }));
});
document.monetization.addEventListener('monetizationstop', (event) => {
document.dispatchEvent(new CustomEvent('akita_monetizationstop', { detail: event.detail }));
});
}
`;
document.body.appendChild(scriptEl);

/**
* Gets the payment pointer as a string from the monetization meta tag on the current page.
* If there is no monetization meta tag on the page, null is returned.
*
* For more info on the monetization meta tag:
* - https://webmonetization.org/docs/getting-started
* - https://webmonetization.org/specification.html#meta-tags-set
*
* @return {string|null} The payment pointer on the current page, or null if no meta tag is present.
*/
function getPaymentPointerFromPage() {
const monetizationMetaTag = document.querySelector('meta[name="monetization"]');

if (monetizationMetaTag) {
return monetizationMetaTag.content;
} else {
return null;
}
}
84 changes: 84 additions & 0 deletions src/content_scripts/content_iframe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/***********************************************************
* content_iframe.js runs inside of each iframe and tracks and notifies content_origin.js of
* payment pointer changes in the iframe via the `webBrowser.runtime` message channel. Each
* content_iframe.js in each iframe is assigned a uuid from content_origin.js via postMessage so
* that content_origin.js can know which iframe the script is associated with when the script sends
* messages.
***********************************************************/

// window.isTopLevel is set to true in ./content_origin.js, which only runs in the top level page;
// so if window.isTopLevel is not set to true, then this script is running in an iframe. All code
// in content_iframe.js should be inside this if block.
if (!window.isTopLevel) {

let topLevelOrigin = null;
let monetizationPaymentEvents = [];

document.addEventListener('akita_monetizationprogress', (event) => {
const monetizationPaymentEvent = event.detail;

// Payment events may be received by the iframe before the top level page communicates the
// topLevelOrigin to the iframe. However, since payment events are stored under the
// topLevelOrigin, the topLevelOrigin needs to be received before payment events are saved.
// Until a topLevelOrigin is received, payment events are put into an array for later storage.
if (topLevelOrigin === null) {
monetizationPaymentEvents.push(monetizationPaymentEvent);
} else {
storeDataIntoAkitaFormat(monetizationPaymentEvent, AKITA_DATA_TYPE.PAYMENT, topLevelOrigin);
}
});


// Listen for a message from the top level page (./content_origin.js).
// The iframe expects to receive a uuid and origin from the top level page soon after the iframe
// is created.
window.addEventListener('message', (event) => {
const data = event.data;

if (data.uuid && data.topLevelOrigin) {
// Set topLevelOrigin so that iframe monetization events can be stored under this origin
// and store any payment events that occured before receiving the topLevelOrigin.
topLevelOrigin = data.topLevelOrigin;
for (const monetizationPaymentEvent of monetizationPaymentEvents) {
storeDataIntoAkitaFormat(monetizationPaymentEvent, AKITA_DATA_TYPE.PAYMENT, topLevelOrigin);
}
monetizationPaymentEvents = [];

const iframeUuid = data.uuid;
// Let the top level page know that the iframe has receieved the uuid
webBrowser.runtime.sendMessage({
iframeReceivedUuid: {
iframeUuid: iframeUuid
}
});
trackIframePaymentPointer(iframeUuid);
}
});


/**
* Regularly check for payment pointer changes in the iframe. If the payment pointer changes,
* the top level page (./content_origin.js) is notified via ./background_script.js, which forwards
* the `webBrowser.runtime.sendMessage` iframePaymentPointerChange message.
*
* @param {string} iframeUuid the uuid given to the iframe by top level page.
*/
function trackIframePaymentPointer(iframeUuid) {
let cachedPaymentPointer = null;

setInterval(async () => {
const paymentPointerInIframe = getPaymentPointerFromPage();

// If the payment pointer in iframe changes, send new payment pointer to content_origin.js
if (paymentPointerInIframe !== cachedPaymentPointer) {
webBrowser.runtime.sendMessage({
iframePaymentPointerChange: {
iframeUuid: iframeUuid,
paymentPointer: paymentPointerInIframe
}
});
cachedPaymentPointer = paymentPointerInIframe;
}
}, NEW_PAYMENT_POINTER_CHECK_RATE_MS);
}
}

0 comments on commit fb18491

Please sign in to comment.