**Summary** Adds a card for migrated Review Checker notifying users can move the sidebar to the left or the right side. Visibility is controlled by two factors: - browser.shopping.experience2023.newPositionCard.hasSeen - if true, the user already saw the card and we don't have to show it again. If false (default), make sure to show the card once we're able to. - browser.shopping.experience2023.integratedSidebar - must be true, since this enables Review Checker in the sidebar and allows us to use ReviewChecker actors There's also three actors in play: **ReviewCheckerManager** - Responsible for the sidebar's visibility and auto-open behaviour. We only show the notification card on auto-open and a PDP, so we check if we can render the notification card in this file. - It also handles behaviour for moving the sidebar position, or showing the sidebar settings panel. - However, because ReviewCheckerManager is instantiated before the ReviewChecker parent and child pair actors, we have to wait for RC parent to be created before RC manager can communicate with it. **ReviewCheckerParent** - Once created, it communicates with ReviewCheckerManager, so that we know if we should render the notification card or not. - If it receives a response back from RC manager, it will send a message to its RC child counterpart. - Otherwise, if it receives a message from RC child to move the sidebar position or show the sidebar settings panel, communicate with RC manager again to do the appropriate action. **ReviewCheckerChild** - It receives a message from ReviewCheckerParent to know if we should show the notification card. If yes, then send an event to the content (shopping-container) - It also listens for and handles events from content (shopping-container), like if a user presses the move position buttons, or wants to see the sidebar settings panel. Once an event is detected for either action from content, send a message to ReviewCheckerParent, and then RC manager by extension, to make the appropriate action. **Follow-up work** There's another patch https://phabricator.services.mozilla.com/D239083 that updates how the notification card and keep closed message behave, ensuring they don't visually conflict with each other. Differential Revision: https://phabricator.services.mozilla.com/D238089
821 lines
22 KiB
JavaScript
821 lines
22 KiB
JavaScript
/* 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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs";
|
|
|
|
import {
|
|
ShoppingProduct,
|
|
isProductURL,
|
|
isSupportedSiteURL,
|
|
} from "chrome://global/content/shopping/ShoppingProduct.mjs";
|
|
|
|
let lazy = {};
|
|
|
|
let gAllActors = new Set();
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"optedIn",
|
|
"browser.shopping.experience2023.optedIn",
|
|
null,
|
|
function optedInStateChanged() {
|
|
for (let actor of gAllActors) {
|
|
actor.optedInStateChanged();
|
|
}
|
|
}
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"adsEnabled",
|
|
"browser.shopping.experience2023.ads.enabled",
|
|
true
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"adsEnabledByUser",
|
|
"browser.shopping.experience2023.ads.userEnabled",
|
|
true,
|
|
function adsEnabledByUserChanged() {
|
|
for (let actor of gAllActors) {
|
|
actor.adsEnabledByUserChanged();
|
|
}
|
|
}
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"autoOpenEnabled",
|
|
"browser.shopping.experience2023.autoOpen.enabled",
|
|
true
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"autoOpenEnabledByUser",
|
|
"browser.shopping.experience2023.autoOpen.userEnabled",
|
|
true,
|
|
function autoOpenEnabledByUserChanged() {
|
|
for (let actor of gAllActors) {
|
|
actor.autoOpenEnabledByUserChanged();
|
|
}
|
|
}
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"autoCloseEnabledByUser",
|
|
"browser.shopping.experience2023.autoClose.userEnabled",
|
|
true,
|
|
function autoCloseEnabledByUserChanged() {
|
|
for (let actor of gAllActors) {
|
|
actor.autoCloseEnabledByUserChanged();
|
|
}
|
|
}
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"isSidebarStartPosition",
|
|
"sidebar.position_start"
|
|
);
|
|
|
|
/**
|
|
* The ReviewCheckerChild will get the current URL from the parent
|
|
* and will request data to update the sidebar UI if that URL is a
|
|
* product or display the current opt-in or empty state.
|
|
*/
|
|
export class ReviewCheckerChild extends RemotePageChild {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
actorCreated() {
|
|
super.actorCreated();
|
|
gAllActors.add(this);
|
|
}
|
|
|
|
didDestroy() {
|
|
this._destroyed = true;
|
|
super.didDestroy?.();
|
|
gAllActors.delete(this);
|
|
this.#product?.uninit();
|
|
}
|
|
|
|
#currentURI = null;
|
|
#product = null;
|
|
#supportedDomains = null;
|
|
|
|
get currentURL() {
|
|
return this.#currentURI?.spec;
|
|
}
|
|
|
|
get canFetchAndShowData() {
|
|
return lazy.optedIn === 1;
|
|
}
|
|
|
|
get adsEnabled() {
|
|
return lazy.adsEnabled;
|
|
}
|
|
|
|
get adsEnabledByUser() {
|
|
return lazy.adsEnabledByUser;
|
|
}
|
|
|
|
get canFetchAndShowAd() {
|
|
return this.adsEnabled && this.adsEnabledByUser;
|
|
}
|
|
|
|
get autoOpenEnabled() {
|
|
return lazy.autoOpenEnabled;
|
|
}
|
|
|
|
get autoOpenEnabledByUser() {
|
|
return lazy.autoOpenEnabledByUser;
|
|
}
|
|
|
|
get autoCloseEnabledByUser() {
|
|
return lazy.autoCloseEnabledByUser;
|
|
}
|
|
|
|
get isSidebarStartPosition() {
|
|
return lazy.isSidebarStartPosition;
|
|
}
|
|
|
|
receiveMessage(message) {
|
|
if (this.browsingContext.usePrivateBrowsing) {
|
|
throw new Error("We should never be invoked in PBM.");
|
|
}
|
|
switch (message.name) {
|
|
case "ReviewChecker:UpdateCurrentURL":
|
|
this.locationChanged(message.data);
|
|
break;
|
|
case "ReviewChecker:ShowNewPositionCard":
|
|
this.sendToContent("ShowNewPositionCard", {
|
|
isSidebarStartPosition: this.isSidebarStartPosition,
|
|
});
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
handleEvent(event) {
|
|
let aid, sponsored, product;
|
|
switch (event.type) {
|
|
case "ContentReady":
|
|
this.resetContent();
|
|
this.setLocation();
|
|
break;
|
|
case "PolledRequestMade":
|
|
product = this.getProductForURI(this.#currentURI);
|
|
this.updateProductData(product, { isPolledRequest: true });
|
|
break;
|
|
case "ReportProductAvailable":
|
|
product = this.getProductForURI(this.#currentURI);
|
|
this.reportProductAvailable(product);
|
|
break;
|
|
case "AdClicked":
|
|
aid = event.detail.aid;
|
|
sponsored = event.detail.sponsored;
|
|
ShoppingProduct.sendAttributionEvent("click", aid);
|
|
Glean.shopping.surfaceAdsClicked.record({ sponsored });
|
|
break;
|
|
case "AdImpression":
|
|
aid = event.detail.aid;
|
|
sponsored = event.detail.sponsored;
|
|
ShoppingProduct.sendAttributionEvent("impression", aid);
|
|
Glean.shopping.surfaceAdsImpression.record({ sponsored });
|
|
break;
|
|
case "DisableShopping":
|
|
this.sendAsyncMessage("DisableShopping");
|
|
break;
|
|
case "CloseShoppingSidebar":
|
|
this.sendAsyncMessage("CloseShoppingSidebar");
|
|
break;
|
|
case "MoveSidebarToRight":
|
|
this.sendAsyncMessage("ReverseSidebarPosition");
|
|
break;
|
|
case "MoveSidebarToLeft":
|
|
this.sendAsyncMessage("ReverseSidebarPosition");
|
|
break;
|
|
case "ShowSidebarSettings":
|
|
this.sendAsyncMessage("ShowSidebarSettings");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exposed for testing to set the private currentURI.
|
|
*
|
|
* @param {nsIURI} uri
|
|
*/
|
|
set currentURI(uri) {
|
|
if (!(uri instanceof Ci.nsIURI)) {
|
|
throw new Error("currentURI setter expects an nsIURI");
|
|
}
|
|
this.#currentURI = uri;
|
|
}
|
|
|
|
/**
|
|
* Get the URI the content has been set to.
|
|
*
|
|
* @returns {nsIURI} currentURI
|
|
*/
|
|
get currentURI() {
|
|
return this.#currentURI;
|
|
}
|
|
|
|
/**
|
|
* Exposed for testing to set the private product.
|
|
*
|
|
* TODO: Bug 1927956 - This will need to get the URI for the product once we are
|
|
* caching them.
|
|
*
|
|
* @param {ShoppingProduct} product
|
|
*/
|
|
set product(product) {
|
|
if (!(product instanceof ShoppingProduct)) {
|
|
throw new Error("product setter expects an instance of ShoppingProduct");
|
|
}
|
|
this.#product = product;
|
|
}
|
|
|
|
/**
|
|
* Get or create a product for the given URI.
|
|
*
|
|
* @param {nsIURI} productURI
|
|
* @returns {ShoppingProduct}
|
|
*/
|
|
getProductForURI(productURI) {
|
|
// TODO: Bug 1927956 - Check a product is cached for this URI.
|
|
if (this.#product) {
|
|
return this.#product;
|
|
}
|
|
let product = new ShoppingProduct(productURI);
|
|
// TODO: Bug 1927956 - Add to product cache for this URI.
|
|
this.#product = product;
|
|
return product;
|
|
}
|
|
|
|
/**
|
|
* Check check if URIs represent the same product by
|
|
* comparing URLs and then parsed product ID.
|
|
*
|
|
* @param {nsIURI} newURI
|
|
* @param {nsIURI} currentURI
|
|
* @returns {boolean}
|
|
*/
|
|
isSameProduct(newURI, currentURI) {
|
|
if (!newURI || !currentURI) {
|
|
return false;
|
|
}
|
|
|
|
// Check if the URIs are equal:
|
|
if (currentURI.equalsExceptRef(newURI)) {
|
|
return true;
|
|
}
|
|
|
|
let product = this.getProductForURI(currentURI);
|
|
if (!product) {
|
|
return false;
|
|
}
|
|
|
|
// If the current ShoppingProduct has product info set,
|
|
// check if the product ids are the same:
|
|
let currentProduct = product.product;
|
|
if (currentProduct) {
|
|
let newProduct = ShoppingProduct.fromURL(URL.fromURI(newURI));
|
|
if (newProduct.id === currentProduct.id) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Reset the content and update for the current URI.
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async optedInStateChanged() {
|
|
// Clear the current content
|
|
this.resetContent({ focusCloseButton: true });
|
|
|
|
// Get a URI if we don't have one yet.
|
|
if (!this.#currentURI) {
|
|
await this.setLocation();
|
|
}
|
|
|
|
await this.updateContent(this.#currentURI);
|
|
}
|
|
|
|
/**
|
|
* Get recommendations for the current ShoppingProduct
|
|
* if enabled or remove current recommendations.
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async adsEnabledByUserChanged() {
|
|
this.updateAdsEnabledByUser(this.adsEnabledByUser);
|
|
|
|
if (!this.canFetchAndShowAd) {
|
|
return;
|
|
}
|
|
|
|
let product = this.getProductForURI(this.#currentURI);
|
|
await this.updateRecommendations(product);
|
|
}
|
|
|
|
/**
|
|
* Update auto-open to user's pref value.
|
|
*
|
|
*/
|
|
autoOpenEnabledByUserChanged() {
|
|
this.updateAutoOpenEnabledByUser(this.autoOpenEnabledByUser);
|
|
}
|
|
|
|
/**
|
|
* Update auto-close to user's pref value.
|
|
*
|
|
*/
|
|
autoCloseEnabledByUserChanged() {
|
|
this.updateAutoCloseEnabledByUser(this.autoCloseEnabledByUser);
|
|
}
|
|
|
|
/**
|
|
* Get current URL for the parent and update the location to it.
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async setLocation() {
|
|
// check if can fetch and show data
|
|
let url = await this.sendQuery("GetCurrentURL");
|
|
|
|
// Bail out if we opted out in the meantime, or don't have a URI.
|
|
if (!this.canContinue(null, false)) {
|
|
return;
|
|
}
|
|
|
|
await this.locationChanged({ url });
|
|
}
|
|
|
|
/**
|
|
* Update the currentURI to a new location.
|
|
*
|
|
* For a new location this will:
|
|
* - Reset remove the ShoppingProduct for the previous URI.
|
|
* - Update the content for the new URI.
|
|
*
|
|
* @param {object?} options
|
|
* @param {bool} options.url
|
|
* @param {bool} [options.isReload]
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async locationChanged({ url, isReload } = {}) {
|
|
let uri = url ? Services.io.newURI(url) : null;
|
|
|
|
// If we're going from null to null, bail out:
|
|
if (!this.#currentURI && !uri) {
|
|
return;
|
|
}
|
|
|
|
// If we haven't reloaded, check if the URIs represent the same product
|
|
// as sites might change the URI after they have loaded (Bug 1852099).
|
|
if (!isReload && this.isSameProduct(uri, this.#currentURI)) {
|
|
return;
|
|
}
|
|
|
|
this.#product?.uninit();
|
|
this.#product = null;
|
|
|
|
this.#currentURI = uri;
|
|
|
|
await this.updateContent(uri);
|
|
}
|
|
|
|
/**
|
|
* Re-renders the content whenever whenever the location changes.
|
|
*
|
|
* The expected cases for this are:
|
|
* - page navigations (both to new products and away from a product once
|
|
* the sidebar has been created)
|
|
* - opt in state changes.
|
|
*
|
|
* For a new location this will:
|
|
* - Check if the location is a product page or supported site.
|
|
* - Update the content location and empty states.
|
|
* - Get or create a new ShoppingProduct for the URI if needed.
|
|
* - Update state and data for that product.
|
|
* - Update recommendation for that product if enabled.
|
|
*
|
|
* @param {nsIURI} uri
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async updateContent(uri) {
|
|
if (this._destroyed || !uri) {
|
|
return;
|
|
}
|
|
|
|
let isProductPage = isProductURL(uri);
|
|
let isSupportedSite = isSupportedSiteURL(uri);
|
|
|
|
if (!this.canFetchAndShowData) {
|
|
this.showOnboarding({
|
|
productUrl: uri.spec,
|
|
isProductPage,
|
|
isSupportedSite,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isProductPage) {
|
|
let product = this.getProductForURI(uri);
|
|
|
|
// We want to update the location to clear out the content from
|
|
// the previous URL immediately, without waiting for potentially
|
|
// async operations like obtaining product information.
|
|
this.updateLocation({ isProductPage });
|
|
|
|
await this.updateProductData(product);
|
|
|
|
if (this.canShowAds(uri)) {
|
|
await this.updateRecommendations(product);
|
|
}
|
|
} else {
|
|
if (!this.#supportedDomains) {
|
|
this.#supportedDomains = ShoppingProduct.getSupportedDomains();
|
|
}
|
|
// If the URI is not a product page, we should display an empty state.
|
|
// That empty state could be for either a support or unsupported site.
|
|
this.updateLocation({
|
|
isProductPage,
|
|
isSupportedSite,
|
|
supportedDomains: this.#supportedDomains,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* updateContent is an async function, and when we're off making requests or doing
|
|
* other things asynchronously, the actor can be destroyed, the user
|
|
* might navigate to a new page, the user might disable the feature ... -
|
|
* all kinds of things can change. So we need to repeatedly check
|
|
* whether we can keep going with our async processes. This helper takes
|
|
* care of these checks.
|
|
*
|
|
* @param {nsIURI} currentURI
|
|
* @param {boolean} [checkURI] = true
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
canContinue(currentURI, checkURI = true) {
|
|
if (this._destroyed || !this.canFetchAndShowData) {
|
|
return false;
|
|
}
|
|
if (!checkURI) {
|
|
return true;
|
|
}
|
|
return currentURI && currentURI == this.#currentURI;
|
|
}
|
|
|
|
/**
|
|
* Utility function to determine if we should display ads. This is different
|
|
* from fetching ads, because of ads exposure telemetry (bug 1858470).
|
|
*
|
|
* @param {nsIURI} uri
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
canShowAds(uri) {
|
|
return (
|
|
uri.equalsExceptRef(this.#currentURI) &&
|
|
this.canFetchAndShowData &&
|
|
this.canFetchAndShowAd
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Async helper that will request data from the Fakespot API for a passed product
|
|
* and update the content with the new state.
|
|
*
|
|
* Records telemetry if reviews are not available for the product.
|
|
*
|
|
* @param {ShoppingProduct} product
|
|
* @param {object} options
|
|
* @param {boolean} [options.isPolledRequest=false]
|
|
*/
|
|
async updateProductData(product, { isPolledRequest = false } = {}) {
|
|
let uri = this.#currentURI;
|
|
let productUrl = uri?.spec;
|
|
let analysisStatusResponse;
|
|
let data;
|
|
try {
|
|
if (isPolledRequest) {
|
|
// Request a new analysis.
|
|
analysisStatusResponse = await product.requestCreateAnalysis();
|
|
} else {
|
|
// Request the current analysis status.
|
|
analysisStatusResponse = await product.requestAnalysisCreationStatus();
|
|
}
|
|
// Status will be "not_found" if the current analysis is up-to-date.
|
|
let analysisStatus = analysisStatusResponse?.status ?? "not_found";
|
|
|
|
let isAnalysisInProgress =
|
|
ShoppingProduct.isAnalysisInProgress(analysisStatus);
|
|
if (isAnalysisInProgress) {
|
|
// Do not clear data however if an analysis was requested via a call-to-action.
|
|
if (!isPolledRequest) {
|
|
this.updateAnalysisStatus({ analysisStatus }, productUrl);
|
|
}
|
|
|
|
analysisStatus = await this.waitForAnalysisCompleted(
|
|
product,
|
|
analysisStatus
|
|
);
|
|
}
|
|
|
|
this.updateAnalysisStatus({ analysisStatus }, productUrl);
|
|
|
|
let hasAnalysisCompleted =
|
|
ShoppingProduct.hasAnalysisCompleted(analysisStatus);
|
|
if (!hasAnalysisCompleted) {
|
|
return;
|
|
}
|
|
|
|
data = await product.requestAnalysis();
|
|
} catch (err) {
|
|
console.warn("Failed to fetch product analysis data", err);
|
|
data = { error: err };
|
|
}
|
|
|
|
// Check if we got nuked from orbit, or the product URI or opt in changed while we waited.
|
|
if (!data || !this.canContinue(uri)) {
|
|
return;
|
|
}
|
|
|
|
this.sendToContent("Update", {
|
|
productUrl,
|
|
data,
|
|
showOnboarding: false,
|
|
isSidebarStartPosition: this.isSidebarStartPosition,
|
|
});
|
|
|
|
if (!isPolledRequest && !data.error && !data.grade) {
|
|
Glean.shopping.surfaceNoReviewReliabilityAvailable.record();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Async helper that will request recommendation data from the Fakespot API for a passed product
|
|
* and update the content with the new state.
|
|
*
|
|
* Records telemetry if recommendation are placed or not available for the product.
|
|
*
|
|
* @param {ShoppingProduct} product
|
|
*/
|
|
async updateRecommendations(product) {
|
|
let uri = this.#currentURI;
|
|
let recommendationData;
|
|
try {
|
|
recommendationData = await product.requestRecommendations();
|
|
} catch (err) {
|
|
console.warn("Failed to fetch product recommendations data", err);
|
|
recommendationData = [];
|
|
}
|
|
// Check if the product URI or opt in changed while we waited.
|
|
if (!this.canShowAds(uri)) {
|
|
return;
|
|
}
|
|
|
|
if (!recommendationData.length) {
|
|
// We tried to fetch an ad, but didn't get one.
|
|
Glean.shopping.surfaceNoAdsAvailable.record();
|
|
} else {
|
|
let sponsored = recommendationData[0].sponsored;
|
|
|
|
ShoppingProduct.sendAttributionEvent(
|
|
"placement",
|
|
recommendationData[0].aid
|
|
);
|
|
|
|
Glean.shopping.surfaceAdsPlacement.record({
|
|
sponsored,
|
|
});
|
|
}
|
|
|
|
this.sendToContent("UpdateRecommendations", {
|
|
recommendationData,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Async helper that will report a product is now available to the Fakespot API.
|
|
*
|
|
* @param {ShoppingProduct} product
|
|
*/
|
|
async reportProductAvailable(product) {
|
|
await product.sendReport();
|
|
}
|
|
|
|
/**
|
|
* Async helper that will poll the Fakespot API until a product's analysis is no longer
|
|
* in progress, this could complete or fail as with a status.
|
|
*
|
|
* Callback updates the analysis progress as reported by the Fakespot API.
|
|
*
|
|
* @param {ShoppingProduct} product
|
|
*/
|
|
async waitForAnalysisCompleted(product, analysisStatus) {
|
|
try {
|
|
let analysisStatusResponse = await product.pollForAnalysisCompleted(
|
|
{
|
|
pollInitialWait: analysisStatus == "in_progress" ? 0 : undefined,
|
|
},
|
|
progress => {
|
|
this.sendToContent("UpdateAnalysisProgress", {
|
|
progress,
|
|
});
|
|
}
|
|
);
|
|
return analysisStatusResponse.status;
|
|
} catch (err) {
|
|
console.warn("Failed to get product status", err);
|
|
return analysisStatus;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Content state helper to send preference changes to the shopping-container
|
|
* and clear the current state.
|
|
*
|
|
* @param {object?} options
|
|
* @param {bool} [options.focusCloseButton=false]
|
|
*/
|
|
resetContent({ focusCloseButton = false } = {}) {
|
|
this.sendToContent("Update", {
|
|
adsEnabled: lazy.adsEnabled,
|
|
adsEnabledByUser: lazy.adsEnabledByUser,
|
|
autoOpenEnabled: lazy.autoOpenEnabled,
|
|
autoOpenEnabledByUser: lazy.autoOpenEnabledByUser,
|
|
autoCloseEnabledByUser: lazy.autoCloseEnabledByUser,
|
|
showOnboarding: !this.canFetchAndShowData,
|
|
data: null,
|
|
recommendationData: null,
|
|
focusCloseButton,
|
|
isSidebarStartPosition: this.isSidebarStartPosition,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Content state helper to send analysis status changes to the shopping-container.
|
|
*
|
|
* @param {object?} options
|
|
* @param {string} options.analysisStatus
|
|
* @param {string} productUrl the url of the analyzed product
|
|
*/
|
|
updateAnalysisStatus({ analysisStatus } = {}, productUrl) {
|
|
let data;
|
|
// Use the analysis status instead of re-requesting unnecessarily,
|
|
// or throw if the status from the last analysis was an error.
|
|
switch (analysisStatus) {
|
|
case "not_analyzable":
|
|
case "page_not_supported":
|
|
data = { page_not_supported: true };
|
|
break;
|
|
case "not_enough_reviews":
|
|
data = { not_enough_reviews: true };
|
|
break;
|
|
case "unprocessable":
|
|
case "stale":
|
|
data = { error: new Error(analysisStatus, { cause: analysisStatus }) };
|
|
break;
|
|
default:
|
|
// Status is "completed" or "not_found" (no analysis status),
|
|
// so we should request the analysis data.
|
|
}
|
|
|
|
let isAnalysisInProgress =
|
|
ShoppingProduct.isAnalysisInProgress(analysisStatus);
|
|
if (
|
|
(!data && !isAnalysisInProgress) ||
|
|
this.#currentURI.spec !== productUrl
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.sendToContent("Update", {
|
|
data,
|
|
isAnalysisInProgress,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Content state helper to update the current location in the content.
|
|
*
|
|
* @param {object?} options
|
|
* @param {bool} [options.isProductPage=false] If the location has a product or not.
|
|
* @param {bool} [options.isSupportedSite=false] If the location is on a supported site or not.
|
|
* @param {object | null} [options.supportedDomains] Object mapping supported sites and domains, or null if the list is unavailable or inapplicable.
|
|
*/
|
|
updateLocation({
|
|
isProductPage = true,
|
|
isSupportedSite = false,
|
|
supportedDomains,
|
|
} = {}) {
|
|
this.sendToContent("Update", {
|
|
isProductPage,
|
|
isSupportedSite,
|
|
supportedDomains,
|
|
isSidebarStartPosition: this.isSidebarStartPosition,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shows the onboarding flow in the content.
|
|
*
|
|
* @param {object?} options
|
|
* @param {string} [options.productUrl] URL of the current product if this is a product page.
|
|
* @param {boolean} [options.isProductPage] True if a product page, else false.
|
|
* @param {boolean} [options.isSupportedSite] True if a supported site, else false.
|
|
*/
|
|
showOnboarding({ productUrl, isProductPage, isSupportedSite } = {}) {
|
|
// Similar to canContinue() above, check to see if things
|
|
// have changed while we were waiting. Bail out if the user
|
|
// opted in, or if the actor doesn't exist.
|
|
if (this._destroyed || this.canFetchAndShowData) {
|
|
return;
|
|
}
|
|
|
|
// Send the productUrl to content for Onboarding's dynamic text
|
|
this.sendToContent("Update", {
|
|
showOnboarding: true,
|
|
data: null,
|
|
productUrl,
|
|
isProductPage,
|
|
isSupportedSite,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates if recommendation have been enabled or disable in the content settings.
|
|
*
|
|
* @param {bool} adsEnabledByUser
|
|
*/
|
|
updateAdsEnabledByUser(adsEnabledByUser) {
|
|
this.sendToContent("adsEnabledByUserChanged", {
|
|
adsEnabledByUser,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates if auto open has been enabled or disable in the content settings.
|
|
*
|
|
* @param {bool} autoOpenEnabledByUser
|
|
*/
|
|
updateAutoOpenEnabledByUser(autoOpenEnabledByUser) {
|
|
this.sendToContent("autoOpenEnabledByUserChanged", {
|
|
autoOpenEnabledByUser,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates if auto close has been enabled or disable in the content settings.
|
|
*
|
|
* @param {bool} autoCloseEnabledByUser
|
|
*/
|
|
updateAutoCloseEnabledByUser(autoCloseEnabledByUser) {
|
|
this.sendToContent("autoCloseEnabledByUserChanged", {
|
|
autoCloseEnabledByUser,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates percentage complete for a product analysis.
|
|
*
|
|
* @param {number} progress
|
|
*/
|
|
updateAnalysisProgress(progress) {
|
|
this.sendToContent("UpdateAnalysisProgress", {
|
|
progress,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send messages and cloned objects to the content.
|
|
*
|
|
* @param {string} eventName event string to pass.
|
|
* @param {object} detail object to clone.
|
|
*/
|
|
sendToContent(eventName, detail) {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
let win = this.contentWindow;
|
|
let evt = new win.CustomEvent(eventName, {
|
|
bubbles: true,
|
|
detail: Cu.cloneInto(detail, win),
|
|
});
|
|
win.document.dispatchEvent(evt);
|
|
}
|
|
}
|