From a2db1fa242e9f378fa3a3b0a0ee9a826b6618b25 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 19 Nov 2020 16:27:47 -0700 Subject: [PATCH] Add periodic tracking and periodic validation for payment pointers. --- src/content_main.js | 167 +++++++++++++++++++++------- src/data/AkitaOriginData.js | 17 ++- src/data/AkitaPaymentPointerData.js | 18 +++ src/data/storage.js | 5 +- 4 files changed, 162 insertions(+), 45 deletions(-) diff --git a/src/content_main.js b/src/content_main.js index 20c69bd..601e6d0 100644 --- a/src/content_main.js +++ b/src/content_main.js @@ -1,3 +1,6 @@ +const NEW_PAYMENT_POINTER_CHECK_RATE_MS = 1000; // 1 second +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! @@ -32,17 +35,6 @@ main(); * Main function to initiate the application. */ async function main() { - // TODO: check payment pointer periodically for existence and validity - const { - isValid, - paymentPointer - } = await getAndValidatePaymentPointer(); - - setExtensionIconMonetizationState(isValid); - - // paymentPointer will be null if it doesn't exist or is invalid - await storeDataIntoAkitaFormat({ paymentPointer: paymentPointer }, AKITA_DATA_TYPE.PAYMENT); - // Test storing assets // await storeDataIntoAkitaFormat({ // paymentPointer: paymentPointer, @@ -58,6 +50,8 @@ async function main() { // storeDataIntoAkitaFormat(null, AKITA_DATA_TYPE.PAYMENT); //}); + trackPaymentPointer(); + handleUpdatingExtensionIconOnTabVisibilityChange(); await trackTimeOnSite(); await trackVisitToSite(); @@ -77,8 +71,11 @@ async function main() { * If true, a pink $ badge is displayed. If false, just the dog face without the tongue is used as the icon. */ function setExtensionIconMonetizationState(isCurrentlyMonetized) { - const webBrowser = chrome ? chrome : browser; - webBrowser.runtime.sendMessage({ isCurrentlyMonetized }); + const tabIsVisible = !document.hidden; + if (tabIsVisible) { + const webBrowser = chrome ? chrome : browser; + webBrowser.runtime.sendMessage({ isCurrentlyMonetized }); + } } /*********************************************************** @@ -114,12 +111,6 @@ async function trackTimeOnSite() { } else { // The page is now visible docVisibleTime = getCurrentTime(); - - const { - isValid, - paymentPointer - } = await getAndValidatePaymentPointer(); - setExtensionIconMonetizationState(isValid); } }); @@ -171,42 +162,134 @@ async function storeRecentTimeSpent(recentTimeSpent) { } /*********************************************************** - * Validate Payment Pointer + * Track and Validate Payment Pointer ***********************************************************/ /** - * Check for a monetization meta tag on the website and verify that - * the payment pointer is valid (resolves to a valid SPSP endpoint). - * - * TODO: use enum to indicate no meta tag, meta tag + valid endpoint, - * meta tag + invalid endpoint. - * - * @return {Promise<{ isPaymentPointerValid: boolean, paymentPointer:string}>} - * isPaymentPointerValid is true if both monetization is present and the payment endpoint is valid. - * paymentPointer is the paymentPointer if it is found in the monetization meta tag, otherwise null. + * getPaymentPointerFromPage 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 + * + * @return {string|null} The payment pointer on the current page, or null if no meta tag is present. */ -async function getAndValidatePaymentPointer() { - const monetizationMeta = document.querySelector('meta[name="monetization"]'); - let paymentPointer = (monetizationMeta) ? monetizationMeta.content : null; - let isValid = false; +function getPaymentPointerFromPage() { + const monetizationMetaTag = document.querySelector('meta[name="monetization"]'); + if (monetizationMetaTag) { + return monetizationMetaTag.content; + } else { + return null; + } +} + +/** + * trackPaymentPointer checks repeatedly (every NEW_PAYMENT_POINTER_CHECK_RATE_MS milliseconds) + * to see if a new payment pointer is added to the page. If a new payment pointer is added to the + * page, handleValidationAndSetExtensionIcon is called to + * - ensure the payment pointer is validated, + * - store the payment pointer in storage, + * - and update the extension icon to the correct image. + */ +function trackPaymentPointer() { + let cachedPaymentPointer = null; + + setInterval(async () => { + const paymentPointerOnPage = getPaymentPointerFromPage(); + + const isNewPaymentPointer = + (paymentPointerOnPage !== null) && + (paymentPointerOnPage !== cachedPaymentPointer); + if (isNewPaymentPointer) { + // Note that paymentPointerOnPage may be newly added to the current page and tab, + // but it is possible that this payment pointer was recently validated in another tab. + // Therefore it is necessary to use handleValidationAndSetExtensionIcon which checks the + // browser storage to see if the payment pointer has been recently validated. + handleValidationAndSetExtensionIcon(paymentPointerOnPage); + cachedPaymentPointer = paymentPointerOnPage; + } + }, NEW_PAYMENT_POINTER_CHECK_RATE_MS); +} - if (null === monetizationMeta) { - /* No monetization meta tag provided */ +/** + * handleUpdatingExtensionIconOnTabVisibilityChange ensures that the extension icon is updated + * to the correct state when switching between tabs. + */ +function handleUpdatingExtensionIconOnTabVisibilityChange() { + document.addEventListener('visibilitychange', (event) => { + const tabIsVisible = !document.hidden; + if (tabIsVisible) { + const paymentPointerOnPage = getPaymentPointerFromPage(); + if (paymentPointerOnPage !== null) { + handleValidationAndSetExtensionIcon(paymentPointerOnPage); + } + } + }); +} + +/** + * Takes a payment pointer and validates it if it has not been validated recently. If the payment + * pointer is sucessfuly validated then the extension icon is set to its monetized state, otherwise + * it is set to its unmonetized state. + * + * @param {string} paymentPointer the payment pointer to validate. Extension icon is set to its + * monetized image if the validation is successful. + * @returns {Promise<>} a promise that resolves when validation is finished. + */ +async function handleValidationAndSetExtensionIcon(paymentPointer) { + if (await isPaymentPointerRecentlyValidated(paymentPointer)) { + setExtensionIconMonetizationState(true); } else { if (await isPaymentPointerValid(paymentPointer)) { - isValid = true; + // isPaymentPointerValid validates the payment pointer, so we store the payment + // pointer and the UTC timestamp for the time it was validated at (now). + await storeDataIntoAkitaFormat({ + paymentPointer: paymentPointer, + validationTimestamp: Date.now() + }, AKITA_DATA_TYPE.PAYMENT); + setExtensionIconMonetizationState(true); + } else { + setExtensionIconMonetizationState(false); } } +} + +/** + * Checks the browser storage to see if Akita has validated the given payment pointer and stored a + * validation timestamp for it. + * Returns true if there is a timestamp retrieved from storage and the + * timestamp is recent (less than PAYMENT_POINTER_VALIDATION_RATE_MS from present time). + * Returns false otherwise. + * + * @param {string} paymentPointer check if this payment pointer was recently validated by Akita. + * @returns {Promise} true if the given payment pointer was recently validated by Akita, + * false otherwise. + */ +async function isPaymentPointerRecentlyValidated(paymentPointer) { + const originData = await loadOriginData(window.location.origin); + let validationTimestamp = null; + // get validationTimestamp from originData under the given payment pointer. + if (originData && originData.paymentPointerMap[paymentPointer]) { + validationTimestamp = + originData.paymentPointerMap[paymentPointer].validationTimestamp; + } + if (validationTimestamp === null) { + return false; + } - return { - isValid, - paymentPointer - }; + // check if validationTimestamp is recent. + const timeSinceLastValidated = Date.now() - validationTimestamp; + if (timeSinceLastValidated < PAYMENT_POINTER_VALIDATION_RATE_MS) { + return true; + } else { + return false; + } } /** - * Check if a payment pointer is valid or not. - * + * Check if a payment pointer is valid (resolves to a valid SPSP endpoint). + * For more information on payment pointer resolution and validation see: + * - https://paymentpointers.org/syntax-resolution/#requirements + * - https://interledger.org/rfcs/0009-simple-payment-setup-protocol + * * @param {string} paymentPointer The paymentPointer found in a meta tag. * @return {Promise} Whether or not the specified payment pointer is valid. */ diff --git a/src/data/AkitaOriginData.js b/src/data/AkitaOriginData.js index 93f6f6a..cae70f7 100644 --- a/src/data/AkitaOriginData.js +++ b/src/data/AkitaOriginData.js @@ -54,6 +54,7 @@ class AkitaOriginData { /** * @param {{ * paymentPointer: String, + * validationTimestamp: Number, * amount?: Number, * assetScale?: Number, * assetCode?: String @@ -61,7 +62,9 @@ class AkitaOriginData { * This object may be created, or a Web Monetization event detail object can be used. * Pass in an object with just a paymentPointer to register a payment pointer for * the current website. Payment pointer should be validated first. - * Additionally pass in assetCode, assetScale, and amount together to add to the + * Optionally pass in validationTimestamp to set when the payment pointer was most + * recently validated. + * Optionally pass in assetCode, assetScale, and amount together to add to the * total amount sent to the current website. * * assetCode e.g. 'XRP', 'USD', 'CAD' @@ -74,7 +77,7 @@ class AkitaOriginData { updatePaymentData(paymentData) { if (paymentData) { const paymentPointer = paymentData.paymentPointer; - + if (paymentPointer) { this.isCurrentlyMonetized = true; @@ -82,6 +85,16 @@ class AkitaOriginData { this.paymentPointerMap[paymentPointer] = new AkitaPaymentPointerData(paymentPointer); } + // Handling for optional argument: validationTimestamp + const validationTimestamp = paymentData.validationTimestamp; + + if (validationTimestamp) { + this.paymentPointerMap[paymentPointer].setValidationTimestamp( + validationTimestamp + ); + } + + // Handling for the 3 optional arguments: amount, assetScale, and assetCode const amount = paymentData.amount; const assetScale = paymentData.assetScale; const assetCode = paymentData.assetCode; diff --git a/src/data/AkitaPaymentPointerData.js b/src/data/AkitaPaymentPointerData.js index f14bd01..98650b6 100644 --- a/src/data/AkitaPaymentPointerData.js +++ b/src/data/AkitaPaymentPointerData.js @@ -4,6 +4,9 @@ */ class AkitaPaymentPointerData { paymentPointer = null; + // The most recent time (UTC timestamp) when Akita validated the payment pointer + // For more info on payment pointer validation: see ./main.js, function isPaymentPointerValid + validationTimestamp = null; // The type of each entry in sentAssetsMap is: WebMonetizationAsset sentAssetsMap = {}; @@ -21,6 +24,7 @@ class AkitaPaymentPointerData { */ static fromObject(akitaPaymentPointerDataObject) { const newPaymentPointerData = new AkitaPaymentPointerData(akitaPaymentPointerDataObject.paymentPointer); + newPaymentPointerData.validationTimestamp = akitaPaymentPointerDataObject.validationTimestamp; for (const assetCode in akitaPaymentPointerDataObject.sentAssetsMap) { newPaymentPointerData.sentAssetsMap[assetCode] = WebMonetizationAsset.fromObject( @@ -30,6 +34,20 @@ class AkitaPaymentPointerData { return newPaymentPointerData; } + /** + * It is expected that payment pointers are validated by Akita often. In order to make sure + * that validation occurs often, we keep track of the last time the payment pointer was + * validated. When Akita validates a payment pointer, the time it was validated can be + * set using this function. + * + * For more info on payment pointer validation: see ./main.js, function isPaymentPointerValid + * + * @param {Number} validationTimestamp UTC Timestamp of last time payment pointer was validated. + */ + setValidationTimestamp(validationTimestamp) { + this.validationTimestamp = validationTimestamp; + } + /** * assetScale and amount e.g. * if assetCode is 'USD', amount is 235, and assetScale is 3 then actual amount of diff --git a/src/data/storage.js b/src/data/storage.js index 3ce66e0..c583c27 100644 --- a/src/data/storage.js +++ b/src/data/storage.js @@ -85,11 +85,12 @@ async function storeDataIntoAkitaFormat(data, typeOfData) { /** * Update payment data in originData and in originStats. - * + * * @param {AkitaOriginData} originData The origin data to update. * @param {AkitaOriginStats} originStats The origin stats to update. * @param {{ * paymentPointer: String, + * validationTimestamp: Number, * amount?: Number, * assetScale?: Number, * assetCode?: String @@ -97,6 +98,8 @@ async function storeDataIntoAkitaFormat(data, typeOfData) { * This object may be created, or a Web Monetization event detail object can be used. * Pass in an object with just a paymentPointer to register a payment pointer for * the current website. Payment pointer should be validated first. + * Optionally pass in validationTimestamp to set when the payment pointer was most + * recently validated. * Additionally pass in assetCode, assetScale, and amount together to add to the * total amount sent to the current website. */