From c598dd68b5d529c404ec7f1a752e4cd75d5493d9 Mon Sep 17 00:00:00 2001 From: Michael Kelly Date: Wed, 24 Oct 2018 11:49:39 -0700 Subject: [PATCH] Fix #205, fix #198: Include static vendor info in add-on. In the future we can fill in the rest of the vendor duck to support non-allowlisted sites. --- src/background/extraction.js | 8 +-- src/background/price_alerts.js | 5 +- src/browser_action/components/ProductCard.jsx | 10 +++- src/state/migrations.js | 10 ++++ src/state/products.js | 3 - src/state/vendors.js | 60 +++++++++++++++++++ 6 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 src/state/vendors.js diff --git a/src/background/extraction.js b/src/background/extraction.js index d7fdfa4..79eb90b 100644 --- a/src/background/extraction.js +++ b/src/background/extraction.js @@ -21,13 +21,7 @@ import {validatePropType} from 'commerce/utils'; * @param {MessageSender} sender * The sender for the content script that extracted this product */ -export async function handleExtractedProductData(message, sender) { - // Fetch the favicon, which the content script cannot do itself. - const extractedProduct = { - ...message.extractedProduct, - vendorFaviconUrl: sender.tab ? sender.tab.favIconUrl : '', - }; - +export async function handleExtractedProductData({extractedProduct}, sender) { // Do nothing if the extracted product is missing fields. const result = validatePropType(extractedProduct, extractedProductShape); if (result !== undefined) { diff --git a/src/background/price_alerts.js b/src/background/price_alerts.js index cb5432d..82e23fd 100644 --- a/src/background/price_alerts.js +++ b/src/background/price_alerts.js @@ -21,6 +21,7 @@ import { showPriceAlert, } from 'commerce/state/prices'; import {getProduct} from 'commerce/state/products'; +import {getVendor} from 'commerce/state/vendors'; import {recordEvent} from 'commerce/telemetry/extension'; /** @@ -48,6 +49,8 @@ export function handlePriceAlerts() { const product = getProduct(state, alert.productId); const originalPrice = getOldestPriceForProduct(state, alert.productId); const highPriceAmount = Dinero({amount: alert.highPriceAmount}); + const vendor = getVendor(state, product.url); + const vendorName = vendor ? vendor.name : new URL(product.url).hostname; // Display notification const original = originalPrice.amount.toFormat('$0.00'); @@ -56,7 +59,7 @@ export function handlePriceAlerts() { browser.notifications.create(alert.priceId, { type: 'basic', title: `Price Alert: ${product.title}`, - message: `Placeholder · Originally ${original}, high of ${high}, now ${now}`, + message: `${vendorName} · Originally ${original}, high of ${high}, now ${now}`, }); // Update state now that we've shown it diff --git a/src/browser_action/components/ProductCard.jsx b/src/browser_action/components/ProductCard.jsx index 1b0ada9..b7a1d9c 100644 --- a/src/browser_action/components/ProductCard.jsx +++ b/src/browser_action/components/ProductCard.jsx @@ -16,6 +16,7 @@ import { } from 'commerce/state/prices'; import {productShape} from 'commerce/state/products'; import * as productActions from 'commerce/state/products'; +import {getVendor, vendorShape} from 'commerce/state/vendors'; import {recordEvent} from 'commerce/telemetry/extension'; import 'commerce/browser_action/components/ProductCard.css'; @@ -28,6 +29,7 @@ import 'commerce/browser_action/components/ProductCard.css'; latestPrice: getLatestPriceForProduct(state, props.product.id), originalPrice: getOldestPriceForProduct(state, props.product.id), activePriceAlert: getActivePriceAlertForProduct(state, props.product.id), + vendor: getVendor(state, props.product.url), }), { setDeletionFlag: productActions.setDeletionFlag, @@ -44,6 +46,7 @@ export default class ProductCard extends React.Component { latestPrice: priceWrapperShape.isRequired, originalPrice: priceWrapperShape.isRequired, activePriceAlert: priceAlertShape, + vendor: vendorShape, // Dispatch props setDeletionFlag: pt.func.isRequired, @@ -51,6 +54,7 @@ export default class ProductCard extends React.Component { static defaultProps = { activePriceAlert: null, + vendor: null, } /** @@ -89,7 +93,7 @@ export default class ProductCard extends React.Component { } render() { - const {activePriceAlert, latestPrice, originalPrice, product} = this.props; + const {activePriceAlert, latestPrice, originalPrice, product, vendor} = this.props; if (product.isDeleted) { return ( @@ -131,8 +135,8 @@ export default class ProductCard extends React.Component {

{product.title}

- {product.vendorFaviconUrl && ( - + {vendor && vendor.faviconUrl && ( + )} { + const {vendorFaviconUrl, ...newProduct} = product; + return newProduct; + }), + }; + }, ]; // Actions diff --git a/src/state/products.js b/src/state/products.js index 155840b..c97be0d 100644 --- a/src/state/products.js +++ b/src/state/products.js @@ -23,7 +23,6 @@ export const productShape = pt.shape({ url: pt.string.isRequired, image: pt.string.isRequired, isDeleted: pt.bool.isRequired, - vendorFaviconUrl: pt.string.isRequired, anonId: pt.string.isRequired, }); @@ -38,7 +37,6 @@ export const extractedProductShape = pt.shape({ image: pt.string.isRequired, price: pt.number.isRequired, date: pt.string.isRequired, - vendorFaviconUrl: pt.string, }); @@ -196,7 +194,6 @@ export function getProductFromExtracted(data, anonId) { title: data.title, url: data.url, image: data.image, - vendorFaviconUrl: data.vendorFaviconUrl || '', isDeleted: false, anonId, }; diff --git a/src/state/vendors.js b/src/state/vendors.js new file mode 100644 index 0000000..b4f9dcc --- /dev/null +++ b/src/state/vendors.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Redux duck for vendors that sell products. + * @module + */ + +import pt from 'prop-types'; + +// Types + +export const vendorShape = pt.shape({ + name: pt.string.isRequired, + hostnames: pt.arrayOf(pt.string).isRequired, + faviconUrl: pt.string, +}); + +// Eventually we will dynamically extract vendor data, but for now it's +// hard-coded. +const VENDORS = [ + { + name: 'Amazon', + hostnames: ['amazon.com', 'www.amazon.com', 'smile.amazon.com'], + faviconUrl: 'https://www.amazon.com/favicon.ico', + }, + { + name: 'Best Buy', + hostnames: ['bestbuy.com', 'www.bestbuy.com'], + faviconUrl: 'https://www.bestbuy.com/favicon.ico', + }, + { + name: 'eBay', + hostnames: ['ebay.com', 'www.ebay.com'], + faviconUrl: 'https://www.ebay.com/favicon.ico', + }, + { + name: 'The Home Depot', + hostnames: ['homedepot.com', 'www.homedepot.com'], + faviconUrl: 'https://www.homedepot.com/favicon.ico', + }, + { + name: 'Walmart', + hostnames: ['walmart.com', 'www.walmart.com'], + faviconUrl: 'https://www.walmart.com/favicon.ico', + }, + { + name: 'mkelly Test', + hostnames: ['mkelly.me', 'www.mkelly.me'], + faviconUrl: '', + }, +]; + +// Selectors + +export function getVendor(state, url) { + const hostname = new URL(url).hostname; + return VENDORS.find(vendor => vendor.hostnames.includes(hostname)); +}