diff --git a/cashtab/extension/src/contentscript.js b/cashtab/extension/src/contentscript.js --- a/cashtab/extension/src/contentscript.js +++ b/cashtab/extension/src/contentscript.js @@ -19,6 +19,7 @@ // 1 - A web page requests a Cashtab user's address // 2 - A web page requests opening a Cashtab window with a prepopulated transaction // Note we will pass every intercepted msg to service_worker.js to evaluate +// 3 - A web page requests a Cashtab user's contact list window.addEventListener( 'message', async function (event) { @@ -41,6 +42,8 @@ // Supported types // 1 - Extension pop-up window returning an address request approval / denial // 2 - The extension service_worker sending the address to the web page if approved +// 3 - Extension pop-up window returning a contact list request approval / denial +// 4 - The extension service_worker sending the contact list to the web page if approved extension.runtime.onMessage.addListener(message => { // Parse message for address if (typeof message.address !== 'undefined') { @@ -57,5 +60,20 @@ // We need to get the address from service_worker.js return extension.runtime.sendMessage(message); } + // Parse message for contacts + if (typeof message.contacts !== 'undefined') { + // Send as message that webpage can listen for + return window.postMessage( + { + type: 'FROM_CASHTAB', + contacts: message.contacts, + }, + '*', + ); + } + if (typeof message.contactsRequestApproved !== 'undefined') { + // We need to get the contacts from service_worker.js + return extension.runtime.sendMessage(message); + } return true; }); diff --git a/cashtab/extension/src/service_worker.js b/cashtab/extension/src/service_worker.js --- a/cashtab/extension/src/service_worker.js +++ b/cashtab/extension/src/service_worker.js @@ -3,7 +3,6 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. const extension = require('extensionizer'); - const NOTIFICATION_HEIGHT = 750; const NOTIFICATION_WIDTH = 450; const EXTENSION_DEV_ID = 'aleabaopoakgpbijdnicepefdiglggfl'; @@ -24,7 +23,7 @@ // so need to use this Promise.then() syntax getCurrentActiveTab().then( requestingTab => { - openAddressShareApproval('addressRequest', requestingTab); + openShareApproval('addressRequest', requestingTab); }, err => { console.log( @@ -47,6 +46,37 @@ handleDeniedAddressRequest(request.tabId); } } + + // Handle a contact list sharing request + if (request.text === `Cashtab` && request.contactsRequest) { + // get the tab this message came from + getCurrentActiveTab().then( + requestingTab => { + openShareApproval('contactsRequest', requestingTab); + }, + err => { + console.log( + 'Error in getCurrentActiveTab() triggered by contact list request', + err, + ); + }, + ); + } + // Handle user approval / rejection of a contact list sharing request + if ( + request.text === `Cashtab` && + Object.keys(request).includes('contactsRequestApproved') + ) { + // If approved, then share the contact list + if (request.contactsRequestApproved) { + (async () => { + await fetchContacts(request.tabId); + })(); + } else { + // If denied, let the webpage know that the user denied this request + handleDeniedContactsRequest(request.tabId); + } + } }); // Fetch item from extension storage and return it as a variable @@ -91,8 +121,27 @@ }); } -// Open Cashtab extension with a request for address sharing -async function openAddressShareApproval(request, tab) { +// Fetch the active extension contact list from extension storage API +async function fetchContacts(tabId) { + let fetchedContacts; + try { + fetchedContacts = await getObjectFromExtensionStorage(['contacts']); + } catch (err) { + console.error('Error retrieving contacts from extension storage.', err); + } + // Send this info back to the browser + extension.tabs.sendMessage(Number(tabId), { contacts: fetchedContacts }); +} + +// Sends a message back to the web app indicating the request rejection +async function handleDeniedContactsRequest(tabId) { + extension.tabs.sendMessage(Number(tabId), { + address: 'Contact list sharing request denied by user', + }); +} + +// Open Cashtab extension with a request for address or contact list sharing +async function openShareApproval(request, tab) { let left = 0; let top = 0; try { diff --git a/cashtab/src/components/AppModes/Extension.js b/cashtab/src/components/AppModes/Extension.js --- a/cashtab/src/components/AppModes/Extension.js +++ b/cashtab/src/components/AppModes/Extension.js @@ -6,6 +6,7 @@ import Modal from 'components/Common/Modal'; import extension from 'extensionizer'; import PropTypes from 'prop-types'; +import localforage from 'localforage'; const Extension = ({ wallet }) => { // Extension-only state fields @@ -13,6 +14,10 @@ useState(false); const [addressRequestTabId, setAddressRequestTabId] = useState(null); const [addressRequestTabUrl, setAddressRequestTabUrl] = useState(''); + const [showApproveContactsShareModal, setShowApproveContactsShareModal] = + useState(false); + const [contactsRequestTabId, setContactsRequestTabId] = useState(null); + const [contactsRequestTabUrl, setContactsRequestTabUrl] = useState(''); // Extension storage get method const getObjectFromExtensionStorage = async function (key) { @@ -52,6 +57,36 @@ }); }; + // Updates the contact list in extension storage with the localforage instance + // if it does not exist yet + const copyContactListToExtensionStorage = async () => { + let contactList = await localforage.getItem('contactList'); + + // Check for stored value + let storedConactList; + try { + storedConactList = await getObjectFromExtensionStorage([ + 'contacts', + ]); + } catch (err) { + console.log('Error retrieving contacts from extension storage'); + } + if (contactList === storedConactList) { + // No need to store it again + return; + } + + // If the contact list has not been set (or if the user has changed wallets since it was last set), set it + await extension.storage.sync.set( + { contacts: contactList }, + function () { + console.info( + `Contact list saved to storage under key 'contacts'`, + ); + }, + ); + }; + const handleApprovedAddressShare = async () => { await extension.tabs.sendMessage(addressRequestTabId, { type: 'FROM_CASHTAB', @@ -78,9 +113,41 @@ window.close(); }; + // Notify the extension the user has approved the sharing of contacts + const handleApprovedContactsShare = async () => { + await extension.tabs.sendMessage(contactsRequestTabId, { + type: 'FROM_CASHTAB', + text: 'Cashtab', + url: contactsRequestTabUrl, + tabId: contactsRequestTabId, + contactsRequestApproved: true, + }); + setShowApproveContactsShareModal(false); + // Close the popup after user action + window.close(); + }; + + // Notify the extension the user has rejected the sharing of contacts + const handleRejectedContactsShare = async () => { + await extension.tabs.sendMessage(contactsRequestTabId, { + type: 'FROM_CASHTAB', + text: 'Cashtab', + url: contactsRequestTabUrl, + contactsRequestApproved: false, + tabId: contactsRequestTabId, + }); + setShowApproveContactsShareModal(false); + // Close the popup after user action + window.close(); + }; + useEffect(() => { // On wallet change copyAddressToExtensionStorage(wallet); + // Async block needed to await within useEffect + (async () => { + await copyContactListToExtensionStorage(); + })(); }, [wallet]); useEffect(() => { @@ -104,14 +171,19 @@ let request = queryStringParams.get('request'); let tabId = parseInt(queryStringParams.get('tabId')); let tabUrl = queryStringParams.get('tabUrl'); - if (request !== 'addressRequest') { - return; - } // Open a modal that asks for user approval - setAddressRequestTabId(tabId); - setAddressRequestTabUrl(tabUrl); - setShowApproveAddressShareModal(true); + if (request === 'addressRequest') { + setAddressRequestTabId(tabId); + setAddressRequestTabUrl(tabUrl); + setShowApproveAddressShareModal(true); + } else if (request === 'contactsRequest') { + setContactsRequestTabId(tabId); + setContactsRequestTabUrl(tabUrl); + setShowApproveContactsShareModal(true); + } else { + return; + } } catch (err) { // If you can't parse this, forget about it return; @@ -132,6 +204,16 @@ showCancelButton /> )} + {showApproveContactsShareModal && ( + handleApprovedContactsShare()} + handleCancel={() => handleRejectedContactsShare()} + showCancelButton + /> + )} ); };