summaryrefslogtreecommitdiffstats
path: root/browser/components/shopping/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/shopping/content')
-rw-r--r--browser/components/shopping/content/adjusted-rating.mjs53
-rw-r--r--browser/components/shopping/content/analysis-explainer.css39
-rw-r--r--browser/components/shopping/content/analysis-explainer.mjs158
-rw-r--r--browser/components/shopping/content/assets/competitiveness.svg6
-rw-r--r--browser/components/shopping/content/assets/optInDark.avifbin0 -> 9746 bytes
-rw-r--r--browser/components/shopping/content/assets/optInLight.avifbin0 -> 9651 bytes
-rw-r--r--browser/components/shopping/content/assets/packaging.svg6
-rw-r--r--browser/components/shopping/content/assets/price.svg7
-rw-r--r--browser/components/shopping/content/assets/priceTagButtonCallout.svg41
-rw-r--r--browser/components/shopping/content/assets/quality.svg7
-rw-r--r--browser/components/shopping/content/assets/ratingDark.avifbin0 -> 14230 bytes
-rw-r--r--browser/components/shopping/content/assets/ratingLight.avifbin0 -> 14071 bytes
-rw-r--r--browser/components/shopping/content/assets/reviewsVisualCallout.svg77
-rw-r--r--browser/components/shopping/content/assets/shipping.svg6
-rw-r--r--browser/components/shopping/content/assets/shopping.svg6
-rw-r--r--browser/components/shopping/content/assets/unanalyzedDark.avifbin0 -> 11485 bytes
-rw-r--r--browser/components/shopping/content/assets/unanalyzedLight.avifbin0 -> 11070 bytes
-rw-r--r--browser/components/shopping/content/highlight-item.css66
-rw-r--r--browser/components/shopping/content/highlight-item.mjs57
-rw-r--r--browser/components/shopping/content/highlights.mjs124
-rw-r--r--browser/components/shopping/content/letter-grade.css132
-rw-r--r--browser/components/shopping/content/letter-grade.mjs78
-rw-r--r--browser/components/shopping/content/onboarding.mjs69
-rw-r--r--browser/components/shopping/content/recommended-ad.css59
-rw-r--r--browser/components/shopping/content/recommended-ad.mjs150
-rw-r--r--browser/components/shopping/content/reliability.mjs48
-rw-r--r--browser/components/shopping/content/settings.css69
-rw-r--r--browser/components/shopping/content/settings.mjs210
-rw-r--r--browser/components/shopping/content/shopping-card.css201
-rw-r--r--browser/components/shopping/content/shopping-card.mjs204
-rw-r--r--browser/components/shopping/content/shopping-container.css152
-rw-r--r--browser/components/shopping/content/shopping-container.mjs471
-rw-r--r--browser/components/shopping/content/shopping-message-bar.css78
-rw-r--r--browser/components/shopping/content/shopping-message-bar.mjs278
-rw-r--r--browser/components/shopping/content/shopping-page.css28
-rw-r--r--browser/components/shopping/content/shopping-sidebar.js80
-rw-r--r--browser/components/shopping/content/shopping.ftl7
-rw-r--r--browser/components/shopping/content/shopping.html53
-rw-r--r--browser/components/shopping/content/unanalyzed.css41
-rw-r--r--browser/components/shopping/content/unanalyzed.mjs61
40 files changed, 3122 insertions, 0 deletions
diff --git a/browser/components/shopping/content/adjusted-rating.mjs b/browser/components/shopping/content/adjusted-rating.mjs
new file mode 100644
index 0000000000..a49b821145
--- /dev/null
+++ b/browser/components/shopping/content/adjusted-rating.mjs
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-five-star.mjs";
+
+/**
+ * Class for displaying the adjusted ratings for a given product.
+ */
+class AdjustedRating extends MozLitElement {
+ static properties = {
+ rating: { type: Number, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ ratingEl: "moz-five-star",
+ };
+ }
+
+ render() {
+ if (!this.rating && this.rating !== 0) {
+ this.hidden = true;
+ return null;
+ }
+
+ this.hidden = false;
+
+ return html`
+ <shopping-card
+ data-l10n-id="shopping-adjusted-rating-label"
+ data-l10n-attrs="label"
+ >
+ <moz-five-star
+ slot="rating"
+ rating="${this.rating === 0 ? 0.5 : this.rating}"
+ ></moz-five-star>
+ <div slot="content">
+ <span
+ data-l10n-id="shopping-adjusted-rating-unreliable-reviews"
+ ></span>
+ </div>
+ </shopping-card>
+ `;
+ }
+}
+
+customElements.define("adjusted-rating", AdjustedRating);
diff --git a/browser/components/shopping/content/analysis-explainer.css b/browser/components/shopping/content/analysis-explainer.css
new file mode 100644
index 0000000000..ce7ebf55cc
--- /dev/null
+++ b/browser/components/shopping/content/analysis-explainer.css
@@ -0,0 +1,39 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+#analysis-explainer-wrapper p,
+.analysis-explainer-grading-scale-description {
+ line-height: 150%;
+}
+
+#analysis-explainer-grading-scale-wrapper {
+ margin-inline-start: 0.54em;
+}
+
+#analysis-explainer-grading-scale-list {
+ list-style: none;
+ padding: 0;
+}
+
+.analysis-explainer-grading-scale-entry {
+ display: flex;
+ align-items: flex-start;
+ align-self: stretch;
+ gap: 0.54rem;
+ padding: 0.54em;
+ padding-inline-start: 0;
+}
+
+.analysis-explainer-grading-scale-letters {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.2rem;
+ width: 3.47em;
+}
+
+.analysis-explainer-grading-scale-description {
+ margin: 0;
+}
diff --git a/browser/components/shopping/content/analysis-explainer.mjs b/browser/components/shopping/content/analysis-explainer.mjs
new file mode 100644
index 0000000000..a32c64dfeb
--- /dev/null
+++ b/browser/components/shopping/content/analysis-explainer.mjs
@@ -0,0 +1,158 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-card.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/letter-grade.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-support-link.mjs";
+
+const VALID_EXPLAINER_L10N_IDS = new Map([
+ ["reliable", "shopping-analysis-explainer-review-grading-scale-reliable"],
+ ["mixed", "shopping-analysis-explainer-review-grading-scale-mixed"],
+ ["unreliable", "shopping-analysis-explainer-review-grading-scale-unreliable"],
+]);
+
+/**
+ * Class for displaying details about letter grades, adjusted rating, and highlights.
+ */
+class AnalysisExplainer extends MozLitElement {
+ static properties = {
+ productUrl: { type: String, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ reviewQualityExplainerLink: "#review-quality-url",
+ };
+ }
+
+ getGradesDescriptionTemplate() {
+ return html`
+ <section id="analysis-explainer-grades-wrapper">
+ <p data-l10n-id="shopping-analysis-explainer-grades-intro"></p>
+ </section>
+ `;
+ }
+
+ createGradingScaleEntry(letters, descriptionL10nId) {
+ let letterGradesTemplate = [];
+ for (let letter of letters) {
+ letterGradesTemplate.push(
+ html`<letter-grade letter=${letter}></letter-grade>`
+ );
+ }
+ return html`
+ <div class="analysis-explainer-grading-scale-entry">
+ <dt class="analysis-explainer-grading-scale-term">
+ <span class="analysis-explainer-grading-scale-letters">
+ ${letterGradesTemplate}
+ </span>
+ </dt>
+ <dd
+ class="analysis-explainer-grading-scale-description"
+ data-l10n-id=${descriptionL10nId}
+ ></dd>
+ </div>
+ `;
+ }
+
+ getGradingScaleListTemplate() {
+ return html`
+ <section id="analysis-explainer-grading-scale-wrapper">
+ <dl id="analysis-explainer-grading-scale-list">
+ ${this.createGradingScaleEntry(
+ ["A", "B"],
+ VALID_EXPLAINER_L10N_IDS.get("reliable")
+ )}
+ ${this.createGradingScaleEntry(
+ ["C"],
+ VALID_EXPLAINER_L10N_IDS.get("mixed")
+ )}
+ ${this.createGradingScaleEntry(
+ ["D", "F"],
+ VALID_EXPLAINER_L10N_IDS.get("unreliable")
+ )}
+ </dl>
+ </section>
+ `;
+ }
+
+ // It turns out we must always return a non-empty string: if not, the fluent
+ // resolver will complain that the variable value is missing. We use the
+ // placeholder "retailer", which should never be visible to users.
+ getRetailerDisplayName() {
+ let defaultName = "retailer";
+ if (!this.productUrl) {
+ return defaultName;
+ }
+ let url = new URL(this.productUrl);
+ let hostname = url.hostname;
+ let displayNames = {
+ "www.amazon.com": "Amazon",
+ "www.bestbuy.com": "Best Buy",
+ "www.walmart.com": "Walmart",
+ };
+ return displayNames[hostname] ?? defaultName;
+ }
+
+ handleReviewQualityUrlClicked(e) {
+ if (e.target.localName == "a" && e.button == 0) {
+ Glean.shopping.surfaceShowQualityExplainerUrlClicked.record();
+ }
+ }
+
+ // Bug 1857620: rather than manually set the utm parameters on the SUMO link,
+ // we should instead update moz-support-link to allow arbitrary utm parameters.
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/analysis-explainer.css"
+ />
+ <shopping-card
+ data-l10n-id="shopping-analysis-explainer-label"
+ data-l10n-attrs="label"
+ type="accordion"
+ >
+ <div slot="content">
+ <div id="analysis-explainer-wrapper">
+ <p data-l10n-id="shopping-analysis-explainer-intro2"></p>
+ ${this.getGradesDescriptionTemplate()}
+ ${this.getGradingScaleListTemplate()}
+ <p
+ data-l10n-id="shopping-analysis-explainer-adjusted-rating-description"
+ ></p>
+ <p
+ data-l10n-id="shopping-analysis-explainer-highlights-description"
+ data-l10n-args="${JSON.stringify({
+ retailer: this.getRetailerDisplayName(),
+ })}"
+ ></p>
+ <p
+ data-l10n-id="shopping-analysis-explainer-learn-more2"
+ @click=${this.handleReviewQualityUrlClicked}
+ >
+ <a
+ id="review-quality-url"
+ data-l10n-name="review-quality-url"
+ target="_blank"
+ href="${window.RPMGetFormatURLPref(
+ "app.support.baseURL"
+ )}review-checker-review-quality?as=u&utm_source=inproduct&utm_campaign=learn-more&utm_term=core-sidebar"
+ ></a>
+ </p>
+ </div>
+ </div>
+ </shopping-card>
+ `;
+ }
+}
+
+customElements.define("analysis-explainer", AnalysisExplainer);
diff --git a/browser/components/shopping/content/assets/competitiveness.svg b/browser/components/shopping/content/assets/competitiveness.svg
new file mode 100644
index 0000000000..a192205fc2
--- /dev/null
+++ b/browser/components/shopping/content/assets/competitiveness.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 1.75A.75.75 0 0 1 3.75 1h8.5a.75.75 0 0 1 .75.75V3h2.25a.75.75 0 0 1 .75.75v2.5a3.75 3.75 0 0 1-3.41 3.735 5.007 5.007 0 0 1-3.84 2.96V14.5H12V16H4v-1.5h3.25v-1.556a5.007 5.007 0 0 1-3.84-2.96A3.75 3.75 0 0 1 0 6.25v-2.5A.75.75 0 0 1 .75 3H3V1.75zm9.986 6.627A2.25 2.25 0 0 0 14.5 6.25V4.5H13V8c0 .127-.005.252-.014.377zM3 8c0 .127.005.252.014.377A2.25 2.25 0 0 1 1.5 6.25V4.5H3V8zm1.5-5.5V8a3.5 3.5 0 0 0 7 0V2.5h-7z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/optInDark.avif b/browser/components/shopping/content/assets/optInDark.avif
new file mode 100644
index 0000000000..fb662e63dd
--- /dev/null
+++ b/browser/components/shopping/content/assets/optInDark.avif
Binary files differ
diff --git a/browser/components/shopping/content/assets/optInLight.avif b/browser/components/shopping/content/assets/optInLight.avif
new file mode 100644
index 0000000000..e148580976
--- /dev/null
+++ b/browser/components/shopping/content/assets/optInLight.avif
Binary files differ
diff --git a/browser/components/shopping/content/assets/packaging.svg b/browser/components/shopping/content/assets/packaging.svg
new file mode 100644
index 0000000000..fddf88c878
--- /dev/null
+++ b/browser/components/shopping/content/assets/packaging.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path fill-rule="evenodd" d="M3 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3Zm-.5 2a.5.5 0 0 1 .5-.5h2v5a.75.75 0 0 0 1.114.656L8 7.108l1.886 1.048A.75.75 0 0 0 11 7.5v-5h2a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5V3Zm7-.5h-3v3.725l1.136-.63a.75.75 0 0 1 .728 0l1.136.63V2.5Z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/price.svg b/browser/components/shopping/content/assets/price.svg
new file mode 100644
index 0000000000..e5b8ba7a22
--- /dev/null
+++ b/browser/components/shopping/content/assets/price.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path d="M0 .75A.75.75 0 0 1 .75 0h6.717a.75.75 0 0 1 .53.22l6.418 6.417a2 2 0 0 1 0 2.828l-4.95 4.95a2 2 0 0 1-2.828 0L.22 7.998a.75.75 0 0 1-.22-.53V.75Zm1.5.75v5.657l6.197 6.198a.5.5 0 0 0 .708 0l4.95-4.95a.5.5 0 0 0 0-.708L7.156 1.5H1.5Z"/>
+<path d="m4.58 5.47 1.061-1.06-1.06-1.061-1.061 1.06L4.58 5.47Z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/priceTagButtonCallout.svg b/browser/components/shopping/content/assets/priceTagButtonCallout.svg
new file mode 100644
index 0000000000..26431804cb
--- /dev/null
+++ b/browser/components/shopping/content/assets/priceTagButtonCallout.svg
@@ -0,0 +1,41 @@
+<!-- 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 https://mozilla.org/MPL/2.0/. -->
+<svg width="352" height="214" viewBox="0 0 352 214" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_860_36488)">
+<rect x="-5.65186" y="-7.44189" width="357.558" height="250" fill="hsla(252, 88%, 70%, 22%)"/>
+<g filter="url(#filter0_d_860_36488)">
+<rect x="-40.5356" y="52.4418" width="340.116" height="207.558" rx="18.6047" fill="#F9F9FB"/>
+</g>
+<path d="M-23.6753 52.4418H282.139C291.772 52.4418 299.581 60.2508 299.581 69.8836V173.953H-23.6753V52.4418Z" fill="#E0E0E6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M273.703 137.919H258.599C258.366 137.919 258.143 137.826 257.978 137.662C257.813 137.497 257.721 137.274 257.721 137.041C257.721 136.808 257.813 136.585 257.978 136.42C258.143 136.255 258.366 136.163 258.599 136.163H273.703C273.936 136.163 274.159 136.255 274.324 136.42C274.489 136.585 274.581 136.808 274.581 137.041C274.581 137.274 274.489 137.497 274.324 137.662C274.159 137.826 273.936 137.919 273.703 137.919ZM273.703 143.539H258.599C258.366 143.539 258.143 143.447 257.978 143.282C257.813 143.118 257.721 142.894 257.721 142.661C257.721 142.428 257.813 142.205 257.978 142.04C258.143 141.876 258.366 141.783 258.599 141.783H273.703C273.936 141.783 274.159 141.876 274.324 142.04C274.489 142.205 274.581 142.428 274.581 142.661C274.581 142.894 274.489 143.118 274.324 143.282C274.159 143.447 273.936 143.539 273.703 143.539ZM258.599 149.159H273.703C273.936 149.159 274.159 149.067 274.324 148.902C274.489 148.737 274.581 148.514 274.581 148.281C274.581 148.048 274.489 147.825 274.324 147.66C274.159 147.495 273.936 147.403 273.703 147.403H258.599C258.366 147.403 258.143 147.495 257.978 147.66C257.813 147.825 257.721 148.048 257.721 148.281C257.721 148.514 257.813 148.737 257.978 148.902C258.143 149.067 258.366 149.159 258.599 149.159Z" fill="#8F8F9D"/>
+<path d="M-23.6753 52.4418H282.139C291.772 52.4418 299.581 60.2508 299.581 69.8836V111.163H-23.6753V52.4418Z" fill="#CFCFD8"/>
+<path d="M180.51 82.0315V83.363H167.208V82.0315H180.51Z" fill="#8F8F9D"/>
+<path d="M226.871 76.7108V90.0127H213.57V76.7108H226.871ZM225.54 78.0423H214.901V88.6812H225.54V78.0423Z" fill="#8F8F9D"/>
+<path d="M267.524 83.3619L273.233 89.071L272.291 90.0128L266.582 84.3037L260.873 90.0128L259.931 89.071L265.64 83.3619L259.931 77.6527L260.873 76.7109L266.582 82.4201L272.291 76.7109L273.233 77.6527L267.524 83.3619Z" fill="#8F8F9D"/>
+<rect x="-50.75" y="120.134" width="220.43" height="46.0116" rx="4.40116" fill="white" stroke="#5B5B66" stroke-width="0.5"/>
+<rect x="84.1739" y="125.988" width="34.8837" height="34.8837" rx="5.52326" stroke="#0060DF" stroke-width="1.74419"/>
+<rect x="-19.605" y="173.953" width="319.186" height="76.7442" fill="#F9F9FB"/>
+<path d="M103.624 132.093H94.9371V134.13H103.2L110.926 141.88L112.372 140.443L104.346 132.393C104.155 132.201 103.895 132.093 103.624 132.093Z" fill="#15141A"/>
+<path d="M98.3554 144.177L100.004 142.53L98.3554 140.883L96.7069 142.53L98.3554 144.177Z" fill="#15141A"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M92.0229 137.187C92.0229 136.624 92.4796 136.168 93.0429 136.168H101.511C101.782 136.168 102.041 136.275 102.233 136.466L110.083 144.308C111.222 145.445 111.222 147.287 110.083 148.424L104.004 154.496C102.866 155.633 101.022 155.633 99.8833 154.496L92.3217 146.943C92.1304 146.752 92.0229 146.493 92.0229 146.223V137.187ZM94.0628 138.205V145.801L101.326 153.055C101.668 153.397 102.22 153.397 102.562 153.055L108.641 146.983C108.983 146.642 108.983 146.09 108.641 145.749L101.089 138.205H94.0628Z" fill="#15141A"/>
+<path d="M138.723 154.174C138.205 154.174 137.676 154.012 137.24 153.698C136.392 153.093 136.016 152.058 136.275 151.058L137.523 146.232L133.638 143.046C132.837 142.384 132.519 141.325 132.849 140.349C133.167 139.372 134.05 138.686 135.097 138.628L140.136 138.302L141.996 133.674C142.385 132.721 143.303 132.093 144.351 132.093C145.398 132.093 146.317 132.709 146.705 133.674L148.565 138.302L153.604 138.628C154.64 138.698 155.523 139.372 155.852 140.349C156.17 141.325 155.864 142.395 155.064 143.046L151.179 146.232L152.427 151.058C152.686 152.058 152.309 153.093 151.461 153.698C150.614 154.302 149.495 154.337 148.612 153.791L144.351 151.128L140.089 153.791C139.677 154.046 139.206 154.186 138.735 154.186L138.723 154.174ZM144.339 134.116C144.221 134.116 143.997 134.151 143.903 134.407L141.796 139.639C141.655 140 141.302 140.256 140.901 140.279L135.215 140.639C134.933 140.663 134.827 140.849 134.791 140.965C134.756 141.058 134.721 141.291 134.944 141.477L139.324 145.07C139.63 145.325 139.759 145.721 139.665 146.105L138.264 151.558C138.194 151.825 138.347 151.988 138.441 152.058C138.535 152.128 138.735 152.221 138.971 152.07L143.786 149.058C144.127 148.849 144.551 148.849 144.88 149.058L149.695 152.07C149.931 152.221 150.131 152.128 150.225 152.058C150.319 151.988 150.472 151.825 150.402 151.558L148.989 146.105C148.895 145.721 149.024 145.325 149.33 145.07L153.71 141.477C153.933 141.291 153.886 141.058 153.863 140.977C153.827 140.86 153.722 140.674 153.439 140.651L147.753 140.291C147.353 140.267 147.011 140.012 146.858 139.651L144.751 134.418C144.645 134.163 144.433 134.128 144.315 134.128L144.339 134.116Z" fill="#8F8F9D"/>
+<line x1="299.581" y1="111.413" x2="-19.605" y2="111.413" stroke="#5B5B66" stroke-width="0.5"/>
+<line x1="299.581" y1="174.203" x2="-35.3027" y2="174.203" stroke="#5B5B66" stroke-width="0.5"/>
+<path d="M-30.6519 52.4418H280.976C291.251 52.4418 299.581 60.7714 299.581 71.0464V242.558" stroke="#5B5B66" stroke-width="0.5"/>
+</g>
+<defs>
+<filter id="filter0_d_860_36488" x="-49.2566" y="40.2325" width="361.046" height="228.488" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dx="1.74419" dy="-1.74419"/>
+<feGaussianBlur stdDeviation="5.23256"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.227451 0 0 0 0 0.223529 0 0 0 0 0.266667 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_860_36488"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_860_36488" result="shape"/>
+</filter>
+<clipPath id="clip0_860_36488">
+<rect width="352" height="214" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/browser/components/shopping/content/assets/quality.svg b/browser/components/shopping/content/assets/quality.svg
new file mode 100644
index 0000000000..cdc092d1ea
--- /dev/null
+++ b/browser/components/shopping/content/assets/quality.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path d="M8.035 9.713 11 6.748l-1.061-1.06-2.434 2.434L6.06 6.677 5 7.737l1.975 1.976a.75.75 0 0 0 1.06 0Z"/>
+<path d="M10.586 1.723 9.414.552a2 2 0 0 0-2.828 0L5.414 1.723H3.757a2 2 0 0 0-2 2v1.656L.585 6.552a2 2 0 0 0 0 2.828l1.172 1.172v1.657a2 2 0 0 0 2 2h1.657l1.172 1.171a2 2 0 0 0 2.828 0l1.172-1.171h1.657a2 2 0 0 0 2-2v-1.657l1.172-1.172a2 2 0 0 0 0-2.828l-1.172-1.173V3.723a2 2 0 0 0-2-2h-1.657Zm-2.94-.11a.5.5 0 0 1 .708 0l1.392 1.39c.14.141.331.22.53.22h1.967a.5.5 0 0 1 .5.5V5.69c0 .199.079.39.22.53l1.39 1.392a.5.5 0 0 1 0 .708l-1.39 1.39a.75.75 0 0 0-.22.531v1.968a.5.5 0 0 1-.5.5h-1.968a.75.75 0 0 0-.53.22l-1.391 1.39a.5.5 0 0 1-.708 0l-1.39-1.39a.75.75 0 0 0-.531-.22H3.757a.5.5 0 0 1-.5-.5v-1.968a.75.75 0 0 0-.22-.53L1.647 8.32a.5.5 0 0 1 0-.708l1.39-1.392a.75.75 0 0 0 .22-.53V3.723a.5.5 0 0 1 .5-.5h1.968a.75.75 0 0 0 .53-.22l1.391-1.39Z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/ratingDark.avif b/browser/components/shopping/content/assets/ratingDark.avif
new file mode 100644
index 0000000000..240c2d6238
--- /dev/null
+++ b/browser/components/shopping/content/assets/ratingDark.avif
Binary files differ
diff --git a/browser/components/shopping/content/assets/ratingLight.avif b/browser/components/shopping/content/assets/ratingLight.avif
new file mode 100644
index 0000000000..a6eb3a0316
--- /dev/null
+++ b/browser/components/shopping/content/assets/ratingLight.avif
Binary files differ
diff --git a/browser/components/shopping/content/assets/reviewsVisualCallout.svg b/browser/components/shopping/content/assets/reviewsVisualCallout.svg
new file mode 100644
index 0000000000..9a9b93ba06
--- /dev/null
+++ b/browser/components/shopping/content/assets/reviewsVisualCallout.svg
@@ -0,0 +1,77 @@
+<!-- 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 https://mozilla.org/MPL/2.0/. -->
+<svg width="352" height="214" viewBox="0 0 352 214" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_860_36523)">
+<rect width="352" height="214" fill="hsla(252, 88%, 70%, 22%)"/>
+<g filter="url(#filter0_d_860_36523)">
+<path d="M301.2 27.6845C301.2 22.8882 297.312 19 292.515 19H58.6845C53.8882 19 50 22.8882 50 27.6845V75.1155C50 79.9118 53.8882 83.8 58.6845 83.8H292.515C297.312 83.8 301.2 79.9118 301.2 75.1155V27.6845Z" fill="white"/>
+</g>
+<path d="M66.8272 36.3504L64.3962 34.3317C64.2369 34.2002 64.1209 34.0236 64.0636 33.825C64.0063 33.6265 64.0102 33.4153 64.0749 33.219C64.1379 33.0226 64.2588 32.8497 64.4217 32.723C64.5846 32.5964 64.7819 32.5219 64.9878 32.5093L68.1303 32.3061L68.5561 31.9874L69.7155 29.0744C69.8736 28.68 70.2493 28.425 70.6735 28.425C71.0976 28.425 71.4733 28.68 71.6314 29.0736V29.0744L72.7908 31.9874L73.2167 32.3061L76.3591 32.5093C76.7824 32.5365 77.1411 32.8153 77.272 33.219C77.4038 33.6236 77.2771 34.0605 76.9507 34.3317L74.524 36.347L74.371 36.8502L75.1445 39.889C75.249 40.3012 75.0943 40.7271 74.7509 40.9761C74.5842 41.0974 74.3849 41.1658 74.1788 41.1726C73.9727 41.1795 73.7694 41.1243 73.5949 41.0144L70.937 39.3339H70.4091L67.752 41.0144C67.3899 41.2405 66.936 41.2243 66.5952 40.9761C66.4277 40.8557 66.3006 40.6874 66.2304 40.4934C66.1601 40.2994 66.1501 40.0888 66.2016 39.889L66.9734 36.8579L66.8272 36.3504Z" fill="#FFA436"/>
+<path d="M83.8272 36.3504L81.3962 34.3317C81.2369 34.2002 81.1209 34.0236 81.0636 33.825C81.0063 33.6265 81.0102 33.4153 81.0749 33.219C81.1379 33.0226 81.2588 32.8497 81.4217 32.723C81.5846 32.5964 81.7819 32.5219 81.9878 32.5093L85.1303 32.3061L85.5561 31.9874L86.7155 29.0744C86.8736 28.68 87.2493 28.425 87.6735 28.425C88.0976 28.425 88.4733 28.68 88.6314 29.0736V29.0744L89.7908 31.9874L90.2167 32.3061L93.3591 32.5093C93.7824 32.5365 94.1411 32.8153 94.272 33.219C94.4038 33.6236 94.2771 34.0605 93.9507 34.3317L91.524 36.347L91.371 36.8502L92.1445 39.889C92.249 40.3012 92.0943 40.7271 91.7509 40.9761C91.5842 41.0974 91.3849 41.1658 91.1788 41.1726C90.9727 41.1795 90.7694 41.1243 90.5949 41.0144L87.937 39.3339H87.4091L84.752 41.0144C84.3899 41.2405 83.936 41.2243 83.5952 40.9761C83.4277 40.8557 83.3006 40.6874 83.2304 40.4934C83.1601 40.2994 83.1501 40.0888 83.2016 39.889L83.9734 36.8579L83.8272 36.3504Z" fill="#FFA436"/>
+<path d="M100.827 36.3504L98.3962 34.3317C98.2369 34.2002 98.1209 34.0236 98.0636 33.825C98.0063 33.6265 98.0102 33.4153 98.0749 33.219C98.1379 33.0226 98.2588 32.8497 98.4217 32.723C98.5846 32.5964 98.7819 32.5219 98.9878 32.5093L102.13 32.3061L102.556 31.9874L103.716 29.0744C103.874 28.68 104.249 28.425 104.673 28.425C105.098 28.425 105.473 28.68 105.631 29.0736V29.0744L106.791 31.9874L107.217 32.3061L110.359 32.5093C110.782 32.5365 111.141 32.8153 111.272 33.219C111.404 33.6236 111.277 34.0605 110.951 34.3317L108.524 36.347L108.371 36.8502L109.144 39.889C109.249 40.3012 109.094 40.7271 108.751 40.9761C108.584 41.0974 108.385 41.1658 108.179 41.1726C107.973 41.1795 107.769 41.1243 107.595 41.0144L104.937 39.3339H104.409L101.752 41.0144C101.39 41.2405 100.936 41.2243 100.595 40.9761C100.428 40.8557 100.301 40.6874 100.23 40.4934C100.16 40.2994 100.15 40.0888 100.202 39.889L100.973 36.8579L100.827 36.3504Z" fill="#FFA436"/>
+<path d="M117.827 36.3504L115.396 34.3317C115.237 34.2002 115.121 34.0236 115.064 33.825C115.006 33.6265 115.01 33.4153 115.075 33.219C115.138 33.0226 115.259 32.8497 115.422 32.723C115.585 32.5964 115.782 32.5219 115.988 32.5093L119.13 32.3061L119.556 31.9874L120.716 29.0744C120.874 28.68 121.249 28.425 121.673 28.425C122.098 28.425 122.473 28.68 122.631 29.0736V29.0744L123.791 31.9874L124.217 32.3061L127.359 32.5093C127.782 32.5365 128.141 32.8153 128.272 33.219C128.404 33.6236 128.277 34.0605 127.951 34.3317L125.524 36.347L125.371 36.8502L126.144 39.889C126.249 40.3012 126.094 40.7271 125.751 40.9761C125.584 41.0974 125.385 41.1658 125.179 41.1726C124.973 41.1795 124.769 41.1243 124.595 41.0144L121.937 39.3339H121.409L118.752 41.0144C118.39 41.2405 117.936 41.2243 117.595 40.9761C117.428 40.8557 117.301 40.6874 117.23 40.4934C117.16 40.2994 117.15 40.0888 117.202 39.889L117.973 36.8579L117.827 36.3504Z" fill="#FFA436"/>
+<path d="M135.431 41.1742C135.145 41.1742 134.86 41.0858 134.617 40.9081C134.392 40.746 134.222 40.5196 134.127 40.2587C134.033 39.9979 134.02 39.7147 134.089 39.4461L134.82 36.5774L132.542 34.6853C132.329 34.5091 132.173 34.2724 132.096 34.0062C132.019 33.74 132.024 33.4568 132.111 33.1935C132.196 32.9295 132.358 32.6973 132.577 32.5271C132.796 32.3569 133.061 32.2568 133.338 32.2398L136.292 32.0486L137.387 29.2971C137.598 28.7676 138.103 28.425 138.674 28.425C139.244 28.425 139.749 28.7676 139.96 29.2971L141.055 32.0486L144.009 32.2398C144.286 32.2567 144.551 32.3568 144.77 32.527C144.989 32.6971 145.152 32.9295 145.236 33.1935C145.323 33.4571 145.328 33.7407 145.251 34.0072C145.174 34.2737 145.018 34.5106 144.804 34.687L142.526 36.5782L143.257 39.447C143.326 39.7155 143.313 39.9987 143.219 40.2596C143.125 40.5205 142.954 40.7468 142.729 40.909C142.505 41.0731 142.237 41.1657 141.96 41.1747C141.682 41.1836 141.409 41.1085 141.175 40.9591L138.674 39.3773L136.171 40.9591C135.95 41.0994 135.693 41.174 135.431 41.1742ZM138.674 29.4875C138.609 29.4859 138.545 29.5046 138.491 29.541C138.438 29.5775 138.397 29.6298 138.374 29.6907L137.155 32.7532L136.696 33.0873L133.406 33.3006C133.341 33.3033 133.279 33.326 133.228 33.3658C133.177 33.4056 133.139 33.4604 133.12 33.5225C133.099 33.5835 133.097 33.6497 133.115 33.7117C133.134 33.7738 133.171 33.8286 133.222 33.8684L135.758 35.9756L135.933 36.5153L135.119 39.7096C135.101 39.772 135.104 39.8382 135.126 39.8991C135.148 39.9599 135.189 40.0123 135.242 40.0488C135.296 40.0887 135.438 40.1669 135.604 40.0607L138.39 38.3003H138.958L141.745 40.0607C141.799 40.0969 141.863 40.1152 141.928 40.1131C141.993 40.111 142.055 40.0885 142.107 40.0488C142.16 40.0117 142.2 39.9591 142.222 39.8982C142.243 39.8373 142.246 39.7711 142.228 39.7088L141.414 36.5145L141.589 35.9747L144.125 33.8684C144.176 33.8285 144.214 33.7736 144.232 33.7113C144.25 33.6491 144.248 33.5828 144.227 33.5216C144.208 33.4596 144.17 33.4049 144.119 33.3651C144.068 33.3254 144.006 33.3025 143.941 33.2998L140.652 33.0864L140.192 32.7524L138.973 29.6898C138.95 29.6293 138.909 29.5772 138.855 29.541C138.802 29.5047 138.738 29.4861 138.674 29.4875Z" fill="#FFA436"/>
+<path d="M151.827 36.3504L149.396 34.3317C149.237 34.2002 149.121 34.0236 149.064 33.825C149.006 33.6265 149.01 33.4153 149.075 33.219C149.138 33.0226 149.259 32.8497 149.422 32.723C149.585 32.5964 149.782 32.5219 149.988 32.5093L153.13 32.3061L153.556 31.9874L154.716 29.0744C154.874 28.68 155.249 28.425 155.673 28.425C156.098 28.425 156.473 28.68 156.631 29.0736V29.0744L157.791 31.9874L158.217 32.3061L161.359 32.5093C161.782 32.5365 162.141 32.8153 162.272 33.219C162.404 33.6236 162.277 34.0605 161.951 34.3317L159.524 36.347L159.371 36.8502L160.144 39.889C160.249 40.3012 160.094 40.7271 159.751 40.9761C159.584 41.0974 159.385 41.1658 159.179 41.1726C158.973 41.1795 158.769 41.1243 158.595 41.0144L155.937 39.3339H155.409L152.752 41.0144C152.39 41.2405 151.936 41.2243 151.595 40.9761C151.428 40.8557 151.301 40.6874 151.23 40.4934C151.16 40.2994 151.15 40.0888 151.202 39.889L151.973 36.8579L151.827 36.3504Z" fill="white"/>
+<path d="M169.431 41.1742C169.145 41.1742 168.86 41.0858 168.617 40.9081C168.392 40.746 168.222 40.5196 168.127 40.2587C168.033 39.9979 168.02 39.7147 168.089 39.4461L168.82 36.5774L166.542 34.6853C166.329 34.5091 166.173 34.2724 166.096 34.0062C166.019 33.74 166.024 33.4568 166.111 33.1935C166.196 32.9295 166.358 32.6973 166.577 32.5271C166.796 32.3569 167.061 32.2568 167.338 32.2398L170.292 32.0486L171.387 29.2971C171.598 28.7676 172.103 28.425 172.674 28.425C173.244 28.425 173.749 28.7676 173.96 29.2971L175.055 32.0486L178.009 32.2398C178.286 32.2567 178.551 32.3568 178.77 32.527C178.989 32.6971 179.152 32.9295 179.236 33.1935C179.323 33.4571 179.328 33.7407 179.251 34.0072C179.174 34.2737 179.018 34.5106 178.804 34.687L176.526 36.5782L177.257 39.447C177.326 39.7155 177.313 39.9987 177.219 40.2596C177.125 40.5205 176.954 40.7468 176.729 40.909C176.505 41.0731 176.237 41.1657 175.96 41.1747C175.682 41.1836 175.409 41.1085 175.175 40.9591L172.674 39.3773L170.171 40.9591C169.95 41.0994 169.693 41.174 169.431 41.1742ZM172.674 29.4875C172.609 29.4859 172.545 29.5046 172.491 29.541C172.438 29.5775 172.397 29.6298 172.374 29.6907L171.155 32.7532L170.696 33.0873L167.406 33.3006C167.341 33.3033 167.279 33.326 167.228 33.3658C167.177 33.4056 167.139 33.4604 167.12 33.5225C167.099 33.5835 167.097 33.6497 167.115 33.7117C167.134 33.7738 167.171 33.8286 167.222 33.8684L169.758 35.9756L169.933 36.5153L169.119 39.7096C169.101 39.772 169.104 39.8382 169.126 39.8991C169.148 39.9599 169.189 40.0123 169.242 40.0488C169.296 40.0887 169.438 40.1669 169.604 40.0607L172.39 38.3003H172.958L175.745 40.0607C175.799 40.0969 175.863 40.1152 175.928 40.1131C175.993 40.111 176.055 40.0885 176.107 40.0488C176.16 40.0117 176.2 39.9591 176.222 39.8982C176.243 39.8373 176.246 39.7711 176.228 39.7088L175.414 36.5145L175.589 35.9747L178.125 33.8684C178.176 33.8285 178.214 33.7736 178.232 33.7113C178.25 33.6491 178.248 33.5828 178.227 33.5216C178.208 33.4596 178.17 33.4049 178.119 33.3651C178.068 33.3254 178.006 33.3025 177.941 33.2998L174.652 33.0864L174.192 32.7524L172.973 29.6898C172.95 29.6293 172.909 29.5772 172.855 29.541C172.802 29.5047 172.738 29.4861 172.674 29.4875Z" fill="white"/>
+<rect x="64.7253" y="53.4" width="168" height="4.8" rx="2.4" fill="#E0E0E6"/>
+<rect x="64.7251" y="67.0001" width="136.8" height="4.8" rx="2.4" fill="#E0E0E6"/>
+<g filter="url(#filter1_d_860_36523)">
+<path d="M301.2 102.085C301.2 97.2882 297.312 93.4 292.515 93.4H58.6845C53.8882 93.4 50 97.2882 50 102.085V149.515C50 154.312 53.8882 158.2 58.6845 158.2H292.515C297.312 158.2 301.2 154.312 301.2 149.515V102.085Z" fill="white"/>
+<path d="M292.515 94.2H58.6845V92.6H292.515V94.2ZM50.8 102.085V149.515H49.2V102.085H50.8ZM58.6845 157.4H292.515V159H58.6845V157.4ZM300.4 149.515V102.085H302V149.515H300.4ZM292.515 157.4C296.87 157.4 300.4 153.87 300.4 149.515H302C302 154.754 297.754 159 292.515 159V157.4ZM50.8 149.515C50.8 153.87 54.33 157.4 58.6845 157.4V159C53.4463 159 49.2 154.754 49.2 149.515H50.8ZM58.6845 94.2C54.33 94.2 50.8 97.7301 50.8 102.085H49.2C49.2 96.8464 53.4463 92.6 58.6845 92.6V94.2ZM292.515 92.6C297.754 92.6 302 96.8464 302 102.085H300.4C300.4 97.73 296.87 94.2 292.515 94.2V92.6Z" fill="#EE0B0B"/>
+</g>
+<path d="M66.8272 110.75L64.3962 108.732C64.2369 108.6 64.1209 108.423 64.0636 108.225C64.0063 108.026 64.0102 107.815 64.0749 107.619C64.1379 107.422 64.2588 107.25 64.4217 107.123C64.5846 106.996 64.7819 106.922 64.9878 106.909L68.1303 106.706L68.5561 106.387L69.7155 103.474C69.8736 103.08 70.2493 102.825 70.6735 102.825C71.0976 102.825 71.4733 103.08 71.6314 103.474V103.474L72.7908 106.387L73.2167 106.706L76.3591 106.909C76.7824 106.936 77.1411 107.215 77.272 107.619C77.4038 108.024 77.2771 108.46 76.9507 108.732L74.524 110.747L74.371 111.25L75.1445 114.289C75.249 114.701 75.0943 115.127 74.7509 115.376C74.5842 115.497 74.3849 115.566 74.1788 115.573C73.9727 115.579 73.7694 115.524 73.5949 115.414L70.937 113.734H70.4091L67.752 115.414C67.3899 115.64 66.936 115.624 66.5952 115.376C66.4277 115.256 66.3006 115.087 66.2304 114.893C66.1601 114.699 66.1501 114.489 66.2016 114.289L66.9734 111.258L66.8272 110.75Z" fill="#FFA436"/>
+<path d="M83.8272 110.75L81.3962 108.732C81.2369 108.6 81.1209 108.423 81.0636 108.225C81.0063 108.026 81.0102 107.815 81.0749 107.619C81.1379 107.422 81.2588 107.25 81.4217 107.123C81.5846 106.996 81.7819 106.922 81.9878 106.909L85.1303 106.706L85.5561 106.387L86.7155 103.474C86.8736 103.08 87.2493 102.825 87.6735 102.825C88.0976 102.825 88.4733 103.08 88.6314 103.474V103.474L89.7908 106.387L90.2167 106.706L93.3591 106.909C93.7824 106.936 94.1411 107.215 94.272 107.619C94.4038 108.024 94.2771 108.46 93.9507 108.732L91.524 110.747L91.371 111.25L92.1445 114.289C92.249 114.701 92.0943 115.127 91.7509 115.376C91.5842 115.497 91.3849 115.566 91.1788 115.573C90.9727 115.579 90.7694 115.524 90.5949 115.414L87.937 113.734H87.4091L84.752 115.414C84.3899 115.64 83.936 115.624 83.5952 115.376C83.4277 115.256 83.3006 115.087 83.2304 114.893C83.1601 114.699 83.1501 114.489 83.2016 114.289L83.9734 111.258L83.8272 110.75Z" fill="#FFA436"/>
+<path d="M100.827 110.75L98.3962 108.732C98.2369 108.6 98.1209 108.423 98.0636 108.225C98.0063 108.026 98.0102 107.815 98.0749 107.619C98.1379 107.422 98.2588 107.25 98.4217 107.123C98.5846 106.996 98.7819 106.922 98.9878 106.909L102.13 106.706L102.556 106.387L103.716 103.474C103.874 103.08 104.249 102.825 104.673 102.825C105.098 102.825 105.473 103.08 105.631 103.474V103.474L106.791 106.387L107.217 106.706L110.359 106.909C110.782 106.936 111.141 107.215 111.272 107.619C111.404 108.024 111.277 108.46 110.951 108.732L108.524 110.747L108.371 111.25L109.144 114.289C109.249 114.701 109.094 115.127 108.751 115.376C108.584 115.497 108.385 115.566 108.179 115.573C107.973 115.579 107.769 115.524 107.595 115.414L104.937 113.734H104.409L101.752 115.414C101.39 115.64 100.936 115.624 100.595 115.376C100.428 115.256 100.301 115.087 100.23 114.893C100.16 114.699 100.15 114.489 100.202 114.289L100.973 111.258L100.827 110.75Z" fill="#FFA436"/>
+<path d="M117.827 110.75L115.396 108.732C115.237 108.6 115.121 108.423 115.064 108.225C115.006 108.026 115.01 107.815 115.075 107.619C115.138 107.422 115.259 107.25 115.422 107.123C115.585 106.996 115.782 106.922 115.988 106.909L119.13 106.706L119.556 106.387L120.716 103.474C120.874 103.08 121.249 102.825 121.673 102.825C122.098 102.825 122.473 103.08 122.631 103.474V103.474L123.791 106.387L124.217 106.706L127.359 106.909C127.782 106.936 128.141 107.215 128.272 107.619C128.404 108.024 128.277 108.46 127.951 108.732L125.524 110.747L125.371 111.25L126.144 114.289C126.249 114.701 126.094 115.127 125.751 115.376C125.584 115.497 125.385 115.566 125.179 115.573C124.973 115.579 124.769 115.524 124.595 115.414L121.937 113.734H121.409L118.752 115.414C118.39 115.64 117.936 115.624 117.595 115.376C117.428 115.256 117.301 115.087 117.23 114.893C117.16 114.699 117.15 114.489 117.202 114.289L117.973 111.258L117.827 110.75Z" fill="#FFA436"/>
+<path d="M134.827 110.75L132.396 108.732C132.237 108.6 132.121 108.423 132.064 108.225C132.006 108.026 132.01 107.815 132.075 107.619C132.138 107.422 132.259 107.25 132.422 107.123C132.585 106.996 132.782 106.922 132.988 106.909L136.13 106.706L136.556 106.387L137.716 103.474C137.874 103.08 138.249 102.825 138.673 102.825C139.098 102.825 139.473 103.08 139.631 103.474V103.474L140.791 106.387L141.217 106.706L144.359 106.909C144.782 106.936 145.141 107.215 145.272 107.619C145.404 108.024 145.277 108.46 144.951 108.732L142.524 110.747L142.371 111.25L143.144 114.289C143.249 114.701 143.094 115.127 142.751 115.376C142.584 115.497 142.385 115.566 142.179 115.573C141.973 115.579 141.769 115.524 141.595 115.414L138.937 113.734H138.409L135.752 115.414C135.39 115.64 134.936 115.624 134.595 115.376C134.428 115.256 134.301 115.087 134.23 114.893C134.16 114.699 134.15 114.489 134.202 114.289L134.973 111.258L134.827 110.75Z" fill="#FFA436"/>
+<path d="M151.827 110.75L149.396 108.732C149.237 108.6 149.121 108.423 149.064 108.225C149.006 108.026 149.01 107.815 149.075 107.619C149.138 107.422 149.259 107.25 149.422 107.123C149.585 106.996 149.782 106.922 149.988 106.909L153.13 106.706L153.556 106.387L154.716 103.474C154.874 103.08 155.249 102.825 155.673 102.825C156.098 102.825 156.473 103.08 156.631 103.474V103.474L157.791 106.387L158.217 106.706L161.359 106.909C161.782 106.936 162.141 107.215 162.272 107.619C162.404 108.024 162.277 108.46 161.951 108.732L159.524 110.747L159.371 111.25L160.144 114.289C160.249 114.701 160.094 115.127 159.751 115.376C159.584 115.497 159.385 115.566 159.179 115.573C158.973 115.579 158.769 115.524 158.595 115.414L155.937 113.734H155.409L152.752 115.414C152.39 115.64 151.936 115.624 151.595 115.376C151.428 115.256 151.301 115.087 151.23 114.893C151.16 114.699 151.15 114.489 151.202 114.289L151.973 111.258L151.827 110.75Z" fill="white"/>
+<path d="M169.431 115.574C169.145 115.574 168.86 115.486 168.617 115.308C168.392 115.146 168.222 114.92 168.127 114.659C168.033 114.398 168.02 114.115 168.089 113.846L168.82 110.977L166.542 109.085C166.329 108.909 166.173 108.672 166.096 108.406C166.019 108.14 166.024 107.857 166.111 107.593C166.196 107.329 166.358 107.097 166.577 106.927C166.796 106.757 167.061 106.657 167.338 106.64L170.292 106.449L171.387 103.697C171.598 103.168 172.103 102.825 172.674 102.825C173.244 102.825 173.749 103.168 173.96 103.697L175.055 106.449L178.009 106.64C178.286 106.657 178.551 106.757 178.77 106.927C178.989 107.097 179.152 107.329 179.236 107.593C179.323 107.857 179.328 108.141 179.251 108.407C179.174 108.674 179.018 108.911 178.804 109.087L176.526 110.978L177.257 113.847C177.326 114.115 177.313 114.399 177.219 114.66C177.125 114.92 176.954 115.147 176.729 115.309C176.505 115.473 176.237 115.566 175.96 115.575C175.682 115.584 175.409 115.508 175.175 115.359L172.674 113.777L170.171 115.359C169.95 115.499 169.693 115.574 169.431 115.574ZM172.674 103.887C172.609 103.886 172.545 103.904 172.491 103.941C172.438 103.977 172.397 104.03 172.374 104.091L171.155 107.153L170.696 107.487L167.406 107.701C167.341 107.703 167.279 107.726 167.228 107.766C167.177 107.805 167.139 107.86 167.12 107.922C167.099 107.983 167.097 108.05 167.115 108.112C167.134 108.174 167.171 108.228 167.222 108.268L169.758 110.376L169.933 110.915L169.119 114.11C169.101 114.172 169.104 114.238 169.126 114.299C169.148 114.36 169.189 114.412 169.242 114.449C169.296 114.489 169.438 114.567 169.604 114.461L172.39 112.7H172.958L175.745 114.461C175.799 114.497 175.863 114.515 175.928 114.513C175.993 114.511 176.055 114.488 176.107 114.449C176.16 114.412 176.2 114.359 176.222 114.298C176.243 114.237 176.246 114.171 176.228 114.109L175.414 110.914L175.589 110.375L178.125 108.268C178.176 108.228 178.214 108.173 178.232 108.111C178.25 108.049 178.248 107.983 178.227 107.922C178.208 107.859 178.17 107.805 178.119 107.765C178.068 107.725 178.006 107.702 177.941 107.7L174.652 107.486L174.192 107.152L172.973 104.09C172.95 104.029 172.909 103.977 172.855 103.941C172.802 103.905 172.738 103.886 172.674 103.887Z" fill="white"/>
+<rect x="64.7253" y="127.8" width="168" height="4.8" rx="2.4" fill="#E0E0E6"/>
+<rect x="64.7251" y="141.4" width="136.8" height="4.8" rx="2.4" fill="#E0E0E6"/>
+<g filter="url(#filter2_d_860_36523)">
+<path d="M301.2 176.485C301.2 171.688 297.312 167.8 292.515 167.8H58.6845C53.8882 167.8 50 171.688 50 176.485V223.916C50 228.712 53.8882 232.6 58.6845 232.6H292.515C297.312 232.6 301.2 228.712 301.2 223.916V176.485Z" fill="white"/>
+</g>
+<path d="M66.8272 185.15L64.3962 183.132C64.2369 183 64.1209 182.824 64.0636 182.625C64.0063 182.426 64.0102 182.215 64.0749 182.019C64.1379 181.822 64.2588 181.65 64.4217 181.523C64.5846 181.396 64.7819 181.322 64.9878 181.309L68.1303 181.106L68.5561 180.787L69.7155 177.874C69.8736 177.48 70.2493 177.225 70.6735 177.225C71.0976 177.225 71.4733 177.48 71.6314 177.874V177.874L72.7908 180.787L73.2167 181.106L76.3591 181.309C76.7824 181.336 77.1411 181.615 77.272 182.019C77.4038 182.424 77.2771 182.86 76.9507 183.132L74.524 185.147L74.371 185.65L75.1445 188.689C75.249 189.101 75.0943 189.527 74.7509 189.776C74.5842 189.897 74.3849 189.966 74.1788 189.973C73.9727 189.979 73.7694 189.924 73.5949 189.814L70.937 188.134H70.4091L67.752 189.814C67.3899 190.04 66.936 190.024 66.5952 189.776C66.4277 189.656 66.3006 189.487 66.2304 189.293C66.1601 189.099 66.1501 188.889 66.2016 188.689L66.9734 185.658L66.8272 185.15Z" fill="#FFA436"/>
+<path d="M83.8272 185.15L81.3962 183.132C81.2369 183 81.1209 182.824 81.0636 182.625C81.0063 182.426 81.0102 182.215 81.0749 182.019C81.1379 181.822 81.2588 181.65 81.4217 181.523C81.5846 181.396 81.7819 181.322 81.9878 181.309L85.1303 181.106L85.5561 180.787L86.7155 177.874C86.8736 177.48 87.2493 177.225 87.6735 177.225C88.0976 177.225 88.4733 177.48 88.6314 177.874V177.874L89.7908 180.787L90.2167 181.106L93.3591 181.309C93.7824 181.336 94.1411 181.615 94.272 182.019C94.4038 182.424 94.2771 182.86 93.9507 183.132L91.524 185.147L91.371 185.65L92.1445 188.689C92.249 189.101 92.0943 189.527 91.7509 189.776C91.5842 189.897 91.3849 189.966 91.1788 189.973C90.9727 189.979 90.7694 189.924 90.5949 189.814L87.937 188.134H87.4091L84.752 189.814C84.3899 190.04 83.936 190.024 83.5952 189.776C83.4277 189.656 83.3006 189.487 83.2304 189.293C83.1601 189.099 83.1501 188.889 83.2016 188.689L83.9734 185.658L83.8272 185.15Z" fill="#FFA436"/>
+<path d="M100.827 185.15L98.3962 183.132C98.2369 183 98.1209 182.824 98.0636 182.625C98.0063 182.426 98.0102 182.215 98.0749 182.019C98.1379 181.822 98.2588 181.65 98.4217 181.523C98.5846 181.396 98.7819 181.322 98.9878 181.309L102.13 181.106L102.556 180.787L103.716 177.874C103.874 177.48 104.249 177.225 104.673 177.225C105.098 177.225 105.473 177.48 105.631 177.874V177.874L106.791 180.787L107.217 181.106L110.359 181.309C110.782 181.336 111.141 181.615 111.272 182.019C111.404 182.424 111.277 182.86 110.951 183.132L108.524 185.147L108.371 185.65L109.144 188.689C109.249 189.101 109.094 189.527 108.751 189.776C108.584 189.897 108.385 189.966 108.179 189.973C107.973 189.979 107.769 189.924 107.595 189.814L104.937 188.134H104.409L101.752 189.814C101.39 190.04 100.936 190.024 100.595 189.776C100.428 189.656 100.301 189.487 100.23 189.293C100.16 189.099 100.15 188.889 100.202 188.689L100.973 185.658L100.827 185.15Z" fill="#FFA436"/>
+<path d="M118.431 189.974C118.145 189.974 117.86 189.886 117.617 189.708C117.392 189.546 117.222 189.32 117.127 189.059C117.033 188.798 117.02 188.515 117.089 188.246L117.82 185.377L115.542 183.485C115.329 183.309 115.173 183.072 115.096 182.806C115.019 182.54 115.024 182.257 115.111 181.993C115.196 181.729 115.358 181.497 115.577 181.327C115.796 181.157 116.061 181.057 116.338 181.04L119.292 180.849L120.387 178.097C120.598 177.568 121.103 177.225 121.674 177.225C122.244 177.225 122.749 177.568 122.96 178.097L124.055 180.849L127.009 181.04C127.286 181.057 127.551 181.157 127.77 181.327C127.989 181.497 128.152 181.729 128.236 181.993C128.323 182.257 128.328 182.541 128.251 182.807C128.174 183.074 128.018 183.311 127.804 183.487L125.526 185.378L126.257 188.247C126.326 188.515 126.313 188.799 126.219 189.06C126.125 189.32 125.954 189.547 125.729 189.709C125.505 189.873 125.237 189.966 124.96 189.975C124.682 189.984 124.409 189.908 124.175 189.759L121.674 188.177L119.171 189.759C118.95 189.899 118.693 189.974 118.431 189.974ZM121.674 178.287C121.609 178.286 121.545 178.305 121.491 178.341C121.438 178.377 121.397 178.43 121.374 178.491L120.155 181.553L119.696 181.887L116.406 182.101C116.341 182.103 116.279 182.126 116.228 182.166C116.177 182.206 116.139 182.26 116.12 182.322C116.099 182.383 116.097 182.45 116.115 182.512C116.134 182.574 116.171 182.629 116.222 182.668L118.758 184.776L118.933 185.315L118.119 188.51C118.101 188.572 118.104 188.638 118.126 188.699C118.148 188.76 118.189 188.812 118.242 188.849C118.296 188.889 118.438 188.967 118.604 188.861L121.39 187.1H121.958L124.745 188.861C124.799 188.897 124.863 188.915 124.928 188.913C124.993 188.911 125.055 188.888 125.107 188.849C125.16 188.812 125.2 188.759 125.222 188.698C125.243 188.637 125.246 188.571 125.228 188.509L124.414 185.314L124.589 184.775L127.125 182.668C127.176 182.628 127.214 182.573 127.232 182.511C127.25 182.449 127.248 182.383 127.227 182.322C127.208 182.26 127.17 182.205 127.119 182.165C127.068 182.125 127.006 182.102 126.941 182.1L123.652 181.886L123.192 181.552L121.973 178.49C121.95 178.429 121.909 178.377 121.855 178.341C121.802 178.305 121.738 178.286 121.674 178.287Z" fill="#FFA436"/>
+<path d="M135.431 189.974C135.145 189.974 134.86 189.886 134.617 189.708C134.392 189.546 134.222 189.32 134.127 189.059C134.033 188.798 134.02 188.515 134.089 188.246L134.82 185.377L132.542 183.485C132.329 183.309 132.173 183.072 132.096 182.806C132.019 182.54 132.024 182.257 132.111 181.993C132.196 181.729 132.358 181.497 132.577 181.327C132.796 181.157 133.061 181.057 133.338 181.04L136.292 180.849L137.387 178.097C137.598 177.568 138.103 177.225 138.674 177.225C139.244 177.225 139.749 177.568 139.96 178.097L141.055 180.849L144.009 181.04C144.286 181.057 144.551 181.157 144.77 181.327C144.989 181.497 145.152 181.729 145.236 181.993C145.323 182.257 145.328 182.541 145.251 182.807C145.174 183.074 145.018 183.311 144.804 183.487L142.526 185.378L143.257 188.247C143.326 188.515 143.313 188.799 143.219 189.06C143.125 189.32 142.954 189.547 142.729 189.709C142.505 189.873 142.237 189.966 141.96 189.975C141.682 189.984 141.409 189.908 141.175 189.759L138.674 188.177L136.171 189.759C135.95 189.899 135.693 189.974 135.431 189.974ZM138.674 178.287C138.609 178.286 138.545 178.305 138.491 178.341C138.438 178.377 138.397 178.43 138.374 178.491L137.155 181.553L136.696 181.887L133.406 182.101C133.341 182.103 133.279 182.126 133.228 182.166C133.177 182.206 133.139 182.26 133.12 182.322C133.099 182.383 133.097 182.45 133.115 182.512C133.134 182.574 133.171 182.629 133.222 182.668L135.758 184.776L135.933 185.315L135.119 188.51C135.101 188.572 135.104 188.638 135.126 188.699C135.148 188.76 135.189 188.812 135.242 188.849C135.296 188.889 135.438 188.967 135.604 188.861L138.39 187.1H138.958L141.745 188.861C141.799 188.897 141.863 188.915 141.928 188.913C141.993 188.911 142.055 188.888 142.107 188.849C142.16 188.812 142.2 188.759 142.222 188.698C142.243 188.637 142.246 188.571 142.228 188.509L141.414 185.314L141.589 184.775L144.125 182.668C144.176 182.628 144.214 182.573 144.232 182.511C144.25 182.449 144.248 182.383 144.227 182.322C144.208 182.26 144.17 182.205 144.119 182.165C144.068 182.125 144.006 182.102 143.941 182.1L140.652 181.886L140.192 181.552L138.973 178.49C138.95 178.429 138.909 178.377 138.855 178.341C138.802 178.305 138.738 178.286 138.674 178.287Z" fill="#FFA436"/>
+<path d="M151.827 185.15L149.396 183.132C149.237 183 149.121 182.824 149.064 182.625C149.006 182.426 149.01 182.215 149.075 182.019C149.138 181.822 149.259 181.65 149.422 181.523C149.585 181.396 149.782 181.322 149.988 181.309L153.13 181.106L153.556 180.787L154.716 177.874C154.874 177.48 155.249 177.225 155.673 177.225C156.098 177.225 156.473 177.48 156.631 177.874V177.874L157.791 180.787L158.217 181.106L161.359 181.309C161.782 181.336 162.141 181.615 162.272 182.019C162.404 182.424 162.277 182.86 161.951 183.132L159.524 185.147L159.371 185.65L160.144 188.689C160.249 189.101 160.094 189.527 159.751 189.776C159.584 189.897 159.385 189.966 159.179 189.973C158.973 189.979 158.769 189.924 158.595 189.814L155.937 188.134H155.409L152.752 189.814C152.39 190.04 151.936 190.024 151.595 189.776C151.428 189.656 151.301 189.487 151.23 189.293C151.16 189.099 151.15 188.889 151.202 188.689L151.973 185.658L151.827 185.15Z" fill="white"/>
+<path d="M169.431 189.974C169.145 189.974 168.86 189.886 168.617 189.708C168.392 189.546 168.222 189.32 168.127 189.059C168.033 188.798 168.02 188.515 168.089 188.246L168.82 185.377L166.542 183.485C166.329 183.309 166.173 183.072 166.096 182.806C166.019 182.54 166.024 182.257 166.111 181.993C166.196 181.729 166.358 181.497 166.577 181.327C166.796 181.157 167.061 181.057 167.338 181.04L170.292 180.849L171.387 178.097C171.598 177.568 172.103 177.225 172.674 177.225C173.244 177.225 173.749 177.568 173.96 178.097L175.055 180.849L178.009 181.04C178.286 181.057 178.551 181.157 178.77 181.327C178.989 181.497 179.152 181.729 179.236 181.993C179.323 182.257 179.328 182.541 179.251 182.807C179.174 183.074 179.018 183.311 178.804 183.487L176.526 185.378L177.257 188.247C177.326 188.515 177.313 188.799 177.219 189.06C177.125 189.32 176.954 189.547 176.729 189.709C176.505 189.873 176.237 189.966 175.96 189.975C175.682 189.984 175.409 189.908 175.175 189.759L172.674 188.177L170.171 189.759C169.95 189.899 169.693 189.974 169.431 189.974ZM172.674 178.287C172.609 178.286 172.545 178.305 172.491 178.341C172.438 178.377 172.397 178.43 172.374 178.491L171.155 181.553L170.696 181.887L167.406 182.101C167.341 182.103 167.279 182.126 167.228 182.166C167.177 182.206 167.139 182.26 167.12 182.322C167.099 182.383 167.097 182.45 167.115 182.512C167.134 182.574 167.171 182.629 167.222 182.668L169.758 184.776L169.933 185.315L169.119 188.51C169.101 188.572 169.104 188.638 169.126 188.699C169.148 188.76 169.189 188.812 169.242 188.849C169.296 188.889 169.438 188.967 169.604 188.861L172.39 187.1H172.958L175.745 188.861C175.799 188.897 175.863 188.915 175.928 188.913C175.993 188.911 176.055 188.888 176.107 188.849C176.16 188.812 176.2 188.759 176.222 188.698C176.243 188.637 176.246 188.571 176.228 188.509L175.414 185.314L175.589 184.775L178.125 182.668C178.176 182.628 178.214 182.573 178.232 182.511C178.25 182.449 178.248 182.383 178.227 182.322C178.208 182.26 178.17 182.205 178.119 182.165C178.068 182.125 178.006 182.102 177.941 182.1L174.652 181.886L174.192 181.552L172.973 178.49C172.95 178.429 172.909 178.377 172.855 178.341C172.802 178.305 172.738 178.286 172.674 178.287Z" fill="white"/>
+<rect x="64.7253" y="202.2" width="168" height="4.8" rx="2.4" fill="#E0E0E6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.4211 126C37.4211 121.357 33.6432 117.579 29 117.579C24.3568 117.579 20.5789 121.357 20.5789 126C20.5789 130.643 24.3568 134.421 29 134.421C33.6432 134.421 37.4211 130.643 37.4211 126ZM19 126C19 120.477 23.4768 116 29 116C34.5232 116 39 120.477 39 126C39 131.523 34.5232 136 29 136C23.4768 136 19 131.523 19 126ZM29.0001 120.737C28.7907 120.737 28.5899 120.82 28.4419 120.968C28.2938 121.116 28.2106 121.317 28.2106 121.526V127.316C28.2106 127.525 28.2938 127.726 28.4419 127.874C28.5899 128.022 28.7907 128.105 29.0001 128.105C29.2095 128.105 29.4103 128.022 29.5584 127.874C29.7064 127.726 29.7896 127.525 29.7896 127.316V121.526C29.7896 121.317 29.7064 121.116 29.5584 120.968C29.4103 120.82 29.2095 120.737 29.0001 120.737ZM28.5264 131.263L28.2106 130.947V130L28.5264 129.684H29.4738L29.7896 130V130.947L29.4738 131.263H28.5264Z" fill="#EE0B0B"/>
+</g>
+<defs>
+<filter id="filter0_d_860_36523" x="44.8842" y="15.5895" width="261.432" height="75.0316" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1.70526"/>
+<feGaussianBlur stdDeviation="2.55789"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.227451 0 0 0 0 0.223529 0 0 0 0 0.266667 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_860_36523"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_860_36523" result="shape"/>
+</filter>
+<filter id="filter1_d_860_36523" x="44.0842" y="89.1894" width="263.032" height="76.6316" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1.70526"/>
+<feGaussianBlur stdDeviation="2.55789"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.227451 0 0 0 0 0.223529 0 0 0 0 0.266667 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_860_36523"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_860_36523" result="shape"/>
+</filter>
+<filter id="filter2_d_860_36523" x="44.8842" y="164.39" width="261.432" height="75.0316" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1.70526"/>
+<feGaussianBlur stdDeviation="2.55789"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.227451 0 0 0 0 0.223529 0 0 0 0 0.266667 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_860_36523"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_860_36523" result="shape"/>
+</filter>
+<clipPath id="clip0_860_36523">
+<rect width="352" height="214" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/browser/components/shopping/content/assets/shipping.svg b/browser/components/shopping/content/assets/shipping.svg
new file mode 100644
index 0000000000..d9b01a0a96
--- /dev/null
+++ b/browser/components/shopping/content/assets/shipping.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path fill-rule="evenodd" d="M10.5 14c.456.607 1.182 1 2 1 .818 0 1.544-.393 2-1h.25c.69 0 1.25-.56 1.25-1.25V8.777a.75.75 0 0 0-.1-.375l-1.75-3.027A.75.75 0 0 0 13.5 5H11a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v7a2 2 0 0 0 1.443 1.921A2.497 2.497 0 0 0 3.5 15c.818 0 1.544-.393 2-1h5ZM2 4.5a.5.5 0 0 0-.5.5v6A2.5 2.5 0 0 1 6 12.5h3.5V5a.5.5 0 0 0-.5-.5H2ZM14.5 9v-.022L13.067 6.5H11V9h3.5Zm-12 3.5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm9 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/shopping.svg b/browser/components/shopping/content/assets/shopping.svg
new file mode 100644
index 0000000000..5b81219d48
--- /dev/null
+++ b/browser/components/shopping/content/assets/shopping.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
+<path d="M9.879 0H4v1.5h5.567l5.37 5.393L16 5.835 10.41.22A.75.75 0 0 0 9.88 0ZM6.346 8.116l1.06-1.06-1.06-1.06-1.06 1.06 1.06 1.06Z"/><path d="M2.119 3.75a.75.75 0 0 1 .75-.75h6.01a.75.75 0 0 1 .53.22l5.057 5.056a2 2 0 0 1 0 2.828l-4.243 4.243a2 2 0 0 1-2.828 0l-5.056-5.056a.75.75 0 0 1-.22-.53V3.75Zm1.5.75v4.95l4.836 4.837a.5.5 0 0 0 .708 0l4.243-4.243a.5.5 0 0 0 0-.708L8.568 4.5H3.62Z"/>
+</svg>
diff --git a/browser/components/shopping/content/assets/unanalyzedDark.avif b/browser/components/shopping/content/assets/unanalyzedDark.avif
new file mode 100644
index 0000000000..66efc3bc6e
--- /dev/null
+++ b/browser/components/shopping/content/assets/unanalyzedDark.avif
Binary files differ
diff --git a/browser/components/shopping/content/assets/unanalyzedLight.avif b/browser/components/shopping/content/assets/unanalyzedLight.avif
new file mode 100644
index 0000000000..28cd1c26f1
--- /dev/null
+++ b/browser/components/shopping/content/assets/unanalyzedLight.avif
Binary files differ
diff --git a/browser/components/shopping/content/highlight-item.css b/browser/components/shopping/content/highlight-item.css
new file mode 100644
index 0000000000..2139363862
--- /dev/null
+++ b/browser/components/shopping/content/highlight-item.css
@@ -0,0 +1,66 @@
+/* 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/. */
+
+/* Separate logo, label and details into rows and columns */
+.highlight-item-wrapper {
+ align-items: center;
+ display: grid;
+ column-gap: 1em;
+ grid-template-columns: 1rem auto;
+ grid-template-rows: 1rem auto;
+}
+
+.highlight-icon {
+ content: url("chrome://global/skin/icons/defaultFavicon.svg");
+ color: var(--icon-color);
+ fill: currentColor;
+ -moz-context-properties: fill;
+ grid-row-start: 1;
+ grid-column-start: 1;
+
+ &.price {
+ content: url("chrome://browser/content/shopping/assets/price.svg");
+ }
+
+ &.quality {
+ content: url("chrome://browser/content/shopping/assets/quality.svg");
+ }
+
+ &.shipping {
+ content: url("chrome://browser/content/shopping/assets/shipping.svg");
+ }
+
+ &.competitiveness {
+ content: url("chrome://browser/content/shopping/assets/competitiveness.svg");
+ }
+
+ &.packaging\/appearance {
+ content: url("chrome://browser/content/shopping/assets/packaging.svg");
+ }
+}
+
+.highlight-label {
+ font-weight: 590;
+ grid-column-start: 2;
+ grid-row-start: 1;
+}
+
+.highlight-details {
+ grid-column-start: 2;
+ grid-row-start: 2;
+ margin: 0;
+ padding: 0;
+}
+
+.highlight-details-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.highlight-details-list > li {
+ /* Render LTR since English review snippets are only supported at this time. */
+ direction: ltr;
+ margin: 1em 0;
+}
diff --git a/browser/components/shopping/content/highlight-item.mjs b/browser/components/shopping/content/highlight-item.mjs
new file mode 100644
index 0000000000..a61764dc86
--- /dev/null
+++ b/browser/components/shopping/content/highlight-item.mjs
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+/**
+ * Class for displaying a list of highlighted product reviews, according to highlight category.
+ */
+class Highlight extends MozLitElement {
+ l10nId;
+ highlightType;
+ /**
+ * reviews is a list of Strings, representing all the reviews to display
+ * under a highlight category.
+ */
+ reviews;
+
+ /**
+ * lang defines the language in which the reviews are written. We should specify
+ * language so that screen readers can read text with the appropriate language packs.
+ */
+ lang;
+
+ render() {
+ let ulTemplate = [];
+
+ for (let review of this.reviews) {
+ ulTemplate.push(
+ html`<li>
+ <q><span lang=${this.lang}>${review}</span></q>
+ </li>`
+ );
+ }
+
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/highlight-item.css"
+ />
+ <div class="highlight-item-wrapper">
+ <span class="highlight-icon ${this.highlightType}"></span>
+ <dt class="highlight-label" data-l10n-id=${this.l10nId}></dt>
+ <dd class="highlight-details">
+ <ul class="highlight-details-list">
+ ${ulTemplate}
+ </ul>
+ </dd>
+ </div>
+ `;
+ }
+}
+
+customElements.define("highlight-item", Highlight);
diff --git a/browser/components/shopping/content/highlights.mjs b/browser/components/shopping/content/highlights.mjs
new file mode 100644
index 0000000000..09ed055b28
--- /dev/null
+++ b/browser/components/shopping/content/highlights.mjs
@@ -0,0 +1,124 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/highlight-item.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-card.mjs";
+
+const VALID_HIGHLIGHT_L10N_IDs = new Map([
+ ["price", "shopping-highlight-price"],
+ ["quality", "shopping-highlight-quality"],
+ ["shipping", "shopping-highlight-shipping"],
+ ["competitiveness", "shopping-highlight-competitiveness"],
+ ["packaging/appearance", "shopping-highlight-packaging"],
+]);
+
+/**
+ * Class for displaying all available highlight categories for a product and any
+ * highlight reviews per category.
+ */
+class ReviewHighlights extends MozLitElement {
+ /**
+ * highlightsMap is a map of highlight categories to an array of reviews per category.
+ * It is possible for a category to have no reviews.
+ */
+ #highlightsMap;
+
+ static properties = {
+ highlights: { type: Object },
+ lang: { type: String, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ reviewHighlightsListEl: "#review-highlights-list",
+ };
+ }
+
+ updateHighlightsMap() {
+ let availableKeys;
+ this.#highlightsMap = null;
+
+ try {
+ if (!this.highlights) {
+ return;
+ }
+
+ // Filter highlights that have data.
+ let keys = Object.keys(this.highlights);
+ availableKeys = keys.filter(
+ key => Object.values(this.highlights[key]).flat().length !== 0
+ );
+
+ // Filter valid highlight category types. Valid types are guaranteed to have data-l10n-ids.
+ availableKeys = availableKeys.filter(key =>
+ VALID_HIGHLIGHT_L10N_IDs.has(key)
+ );
+
+ // If there are no highlights to show in the end, stop here and don't render content.
+ if (!availableKeys.length) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ this.#highlightsMap = new Map();
+
+ for (let key of availableKeys) {
+ // Ignore negative,neutral,positive sentiments and simply append review strings into one array.
+ let reviews = Object.values(this.highlights[key]).flat();
+ this.#highlightsMap.set(key, reviews);
+ }
+ }
+
+ createHighlightElement(type, reviews) {
+ let highlightEl = document.createElement("highlight-item");
+ // We already verify highlight type and its l10n id in updateHighlightsMap.
+ let l10nId = VALID_HIGHLIGHT_L10N_IDs.get(type);
+ highlightEl.id = type;
+ highlightEl.l10nId = l10nId;
+ highlightEl.highlightType = type;
+ highlightEl.reviews = reviews;
+ highlightEl.lang = this.lang;
+ return highlightEl;
+ }
+
+ render() {
+ this.updateHighlightsMap();
+
+ if (!this.#highlightsMap) {
+ this.hidden = true;
+ return null;
+ }
+
+ this.hidden = false;
+
+ let highlightsTemplate = [];
+ for (let [key, value] of this.#highlightsMap) {
+ let highlightEl = this.createHighlightElement(key, value);
+ highlightsTemplate.push(highlightEl);
+ }
+
+ return html`
+ <shopping-card
+ data-l10n-id="shopping-highlights-label"
+ data-l10n-attrs="label"
+ type="show-more"
+ >
+ <div slot="content" id="review-highlights-wrapper">
+ <dl id="review-highlights-list">${highlightsTemplate}</dl>
+ </div>
+ </shopping-card>
+ `;
+ }
+}
+
+customElements.define("review-highlights", ReviewHighlights);
diff --git a/browser/components/shopping/content/letter-grade.css b/browser/components/shopping/content/letter-grade.css
new file mode 100644
index 0000000000..75be411151
--- /dev/null
+++ b/browser/components/shopping/content/letter-grade.css
@@ -0,0 +1,132 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+:host {
+ --background-term-a: #B3FFE3;
+ --background-term-b: #80EBFF;
+ --background-term-c: #FFEA80;
+ --background-term-d: #FFB587;
+ --background-term-f: #FF848B;
+ --in-content-box-border-color: rgba(0, 0, 0, 0.15);
+ --inner-border: 1px solid var(--in-content-box-border-color);
+ --letter-grade-width: 1.5rem;
+ --letter-grade-term-inline-padding: 0.25rem;
+}
+
+#letter-grade-wrapper {
+ border-radius: 4px;
+ color: #000;
+ display: flex;
+ font-weight: 600;
+ line-height: 150%;
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+
+#letter-grade-term {
+ align-items: center;
+ align-self: stretch;
+ box-sizing: border-box;
+ display: flex;
+ flex-shrink: 0;
+ font-size: 1em;
+ justify-content: center;
+ margin: 0;
+ padding: 0.0625rem var(--letter-grade-term-inline-padding);
+ text-align: center;
+ width: var(--letter-grade-width);
+}
+
+:host([showdescription]) #letter-grade-term {
+ /* For border "shadow" inside the container */
+ border-block: var(--inner-border);
+ border-inline-start: var(--inner-border);
+ border-start-start-radius: 4px;
+ border-end-start-radius: 4px;
+ /* Add 1px padding so that the letter does not shift when changing
+ * between the show description and no description variants. */
+ padding-inline-end: calc(var(--letter-grade-term-inline-padding) + 1px);
+}
+
+:host(:not([showdescription])) #letter-grade-term {
+ border: var(--inner-border);
+ border-radius: 4px;
+}
+
+#letter-grade-description {
+ /* For border "shadow" inside the container */
+ border-block: var(--inner-border);
+ border-inline-end: var(--inner-border);
+ border-start-end-radius: 4px;
+ border-end-end-radius: 4px;
+
+ align-items: center;
+ align-self: stretch;
+ box-sizing: border-box;
+ display: flex;
+ font-size: 0.87rem;
+ font-weight: var(--font-weight-default);
+ margin: 0;
+ padding: 0.125rem 0.5rem;
+}
+
+/* Letter grade colors */
+
+:host([letter="A"]) #letter-grade-term {
+ background-color: var(--background-term-a);
+}
+
+:host([letter="A"]) #letter-grade-description {
+ background-color: rgba(231, 255, 246, 1);
+}
+
+:host([letter="B"]) #letter-grade-term {
+ background-color: var(--background-term-b);
+}
+
+:host([letter="B"]) #letter-grade-description {
+ background-color: rgba(222, 250, 255, 1);
+}
+
+:host([letter="C"]) #letter-grade-term {
+ background-color: var(--background-term-c);
+}
+
+:host([letter="C"]) #letter-grade-description {
+ background-color: rgba(255, 249, 218, 1);
+}
+
+:host([letter="D"]) #letter-grade-term {
+ background-color: var(--background-term-d);
+}
+
+:host([letter="D"]) #letter-grade-description {
+ background-color: rgba(252, 230, 213, 1);
+}
+
+:host([letter="F"]) #letter-grade-term {
+ background-color: var(--background-term-f);
+}
+
+:host([letter="F"]) #letter-grade-description {
+ background-color: rgba(255, 228, 230, 1);
+}
+
+@media (prefers-contrast) {
+ :host {
+ --in-content-box-border-color: unset;
+ }
+
+ #letter-grade-term {
+ background-color: var(--in-content-page-color) !important;
+ color: var(--in-content-page-background) !important;
+ }
+
+ #letter-grade-description {
+ background-color: var(--in-content-page-background) !important;
+ color: var(--in-content-page-color) !important;
+ }
+}
diff --git a/browser/components/shopping/content/letter-grade.mjs b/browser/components/shopping/content/letter-grade.mjs
new file mode 100644
index 0000000000..71badb1c45
--- /dev/null
+++ b/browser/components/shopping/content/letter-grade.mjs
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+const VALID_LETTER_GRADE_L10N_IDS = new Map([
+ ["A", "shopping-letter-grade-description-ab"],
+ ["B", "shopping-letter-grade-description-ab"],
+ ["C", "shopping-letter-grade-description-c"],
+ ["D", "shopping-letter-grade-description-df"],
+ ["F", "shopping-letter-grade-description-df"],
+]);
+
+class LetterGrade extends MozLitElement {
+ #descriptionL10N;
+
+ static properties = {
+ letter: { type: String, reflect: true },
+ showdescription: { type: Boolean, reflect: true },
+ };
+
+ get fluentStrings() {
+ if (!this._fluentStrings) {
+ this._fluentStrings = new Localization(["browser/shopping.ftl"], true);
+ }
+ return this._fluentStrings;
+ }
+
+ descriptionTemplate() {
+ if (this.showdescription) {
+ return html`<p
+ id="letter-grade-description"
+ data-l10n-id=${this.#descriptionL10N}
+ ></p>`;
+ }
+
+ return null;
+ }
+
+ render() {
+ // Do not render if letter is invalid
+ if (!VALID_LETTER_GRADE_L10N_IDS.has(this.letter)) {
+ return null;
+ }
+
+ this.#descriptionL10N = VALID_LETTER_GRADE_L10N_IDS.get(this.letter);
+ let tooltipL10NArgs;
+ // Do not localize tooltip if using Storybook.
+ if (!window.IS_STORYBOOK) {
+ const localizedDescription = this.fluentStrings.formatValueSync(
+ this.#descriptionL10N
+ );
+ tooltipL10NArgs = `{"letter": "${this.letter}", "description": "${localizedDescription}"}`;
+ }
+
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/letter-grade.css"
+ />
+ <article
+ id="letter-grade-wrapper"
+ data-l10n-id="shopping-letter-grade-tooltip"
+ data-l10n-attrs="title"
+ data-l10n-args=${tooltipL10NArgs}
+ >
+ <p id="letter-grade-term">${this.letter}</p>
+ ${this.descriptionTemplate()}
+ </article>
+ `;
+ }
+}
+
+customElements.define("letter-grade", LetterGrade);
diff --git a/browser/components/shopping/content/onboarding.mjs b/browser/components/shopping/content/onboarding.mjs
new file mode 100644
index 0000000000..68124c0253
--- /dev/null
+++ b/browser/components/shopping/content/onboarding.mjs
@@ -0,0 +1,69 @@
+/* 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/. */
+
+const BUNDLE_SRC =
+ "chrome://browser/content/aboutwelcome/aboutwelcome.bundle.js";
+
+class Onboarding {
+ constructor({ win } = {}) {
+ this.doc = win.document;
+ win.addEventListener("RenderWelcome", () => this._addScriptsAndRender(), {
+ once: true,
+ });
+ }
+
+ async _addScriptsAndRender() {
+ const addStylesheet = href => {
+ if (this.doc.head.querySelector(`link[href="${href}"]`)) {
+ return;
+ }
+ const link = this.doc.head.appendChild(this.doc.createElement("link"));
+ link.rel = "stylesheet";
+ link.href = href;
+ };
+ addStylesheet("chrome://browser/content/aboutwelcome/aboutwelcome.css");
+ const reactSrc = "resource://activity-stream/vendor/react.js";
+ const domSrc = "resource://activity-stream/vendor/react-dom.js";
+ // Add React script
+ const getReactReady = async () => {
+ return new Promise(resolve => {
+ let reactScript = this.doc.createElement("script");
+ reactScript.src = reactSrc;
+ this.doc.head.appendChild(reactScript);
+ reactScript.addEventListener("load", resolve);
+ });
+ };
+ // Add ReactDom script
+ const getDomReady = async () => {
+ return new Promise(resolve => {
+ let domScript = this.doc.createElement("script");
+ domScript.src = domSrc;
+ this.doc.head.appendChild(domScript);
+ domScript.addEventListener("load", resolve);
+ });
+ };
+ // Load React, then React Dom
+ if (!this.doc.querySelector(`[src="${reactSrc}"]`)) {
+ await getReactReady();
+ }
+ if (!this.doc.querySelector(`[src="${domSrc}"]`)) {
+ await getDomReady();
+ }
+ // Load the bundle to render the content as configured.
+ this.doc.querySelector(`[src="${BUNDLE_SRC}"]`)?.remove();
+ let bundleScript = this.doc.createElement("script");
+ bundleScript.src = BUNDLE_SRC;
+ this.doc.head.appendChild(bundleScript);
+ }
+
+ static getOnboarding() {
+ if (!this.onboarding) {
+ this.onboarding = new Onboarding({ win: window });
+ }
+ return this.onboarding;
+ }
+}
+
+const OnboardingContainer = Onboarding.getOnboarding();
+export default OnboardingContainer;
diff --git a/browser/components/shopping/content/recommended-ad.css b/browser/components/shopping/content/recommended-ad.css
new file mode 100644
index 0000000000..20f0775ce0
--- /dev/null
+++ b/browser/components/shopping/content/recommended-ad.css
@@ -0,0 +1,59 @@
+/* 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/. */
+
+#recommended-ad-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ text-decoration: none;
+ color: var(--in-content-text-color);
+}
+
+#recommended-ad-wrapper:hover {
+ cursor: pointer;
+}
+
+#recommended-ad-wrapper:hover #ad-title {
+ text-decoration: underline;
+ color: var(--link-color-hover);
+}
+
+#recommended-ad-wrapper:focus-visible {
+ outline-offset: 4px;
+ border-radius: 1px;
+}
+
+#ad-content {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ height: 80px;
+}
+
+#ad-preview-image {
+ max-width: 80px;
+ max-height: 80px;
+}
+
+#ad-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ height: fit-content;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ /* This text won't be localized and when in RTL, the ellipsis is positioned
+ awkwardly so we are forcing LTR */
+ direction: ltr;
+}
+
+#price {
+ font-size: 1em;
+ font-weight: 600;
+}
+
+#footer {
+ display: flex;
+ justify-content: space-between;
+}
diff --git a/browser/components/shopping/content/recommended-ad.mjs b/browser/components/shopping/content/recommended-ad.mjs
new file mode 100644
index 0000000000..09351ffe85
--- /dev/null
+++ b/browser/components/shopping/content/recommended-ad.mjs
@@ -0,0 +1,150 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-card.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-five-star.mjs";
+
+const AD_IMPRESSION_TIMEOUT = 1500;
+
+class RecommendedAd extends MozLitElement {
+ static properties = {
+ product: { type: Object, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ letterGradeEl: "letter-grade",
+ linkEl: "#recommended-ad-wrapper",
+ priceEl: "#price",
+ ratingEl: "moz-five-star",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ document.addEventListener("visibilitychange", this);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ document.removeEventListener("visibilitychange", this);
+ this.resetImpressionTimer();
+ this.revokeImageUrl();
+ }
+
+ startImpressionTimer() {
+ if (!this.timeout && document.visibilityState === "visible") {
+ this.timeout = setTimeout(
+ () => this.recordImpression(),
+ AD_IMPRESSION_TIMEOUT
+ );
+ }
+ }
+
+ resetImpressionTimer() {
+ this.timeout = clearTimeout(this.timeout);
+ }
+
+ revokeImageUrl() {
+ if (this.imageUrl) {
+ URL.revokeObjectURL(this.imageUrl);
+ }
+ }
+
+ recordImpression() {
+ if (this.hasImpressed) {
+ return;
+ }
+
+ this.dispatchEvent(
+ new CustomEvent("AdImpression", {
+ bubbles: true,
+ detail: { aid: this.product.aid },
+ })
+ );
+
+ document.removeEventListener("visibilitychange", this);
+ this.resetImpressionTimer();
+
+ this.hasImpressed = true;
+ }
+
+ handleClick(event) {
+ if (event.button === 0 || event.button === 1) {
+ this.dispatchEvent(
+ new CustomEvent("AdClicked", {
+ bubbles: true,
+ detail: { aid: this.product.aid },
+ })
+ );
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type !== "visibilitychange") {
+ return;
+ }
+ if (document.visibilityState === "visible") {
+ this.startImpressionTimer();
+ } else if (!this.hasImpressed) {
+ this.resetImpressionTimer();
+ }
+ }
+
+ priceTemplate() {
+ // We are only showing prices in USD for now. In the future we will need
+ // to update this to include other currencies.
+ return html`<span id="price">$${this.product.price}</span>`;
+ }
+
+ render() {
+ this.startImpressionTimer();
+
+ this.revokeImageUrl();
+ this.imageUrl = URL.createObjectURL(this.product.image_blob);
+
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/recommended-ad.css"
+ />
+ <shopping-card
+ data-l10n-id="more-to-consider-ad-label"
+ data-l10n-attrs="label"
+ >
+ <a id="recommended-ad-wrapper" slot="content" href=${
+ this.product.url
+ } target="_blank" title="${this.product.name}" @click=${
+ this.handleClick
+ } @auxclick=${this.handleClick}>
+ <div id="ad-content">
+ <img id="ad-preview-image" src=${this.imageUrl}></img>
+ <span id="ad-title" lang="en">${this.product.name}</span>
+ <letter-grade letter="${this.product.grade}"></letter-grade>
+ </div>
+ <div id="footer">
+ ${this.priceTemplate()}
+ <moz-five-star rating=${
+ this.product.adjusted_rating
+ }></moz-five-star>
+ </div>
+ </a>
+ </shopping-card>
+ <p data-l10n-id="ad-by-fakespot"></p>
+ `;
+ }
+}
+
+customElements.define("recommended-ad", RecommendedAd);
diff --git a/browser/components/shopping/content/reliability.mjs b/browser/components/shopping/content/reliability.mjs
new file mode 100644
index 0000000000..1e46b30c4b
--- /dev/null
+++ b/browser/components/shopping/content/reliability.mjs
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/letter-grade.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-card.mjs";
+
+class ReviewReliability extends MozLitElement {
+ static properties = {
+ letter: { type: String },
+ };
+
+ static get queries() {
+ return {
+ letterGradeEl: "letter-grade",
+ };
+ }
+
+ render() {
+ if (!this.letter) {
+ this.hidden = true;
+ return null;
+ }
+
+ return html`
+ <shopping-card
+ data-l10n-id="shopping-review-reliability-label"
+ data-l10n-attrs="label"
+ >
+ <div slot="content">
+ <letter-grade
+ letter=${ifDefined(this.letter)}
+ showdescription
+ ></letter-grade>
+ </div>
+ </shopping-card>
+ `;
+ }
+}
+
+customElements.define("review-reliability", ReviewReliability);
diff --git a/browser/components/shopping/content/settings.css b/browser/components/shopping/content/settings.css
new file mode 100644
index 0000000000..cd0f497a23
--- /dev/null
+++ b/browser/components/shopping/content/settings.css
@@ -0,0 +1,69 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+#shopping-settings-wrapper {
+ --shopping-settings-between-label-and-control-option-gap: 4px;
+ display: grid;
+ grid-template-rows: auto;
+ row-gap: 8px;
+ margin-top: 12px;
+
+ .shopping-settings-toggle-option-wrapper {
+ display: grid;
+ row-gap: var(--shopping-settings-between-label-and-control-option-gap);
+ }
+
+ #shopping-settings-opt-out-section {
+ display: grid;
+ justify-content: center;
+
+ #shopping-settings-opt-out-button {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ #powered-by-fakespot {
+ font-size: 12px;
+ color: var(--text-color-deemphasized);
+ }
+
+ #shopping-settings-toggles-section {
+ display: grid;
+ row-gap: 8px;
+
+ #shopping-ads-learn-more,
+ #shopping-auto-open-description {
+ margin-inline-end: 20px;
+ color: var(--text-color-deemphasized);
+ }
+ }
+
+ /* When `browser.shopping.experience2023.autoOpen` is true. */
+ &.shopping-settings-auto-open-ui-enabled {
+ --shopping-settings-between-options-gap: 12px;
+ row-gap: var(--shopping-settings-between-options-gap);
+
+ #shopping-settings-toggles-section {
+ display: grid;
+ row-gap: var(--shopping-settings-between-options-gap);
+ }
+
+ .divider {
+ border: var(--shopping-card-border-width) solid var(--shopping-card-border-color);
+ }
+
+ #shopping-settings-opt-out-section {
+ justify-content: flex-start;
+ row-gap: var(--shopping-settings-between-label-and-control-option-gap);
+
+ #shopping-settings-opt-out-button {
+ width: fit-content;
+ margin-inline-start: 0;
+ }
+ }
+ }
+}
diff --git a/browser/components/shopping/content/settings.mjs b/browser/components/shopping/content/settings.mjs
new file mode 100644
index 0000000000..7619bbb131
--- /dev/null
+++ b/browser/components/shopping/content/settings.mjs
@@ -0,0 +1,210 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-toggle.mjs";
+
+class ShoppingSettings extends MozLitElement {
+ static properties = {
+ adsEnabled: { type: Boolean },
+ adsEnabledByUser: { type: Boolean },
+ autoOpenEnabled: { type: Boolean },
+ autoOpenEnabledByUser: { type: Boolean },
+ hostname: { type: String },
+ };
+
+ static get queries() {
+ return {
+ wrapperEl: "#shopping-settings-wrapper",
+ recommendationsToggleEl: "#shopping-settings-recommendations-toggle",
+ autoOpenToggleEl: "#shopping-settings-auto-open-toggle",
+ autoOpenToggleDescriptionEl: "#shopping-auto-open-description",
+ dividerEl: ".divider",
+ sidebarEnabledStateEl: "#shopping-settings-sidebar-enabled-state",
+ optOutButtonEl: "#shopping-settings-opt-out-button",
+ shoppingCardEl: "shopping-card",
+ adsLearnMoreLinkEl: "#shopping-ads-learn-more-link",
+ fakespotLearnMoreLinkEl: "#powered-by-fakespot-link",
+ };
+ }
+
+ onToggleRecommendations() {
+ this.adsEnabledByUser = this.recommendationsToggleEl.pressed;
+ let action = this.adsEnabledByUser ? "enabled" : "disabled";
+ Glean.shopping.surfaceAdsSettingToggled.record({ action });
+ RPMSetPref(
+ "browser.shopping.experience2023.ads.userEnabled",
+ this.adsEnabledByUser
+ );
+ }
+
+ onToggleAutoOpen() {
+ this.autoOpenEnabledByUser = this.autoOpenToggleEl.pressed;
+ let action = this.autoOpenEnabledByUser ? "enabled" : "disabled";
+ Glean.shopping.surfaceAutoOpenSettingToggled.record({ action });
+ RPMSetPref(
+ "browser.shopping.experience2023.autoOpen.userEnabled",
+ this.autoOpenEnabledByUser
+ );
+ if (!this.autoOpenEnabledByUser) {
+ RPMSetPref("browser.shopping.experience2023.active", false);
+ }
+ }
+
+ onDisableShopping() {
+ window.dispatchEvent(
+ new CustomEvent("DisableShopping", { bubbles: true, composed: true })
+ );
+ Glean.shopping.surfaceOptOutButtonClicked.record();
+ }
+
+ fakespotLinkClicked(e) {
+ if (e.target.localName == "a" && e.button == 0) {
+ Glean.shopping.surfacePoweredByFakespotLinkClicked.record();
+ }
+ }
+
+ render() {
+ // Whether we show recommendations at all (including offering a user
+ // control for them) is controlled via a nimbus-enabled pref.
+ let canShowRecommendationToggle = this.adsEnabled;
+
+ let adsToggleMarkup = canShowRecommendationToggle
+ ? html`
+ <div class="shopping-settings-toggle-option-wrapper">
+ <moz-toggle
+ id="shopping-settings-recommendations-toggle"
+ ?pressed=${this.adsEnabledByUser}
+ data-l10n-id="shopping-settings-recommendations-toggle"
+ data-l10n-attrs="label"
+ @toggle=${this.onToggleRecommendations}>
+ </moz-toggle/>
+ <span id="shopping-ads-learn-more" data-l10n-id="shopping-settings-recommendations-learn-more2">
+ <a
+ id="shopping-ads-learn-more-link"
+ target="_blank"
+ href="${window.RPMGetFormatURLPref(
+ "app.support.baseURL"
+ )}review-checker-review-quality?utm_campaign=learn-more&utm_medium=inproduct&utm_term=core-sidebar#w_ads_for_relevant_products"
+ data-l10n-name="review-quality-url"
+ ></a>
+ </span>
+ </div>`
+ : null;
+
+ /* Auto-open experiment changes how the settings card appears by:
+ * 1. Showing a new toggle for enabling/disabling auto-open behaviour
+ * 2. Adding a divider between the toggles and opt-out button
+ * 3. Showing text indicating that Review Checker is enabled (not opted-out) above the opt-out button
+ *
+ * Only show if `browser.shopping.experience2023.autoOpen.enabled` is true.
+ */
+ let autoOpenDescriptionL10nId;
+ let autoOpenDescriptionL10nArgs;
+
+ switch (this.hostname) {
+ case "www.amazon.fr":
+ case "www.amazon.de":
+ autoOpenDescriptionL10nId =
+ "shopping-settings-auto-open-description-single-site";
+ autoOpenDescriptionL10nArgs = {
+ currentSite: "Amazon",
+ };
+ break;
+ default:
+ autoOpenDescriptionL10nId =
+ "shopping-settings-auto-open-description-three-sites";
+ autoOpenDescriptionL10nArgs = {
+ firstSite: "Amazon",
+ secondSite: "Best Buy",
+ thirdSite: "Walmart",
+ };
+ }
+
+ let autoOpenToggleMarkup = this.autoOpenEnabled
+ ? html` <div class="shopping-settings-toggle-option-wrapper">
+ <moz-toggle
+ id="shopping-settings-auto-open-toggle"
+ ?pressed=${this.autoOpenEnabledByUser}
+ data-l10n-id="shopping-settings-auto-open-toggle"
+ data-l10n-attrs="label"
+ @toggle=${this.onToggleAutoOpen}
+ >
+ </moz-toggle>
+ <span
+ id="shopping-auto-open-description"
+ data-l10n-id=${autoOpenDescriptionL10nId}
+ data-l10n-args=${JSON.stringify(autoOpenDescriptionL10nArgs)}
+ ></span>
+ </div>`
+ : null;
+
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/settings.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-page.css"
+ />
+ <shopping-card
+ data-l10n-id="shopping-settings-label"
+ data-l10n-attrs="label"
+ type=${!this.autoOpenEnabled ? "accordion" : ""}
+ >
+ <div
+ id="shopping-settings-wrapper"
+ class=${this.autoOpenEnabled
+ ? "shopping-settings-auto-open-ui-enabled"
+ : ""}
+ slot="content"
+ >
+ <section id="shopping-settings-toggles-section">
+ ${adsToggleMarkup} ${autoOpenToggleMarkup}
+ </section>
+ ${this.autoOpenEnabled
+ ? html`<span class="divider" role="separator"></span>`
+ : null}
+ <section id="shopping-settings-opt-out-section">
+ ${this.autoOpenEnabled
+ ? html`<span
+ id="shopping-settings-sidebar-enabled-state"
+ data-l10n-id="shopping-settings-sidebar-enabled-state"
+ ></span>`
+ : null}
+ <button
+ class="shopping-button"
+ id="shopping-settings-opt-out-button"
+ data-l10n-id="shopping-settings-opt-out-button"
+ @click=${this.onDisableShopping}
+ ></button>
+ </section>
+ </div>
+ </shopping-card>
+ <p
+ id="powered-by-fakespot"
+ class="deemphasized"
+ data-l10n-id="powered-by-fakespot"
+ @click=${this.fakespotLinkClicked}
+ >
+ <a
+ id="powered-by-fakespot-link"
+ data-l10n-name="fakespot-link"
+ target="_blank"
+ href="https://www.fakespot.com/our-mission?utm_source=review-checker&utm_campaign=fakespot-by-mozilla&utm_medium=inproduct&utm_term=core-sidebar"
+ ></a>
+ </p>
+ `;
+ }
+}
+
+customElements.define("shopping-settings", ShoppingSettings);
diff --git a/browser/components/shopping/content/shopping-card.css b/browser/components/shopping/content/shopping-card.css
new file mode 100644
index 0000000000..5b0ce9c280
--- /dev/null
+++ b/browser/components/shopping/content/shopping-card.css
@@ -0,0 +1,201 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+:host {
+ display: block;
+ --shopping-card-border-color: light-dark(#f0f0f4, #52525e);
+ --shopping-card-border-radius: 8px;
+ --shopping-card-border-width: 1px;
+ --shopping-card-summary-border-radius: calc(var(--shopping-card-border-radius) - var(--shopping-card-border-width));
+ --shopping-card-padding: 12px;
+ border-radius: var(--shopping-card-border-radius);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+ box-sizing: border-box;
+}
+
+.shopping-card {
+ display: flex;
+ border: var(--shopping-card-border-width) solid var(--shopping-card-border-color);
+ border-radius: var(--shopping-card-border-radius);
+ background-color: var(--in-content-page-background);
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding: var(--shopping-card-padding);
+ position: relative;
+}
+
+/* For accordions, adjust padding and border so that focus outlines wrap around the summary. */
+:host([type="accordion"]) > .shopping-card {
+ padding: 0;
+
+ summary {
+ padding: var(--shopping-card-padding);
+ border-radius: var(--shopping-card-summary-border-radius);
+ }
+
+ #content {
+ padding: 0 var(--shopping-card-padding) var(--shopping-card-padding);
+ }
+
+ > details[open] > summary {
+ border-radius: var(--shopping-card-summary-border-radius) var(--shopping-card-summary-border-radius) 0 0;
+ }
+}
+
+button {
+ margin: 0;
+}
+
+#shopping-details {
+ width: 100%;
+}
+
+#label-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+}
+
+:host([type="accordion"]) #label-wrapper {
+ cursor: pointer;
+ position: relative;
+
+ & > #header {
+ /* 24px for the ghost button, 12px gap between
+ * text and button. */
+ margin-inline-end: 36px;
+ }
+}
+
+#header {
+ color: var(--in-content-text-color);
+ font-size: 1em;
+ margin: 0;
+}
+
+#content {
+ align-self: stretch;
+}
+
+details > summary {
+ list-style: none;
+}
+
+details > summary:focus-visible {
+ outline: var(--in-content-focus-outline);
+}
+
+.chevron-icon {
+ background-image: url("chrome://global/skin/icons/arrow-down.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ color: var(--icon-color);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 24px;
+ height: 24px;
+ /* override some default button sizing styles */
+ min-width: 24px;
+ min-height: 24px;
+ padding: 0;
+ /* Abspos rather than flexbox so we don't influence the padding
+ * around the title.
+ */
+ position: absolute;
+ top: calc(50% - var(--shopping-card-padding));
+ /* This approximates the top/bottom 'padding' gap created by the abspos
+ * above. It won't always be perfectly accurate, and that's OK. */
+ inset-inline-end: -2px;
+}
+
+details[open] .chevron-icon {
+ background-image: url("chrome://global/skin/icons/arrow-up.svg");
+}
+
+.show-more footer {
+ width: 100%;
+ background-color: var(--in-content-page-background);
+ box-shadow: 2px -10px 11px var(--in-content-page-background);
+ border-top: var(--shopping-card-border-width) solid var(--shopping-card-border-color);
+ border-radius: 0 0 var(--shopping-card-border-radius) var(--shopping-card-border-radius);
+ position: absolute;
+ bottom: 0;
+ text-align: center;
+ padding-block: 8px;
+ left: 0;
+ right: 0;
+}
+
+.show-more[expanded="false"] {
+ overflow: clip;
+ height: 200px;
+}
+
+:host(:not([showMoreButtonDisabled])) .show-more ::slotted(div) {
+ margin-block-end: 4rem;
+}
+
+:host([showMoreButtonDisabled]) footer {
+ display: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ :host > .shopping-card {
+ background-color: #42414d;
+ }
+
+ .show-more footer {
+ background-color: #42414d;
+ box-shadow: 2px -10px 11px #42414d;
+ }
+}
+
+@media (prefers-contrast) {
+ /* Style accordion card like a dropdown button. */
+ :host([type="accordion"]) > .shopping-card {
+ border: var(--shopping-card-border-width) solid ButtonText;
+
+ summary {
+ background-color: var(--button-background-color);
+ color: ButtonText;
+
+ #label-wrapper {
+ color: inherit;
+
+ > :is(#header, .chevron-icon) {
+ color: inherit;
+ }
+ }
+
+ .chevron-icon {
+ background-color: transparent;
+ border: none;
+ }
+
+ &:hover {
+ background-color: SelectedItemText;
+ border-color: SelectedItem;
+ color: SelectedItem;
+ }
+
+ &:hover:active {
+ border-color: ButtonText;
+ }
+ }
+
+ details[open] {
+ border-color: var(--shopping-card-border-color);
+
+ summary {
+ border-block-end: var(--shopping-card-border-width) solid ButtonText;
+ }
+ }
+ }
+}
diff --git a/browser/components/shopping/content/shopping-card.mjs b/browser/components/shopping/content/shopping-card.mjs
new file mode 100644
index 0000000000..7b2448df36
--- /dev/null
+++ b/browser/components/shopping/content/shopping-card.mjs
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+const MIN_SHOW_MORE_HEIGHT = 200;
+/**
+ * A card container to be used in the shopping sidebar. There are three card types.
+ * The default type where no type attribute is required and the card will have no extra functionality.
+ * The "accordion" type will initially not show any content. The card will contain a arrow to expand the
+ * card so all of the content is visible.
+ *
+ * @property {string} label - The label text that will be used for the card header
+ * @property {string} type - (optional) The type of card. No type specified
+ * will be the default card. The other available types are "accordion" and "show-more".
+ */
+class ShoppingCard extends MozLitElement {
+ static properties = {
+ label: { type: String },
+ type: { type: String },
+ _isExpanded: { type: Boolean },
+ };
+
+ static get queries() {
+ return {
+ detailsEl: "#shopping-details",
+ contentEl: "#content",
+ };
+ }
+
+ labelTemplate() {
+ if (this.label) {
+ if (this.type === "accordion") {
+ return html`
+ <div id="label-wrapper">
+ <h2 id="header">${this.label}</h2>
+ <button
+ tabindex="-1"
+ class="icon chevron-icon ghost-button"
+ aria-labelledby="header"
+ @click=${this.handleChevronButtonClick}
+ ></button>
+ </div>
+ `;
+ }
+ return html`
+ <div id="label-wrapper">
+ <h2 id="header">${this.label}</h2>
+ <slot name="rating"></slot>
+ </div>
+ `;
+ }
+ return "";
+ }
+
+ cardTemplate() {
+ if (this.type === "accordion") {
+ return html`
+ <details id="shopping-details" @toggle=${this.onCardToggle}>
+ <summary>${this.labelTemplate()}</summary>
+ <div id="content"><slot name="content"></slot></div>
+ </details>
+ `;
+ } else if (this.type === "show-more") {
+ return html`
+ ${this.labelTemplate()}
+ <article
+ id="content"
+ class="show-more"
+ aria-describedby="content"
+ expanded="false"
+ >
+ <slot name="content"></slot>
+
+ <footer>
+ <button
+ aria-controls="content"
+ class="small-button shopping-button"
+ data-l10n-id="shopping-show-more-button"
+ @click=${this.handleShowMoreButtonClick}
+ ></button>
+ </footer>
+ </article>
+ `;
+ }
+ return html`
+ ${this.labelTemplate()}
+ <div id="content" aria-describedby="content">
+ <slot name="content"></slot>
+ </div>
+ `;
+ }
+
+ onCardToggle() {
+ const action = this.detailsEl.open ? "expanded" : "collapsed";
+ let l10nId = this.getAttribute("data-l10n-id");
+ switch (l10nId) {
+ case "shopping-settings-label":
+ Glean.shopping.surfaceSettingsExpandClicked.record({ action });
+ break;
+ case "shopping-analysis-explainer-label":
+ Glean.shopping.surfaceShowQualityExplainerClicked.record({
+ action,
+ });
+ break;
+ }
+ }
+
+ handleShowMoreButtonClick(e) {
+ this._isExpanded = !this._isExpanded;
+ // toggle show more/show less text
+ e.target.setAttribute(
+ "data-l10n-id",
+ this._isExpanded
+ ? "shopping-show-less-button"
+ : "shopping-show-more-button"
+ );
+ // toggle content expanded attribute
+ this.contentEl.attributes.expanded.value = this._isExpanded;
+
+ let action = this._isExpanded ? "expanded" : "collapsed";
+ Glean.shopping.surfaceShowMoreReviewsButtonClicked.record({
+ action,
+ });
+ }
+
+ enableShowMoreButton() {
+ this._isExpanded = false;
+ this.toggleAttribute("showMoreButtonDisabled", false);
+ this.contentEl.attributes.expanded.value = false;
+ }
+
+ disableShowMoreButton() {
+ this._isExpanded = true;
+ this.toggleAttribute("showMoreButtonDisabled", true);
+ this.contentEl.attributes.expanded.value = true;
+ }
+
+ handleChevronButtonClick() {
+ this.detailsEl.open = !this.detailsEl.open;
+ }
+
+ firstUpdated() {
+ if (this.type !== "show-more") {
+ return;
+ }
+
+ let contentSlot = this.shadowRoot.querySelector("slot[name='content']");
+ let contentSlotEls = contentSlot.assignedElements();
+ if (!contentSlotEls.length) {
+ return;
+ }
+
+ let slottedDiv = contentSlotEls[0];
+
+ this.handleContentSlotResize = this.handleContentSlotResize.bind(this);
+ this.contentResizeObserver = new ResizeObserver(
+ this.handleContentSlotResize
+ );
+ this.contentResizeObserver.observe(slottedDiv);
+ }
+
+ disconnectedCallback() {
+ this.contentResizeObserver?.disconnect();
+ }
+
+ handleContentSlotResize(entries) {
+ for (let entry of entries) {
+ if (entry.contentRect.height === 0) {
+ return;
+ }
+
+ if (entry.contentRect.height < MIN_SHOW_MORE_HEIGHT) {
+ this.disableShowMoreButton();
+ } else if (this.hasAttribute("showMoreButtonDisabled")) {
+ this.enableShowMoreButton();
+ }
+ }
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-card.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-page.css"
+ />
+ <article
+ class="shopping-card"
+ aria-labelledby="header"
+ aria-label=${ifDefined(this.label)}
+ >
+ ${this.cardTemplate()}
+ </article>
+ `;
+ }
+}
+customElements.define("shopping-card", ShoppingCard);
diff --git a/browser/components/shopping/content/shopping-container.css b/browser/components/shopping/content/shopping-container.css
new file mode 100644
index 0000000000..6ac09964bf
--- /dev/null
+++ b/browser/components/shopping/content/shopping-container.css
@@ -0,0 +1,152 @@
+/* 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/. */
+
+:host {
+ --shopping-close-button-size: var(--button-min-height);
+ --shopping-header-font-size: 1.3rem;
+}
+
+#shopping-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+#header-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+ background-color: var(--shopping-header-background);
+ box-sizing: border-box;
+ padding-block: 16px 8px;
+ padding-inline: 16px 8px;
+ position: sticky;
+ top: 0;
+ width: 100%;
+ z-index: 2;
+}
+
+#shopping-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+#shopping-header-title {
+ font-size: var(--shopping-header-font-size);
+ font-weight: var(--font-weight-bold);
+ margin: 0;
+}
+
+.shopping-header-overflow {
+ box-shadow: 0 1px 2px 0 rgba(58, 57, 68, 0.20);
+}
+
+#beta-marker {
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-default);
+ padding: 2px 4px;
+ margin: 0;
+ line-height: 150%;
+ text-transform: uppercase;
+ color: var(--icon-color);
+ border: 1px solid var(--icon-color);
+ border-radius: var(--border-radius-small);
+}
+
+#close-button {
+ min-width: var(--shopping-close-button-size);
+ min-height: var(--shopping-close-button-size);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: url("chrome://global/skin/icons/close.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ margin-inline-end: 0;
+
+ @media not (prefers-contrast) {
+ color: var(--icon-color);
+ }
+}
+
+#content {
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ padding: 0 16px 16px;
+}
+
+#content:focus-visible {
+ outline-offset: -2px;
+}
+
+.loading-box {
+ box-shadow: none;
+ border: none;
+ background: var(--in-content-button-background);
+ margin-block: 1rem;
+}
+
+.loading-box.small {
+ height: 2.67rem;
+}
+
+.loading-box.medium {
+ height: 5.34rem;
+}
+
+.loading-box.large {
+ height: 12.8rem;
+}
+
+.loading-box:nth-child(odd) {
+ background-color: var(--in-content-button-background);
+}
+
+.loading-box:nth-child(even) {
+ background-color: var(--in-content-button-background-hover);
+}
+
+@media not (prefers-reduced-motion) {
+ .animate > .loading-box {
+ animation-name: fade-in;
+ animation-direction: alternate;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-timing-function: ease-in-out;
+ }
+
+ /* First box + every 4th box, fifth box + every 4th box */
+ .loading-box:nth-child(4n-3) {
+ animation-delay: -1s;
+ }
+
+ /* Second box + every 4th box, sixth box + every 4th box */
+ .loading-box:nth-child(4n-2) {
+ animation-delay: 0s;
+ }
+
+ /* Third box + every 4th box */
+ .loading-box:nth-child(4n-1) {
+ animation-delay: -1.5s;
+ }
+
+ /* Fourth box + every 4th box */
+ .loading-box:nth-child(4n) {
+ animation-delay: -0.5s;
+ }
+
+ @keyframes fade-in {
+ from {
+ opacity: .25;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+}
diff --git a/browser/components/shopping/content/shopping-container.mjs b/browser/components/shopping/content/shopping-container.mjs
new file mode 100644
index 0000000000..a1a6774d49
--- /dev/null
+++ b/browser/components/shopping/content/shopping-container.mjs
@@ -0,0 +1,471 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/highlights.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/settings.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/adjusted-rating.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/reliability.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/analysis-explainer.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-message-bar.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/unanalyzed.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/recommended-ad.mjs";
+
+// The number of pixels that must be scrolled from the
+// top of the sidebar to show the header box shadow.
+const HEADER_SCROLL_PIXEL_OFFSET = 8;
+
+const SIDEBAR_CLOSED_COUNT_PREF =
+ "browser.shopping.experience2023.sidebarClosedCount";
+const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF =
+ "browser.shopping.experience2023.showKeepSidebarClosedMessage";
+const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active";
+
+export class ShoppingContainer extends MozLitElement {
+ static properties = {
+ data: { type: Object },
+ showOnboarding: { type: Boolean },
+ productUrl: { type: String },
+ recommendationData: { type: Array },
+ isOffline: { type: Boolean },
+ analysisEvent: { type: Object },
+ userReportedAvailable: { type: Boolean },
+ adsEnabled: { type: Boolean },
+ adsEnabledByUser: { type: Boolean },
+ isAnalysisInProgress: { type: Boolean },
+ analysisProgress: { type: Number },
+ isOverflow: { type: Boolean },
+ autoOpenEnabled: { type: Boolean },
+ autoOpenEnabledByUser: { type: Boolean },
+ showingKeepClosedMessage: { type: Boolean },
+ };
+
+ static get queries() {
+ return {
+ reviewReliabilityEl: "review-reliability",
+ adjustedRatingEl: "adjusted-rating",
+ highlightsEl: "review-highlights",
+ settingsEl: "shopping-settings",
+ analysisExplainerEl: "analysis-explainer",
+ unanalyzedProductEl: "unanalyzed-product-card",
+ shoppingMessageBarEl: "shopping-message-bar",
+ recommendedAdEl: "recommended-ad",
+ loadingEl: "#loading-wrapper",
+ closeButtonEl: "#close-button",
+ keepClosedMessageBarEl: "#keep-closed-message-bar",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ window.document.addEventListener("Update", this);
+ window.document.addEventListener("NewAnalysisRequested", this);
+ window.document.addEventListener("ReanalysisRequested", this);
+ window.document.addEventListener("ReportedProductAvailable", this);
+ window.document.addEventListener("adsEnabledByUserChanged", this);
+ window.document.addEventListener("scroll", this);
+ window.document.addEventListener("UpdateRecommendations", this);
+ window.document.addEventListener("UpdateAnalysisProgress", this);
+ window.document.addEventListener("autoOpenEnabledByUserChanged", this);
+ window.document.addEventListener("ShowKeepClosedMessage", this);
+ window.document.addEventListener("HideKeepClosedMessage", this);
+
+ window.dispatchEvent(
+ new CustomEvent("ContentReady", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ updated() {
+ if (this.focusCloseButton) {
+ this.closeButtonEl.focus();
+ }
+ }
+
+ async _update({
+ data,
+ showOnboarding,
+ productUrl,
+ recommendationData,
+ adsEnabled,
+ adsEnabledByUser,
+ isAnalysisInProgress,
+ analysisProgress,
+ focusCloseButton,
+ autoOpenEnabled,
+ autoOpenEnabledByUser,
+ }) {
+ // If we're not opted in or there's no shopping URL in the main browser,
+ // the actor will pass `null`, which means this will clear out any existing
+ // content in the sidebar.
+ this.data = data;
+ this.showOnboarding = showOnboarding;
+ this.productUrl = productUrl;
+ this.recommendationData = recommendationData;
+ this.isOffline = !navigator.onLine;
+ this.isAnalysisInProgress = isAnalysisInProgress;
+ this.adsEnabled = adsEnabled;
+ this.adsEnabledByUser = adsEnabledByUser;
+ this.analysisProgress = analysisProgress;
+ this.focusCloseButton = focusCloseButton;
+ this.autoOpenEnabled = autoOpenEnabled;
+ this.autoOpenEnabledByUser = autoOpenEnabledByUser;
+ }
+
+ _updateRecommendations({ recommendationData }) {
+ this.recommendationData = recommendationData;
+ }
+
+ _updateAnalysisProgress({ progress }) {
+ this.analysisProgress = progress;
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "Update":
+ this._update(event.detail);
+ break;
+ case "NewAnalysisRequested":
+ case "ReanalysisRequested":
+ this.isAnalysisInProgress = true;
+ this.analysisEvent = {
+ type: event.type,
+ productUrl: this.productUrl,
+ };
+ window.dispatchEvent(
+ new CustomEvent("PolledRequestMade", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ break;
+ case "ReportedProductAvailable":
+ this.userReportedAvailable = true;
+ window.dispatchEvent(
+ new CustomEvent("ReportProductAvailable", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ Glean.shopping.surfaceReactivatedButtonClicked.record();
+ break;
+ case "adsEnabledByUserChanged":
+ this.adsEnabledByUser = event.detail?.adsEnabledByUser;
+ break;
+ case "scroll":
+ let scrollYPosition = window.scrollY;
+ this.isOverflow = scrollYPosition > HEADER_SCROLL_PIXEL_OFFSET;
+ break;
+ case "UpdateRecommendations":
+ this._updateRecommendations(event.detail);
+ break;
+ case "UpdateAnalysisProgress":
+ this._updateAnalysisProgress(event.detail);
+ break;
+ case "autoOpenEnabledByUserChanged":
+ this.autoOpenEnabledByUser = event.detail?.autoOpenEnabledByUser;
+ break;
+ case "ShowKeepClosedMessage":
+ this.showingKeepClosedMessage = true;
+ break;
+ case "HideKeepClosedMessage":
+ this.showingKeepClosedMessage = false;
+ break;
+ }
+ }
+
+ getHostnameFromProductUrl() {
+ let hostname;
+ try {
+ hostname = new URL(this.productUrl)?.hostname;
+ return hostname;
+ } catch (e) {
+ console.error(`Unknown product url ${this.productUrl}.`);
+ return null;
+ }
+ }
+
+ analysisDetailsTemplate() {
+ /* At present, en is supported as the default language for reviews. As we support more sites,
+ * update `lang` accordingly if highlights need to be displayed in other languages. */
+ let lang;
+ let hostname = this.getHostnameFromProductUrl();
+
+ switch (hostname) {
+ case "www.amazon.fr":
+ lang = "fr";
+ break;
+ case "www.amazon.de":
+ lang = "de";
+ break;
+ default:
+ lang = "en";
+ }
+ return html`
+ <review-reliability letter=${this.data.grade}></review-reliability>
+ <adjusted-rating
+ rating=${ifDefined(this.data.adjusted_rating)}
+ ></adjusted-rating>
+ <review-highlights
+ .highlights=${this.data.highlights}
+ lang=${lang}
+ ></review-highlights>
+ `;
+ }
+
+ contentTemplate() {
+ // The user requested an analysis which is not done yet.
+ if (
+ this.analysisEvent?.productUrl == this.productUrl &&
+ this.isAnalysisInProgress
+ ) {
+ const isReanalysis = this.analysisEvent.type === "ReanalysisRequested";
+ return html`<shopping-message-bar
+ type=${isReanalysis
+ ? "reanalysis-in-progress"
+ : "analysis-in-progress"}
+ progress=${this.analysisProgress}
+ ></shopping-message-bar>
+ ${isReanalysis ? this.analysisDetailsTemplate() : null}`;
+ }
+
+ if (this.data?.error) {
+ return html`<shopping-message-bar
+ type="generic-error"
+ ></shopping-message-bar>`;
+ }
+
+ if (this.data.page_not_supported) {
+ return html`<shopping-message-bar
+ type="page-not-supported"
+ ></shopping-message-bar>`;
+ }
+
+ if (this.data.deleted_product_reported) {
+ return html`<shopping-message-bar
+ type="product-not-available-reported"
+ ></shopping-message-bar>`;
+ }
+
+ if (this.data.deleted_product) {
+ return this.userReportedAvailable
+ ? html`<shopping-message-bar
+ type="thanks-for-reporting"
+ ></shopping-message-bar>`
+ : html`<shopping-message-bar
+ type="product-not-available"
+ ></shopping-message-bar>`;
+ }
+
+ if (this.data.needs_analysis) {
+ if (!this.data.product_id || typeof this.data.grade != "string") {
+ // Product is new to us.
+ return html`<unanalyzed-product-card
+ productUrl=${ifDefined(this.productUrl)}
+ ></unanalyzed-product-card>`;
+ }
+
+ // We successfully analyzed the product before, but the current analysis is outdated and can be updated
+ // via a re-analysis.
+ return html`
+ <shopping-message-bar
+ type="stale"
+ .productUrl=${this.productUrl}
+ ></shopping-message-bar>
+ ${this.analysisDetailsTemplate()}
+ `;
+ }
+
+ if (this.data.not_enough_reviews) {
+ // We already saw and tried to analyze this product before, but there are not enough reviews
+ // to make a detailed analysis.
+ return html`<shopping-message-bar
+ type="not-enough-reviews"
+ ></shopping-message-bar>`;
+ }
+
+ return this.analysisDetailsTemplate();
+ }
+
+ recommendationTemplate() {
+ const canShowAds = this.adsEnabled && this.adsEnabledByUser;
+ if (this.recommendationData?.length && canShowAds) {
+ return html`<recommended-ad
+ .product=${this.recommendationData[0]}
+ ></recommended-ad>`;
+ }
+ return null;
+ }
+
+ /**
+ * @param {object?} options
+ * @param {boolean?} options.animate = true
+ * Whether to animate the loading state. Defaults to true.
+ * There will be no animation for users who prefer reduced motion,
+ * irrespective of the value of this option.
+ */
+ loadingTemplate({ animate = true } = {}) {
+ /* Due to limitations with aria-busy for certain screen readers
+ * (see Bug 1682063), mark loading container as a pseudo image and
+ * use aria-label as a workaround. */
+ return html`
+ <div
+ id="loading-wrapper"
+ data-l10n-id="shopping-a11y-loading"
+ role="img"
+ class=${animate ? "animate" : ""}
+ >
+ <div class="loading-box medium"></div>
+ <div class="loading-box medium"></div>
+ <div class="loading-box large"></div>
+ <div class="loading-box small"></div>
+ <div class="loading-box large"></div>
+ <div class="loading-box small"></div>
+ </div>
+ `;
+ }
+
+ renderContainer(sidebarContent, hideFooter = false) {
+ return html`<link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-container.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/in-content/common.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-page.css"
+ />
+ <div id="shopping-container">
+ <div
+ id="header-wrapper"
+ class=${this.isOverflow ? "shopping-header-overflow" : ""}
+ >
+ <header id="shopping-header" data-l10n-id="shopping-a11y-header">
+ <h1
+ id="shopping-header-title"
+ data-l10n-id="shopping-main-container-title"
+ ></h1>
+ <p id="beta-marker" data-l10n-id="shopping-beta-marker"></p>
+ </header>
+ <button
+ id="close-button"
+ class="ghost-button shopping-button"
+ data-l10n-id="shopping-close-button"
+ @click=${this.handleCloseButtonClick}
+ ></button>
+ </div>
+ <div id="content" aria-live="polite" aria-busy=${!this.data}>
+ <slot name="multi-stage-message-slot"></slot>
+ ${this.keepClosedMessageTemplate()}${sidebarContent}
+ ${!hideFooter ? this.footerTemplate() : null}
+ </div>
+ </div>`;
+ }
+
+ footerTemplate() {
+ let hostname = this.getHostnameFromProductUrl();
+ return html`
+ <analysis-explainer
+ productUrl=${ifDefined(this.productUrl)}
+ ></analysis-explainer>
+ ${this.recommendationTemplate()}
+ <shopping-settings
+ ?adsEnabled=${this.adsEnabled}
+ ?adsEnabledByUser=${this.adsEnabledByUser}
+ ?autoOpenEnabled=${this.autoOpenEnabled}
+ ?autoOpenEnabledByUser=${this.autoOpenEnabledByUser}
+ .hostname=${hostname}
+ ></shopping-settings>
+ `;
+ }
+
+ keepClosedMessageTemplate() {
+ if (
+ this.autoOpenEnabled &&
+ this.autoOpenEnabledByUser &&
+ this.showingKeepClosedMessage &&
+ RPMGetBoolPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true)
+ ) {
+ return html`<shopping-message-bar
+ id="keep-closed-message-bar"
+ type="keep-closed"
+ ></shopping-message-bar>`;
+ }
+ return null;
+ }
+
+ render() {
+ let content;
+ let hideFooter;
+ if (this.showOnboarding) {
+ content = html``;
+ hideFooter = true;
+ } else if (this.isOffline) {
+ content = this.loadingTemplate({ animate: false });
+ hideFooter = true;
+ } else if (!this.data) {
+ if (this.isAnalysisInProgress) {
+ content = html`<shopping-message-bar
+ type="analysis-in-progress"
+ progress=${this.analysisProgress}
+ ></shopping-message-bar>`;
+ } else {
+ content = this.loadingTemplate();
+ hideFooter = true;
+ }
+ } else {
+ content = this.contentTemplate();
+ }
+ return this.renderContainer(content, hideFooter);
+ }
+
+ handleCloseButtonClick() {
+ if (this.autoOpenEnabled && this.autoOpenEnabledByUser) {
+ let sidebarClosedCount = RPMGetIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0);
+ if (
+ !this.showingKeepClosedMessage &&
+ sidebarClosedCount >= 4 &&
+ RPMGetBoolPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true)
+ ) {
+ this.showingKeepClosedMessage = true;
+ return;
+ }
+
+ this.showingKeepClosedMessage = false;
+
+ if (sidebarClosedCount >= 6) {
+ RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false);
+ }
+
+ RPMSetPref(SIDEBAR_CLOSED_COUNT_PREF, sidebarClosedCount + 1);
+ }
+
+ RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
+ Glean.shopping.surfaceClosed.record({ source: "closeButton" });
+ }
+}
+
+customElements.define("shopping-container", ShoppingContainer);
diff --git a/browser/components/shopping/content/shopping-message-bar.css b/browser/components/shopping/content/shopping-message-bar.css
new file mode 100644
index 0000000000..f88b18be45
--- /dev/null
+++ b/browser/components/shopping/content/shopping-message-bar.css
@@ -0,0 +1,78 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+:host {
+ display: block;
+ border-radius: 4px;
+}
+
+#message-bar-container {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0;
+ margin: 0;
+}
+
+:host([type="stale"]) #message-bar-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ column-gap: 8px;
+
+ > span {
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ }
+}
+
+button {
+ margin-inline-start: 0;
+}
+
+message-bar::part(container) {
+ align-items: start;
+ padding: 0.5rem 0.75rem;
+ gap: 0.75rem;
+}
+
+message-bar::part(icon) {
+ padding: 0;
+}
+
+:host([type=analysis-in-progress]) message-bar::part(icon),
+:host([type=reanalysis-in-progress]) message-bar::part(icon) {
+ border: 1px solid var(--icon-color);
+ border-radius: 50%;
+}
+
+:host([type=analysis-in-progress]) message-bar::part(icon)::after,
+:host([type=reanalysis-in-progress]) message-bar::part(icon)::after {
+ --message-bar-icon-url: conic-gradient(var(--icon-color-information) var(--analysis-progress-pcent, 0%), transparent var(--analysis-progress-pcent, 0%));
+ border-radius: 50%;
+ margin-block: 1px 0;
+ margin-inline: 1px 0;
+ width: calc(var(--icon-size) - 2px);
+ height: calc(var(--icon-size) - 2px);
+}
+
+:host([type=reanalysis-in-progress]) message-bar::part(container),
+:host([type=stale]) message-bar::part(container) {
+ align-items: center;
+ background-color: transparent;
+ padding: 0;
+}
+
+:host([type=thank-you-for-feedback]) message-bar::part(icon) {
+ --message-bar-icon-url: url("chrome://global/skin/icons/check-filled.svg");
+}
+
+:host([type=thank-you-for-feedback]) message-bar::part(container) {
+ text-align: start;
+ align-items: center;
+ gap: 12px;
+}
diff --git a/browser/components/shopping/content/shopping-message-bar.mjs b/browser/components/shopping/content/shopping-message-bar.mjs
new file mode 100644
index 0000000000..d6ec9c0888
--- /dev/null
+++ b/browser/components/shopping/content/shopping-message-bar.mjs
@@ -0,0 +1,278 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+import { html, styleMap } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-message-bar.mjs";
+
+const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active";
+const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF =
+ "browser.shopping.experience2023.showKeepSidebarClosedMessage";
+const SHOPPING_AUTO_OPEN_SIDEBAR_PREF =
+ "browser.shopping.experience2023.autoOpen.userEnabled";
+
+class ShoppingMessageBar extends MozLitElement {
+ #MESSAGE_TYPES_RENDER_TEMPLATE_MAPPING = new Map([
+ ["stale", () => this.staleWarningTemplate()],
+ ["generic-error", () => this.genericErrorTemplate()],
+ ["not-enough-reviews", () => this.notEnoughReviewsTemplate()],
+ ["product-not-available", () => this.productNotAvailableTemplate()],
+ ["thanks-for-reporting", () => this.thanksForReportingTemplate()],
+ [
+ "product-not-available-reported",
+ () => this.productNotAvailableReportedTemplate(),
+ ],
+ ["analysis-in-progress", () => this.analysisInProgressTemplate()],
+ ["reanalysis-in-progress", () => this.reanalysisInProgressTemplate()],
+ ["page-not-supported", () => this.pageNotSupportedTemplate()],
+ ["thank-you-for-feedback", () => this.thankYouForFeedbackTemplate()],
+ ["keep-closed", () => this.keepClosedTemplate()],
+ ]);
+
+ static properties = {
+ type: { type: String },
+ productUrl: { type: String, reflect: true },
+ progress: { type: Number, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ reAnalysisButtonEl: "#message-bar-reanalysis-button",
+ productAvailableBtnEl: "#message-bar-report-product-available-btn",
+ yesKeepClosedButtonEl: "#yes-keep-closed-button",
+ noThanksButtonEl: "#no-thanks-button",
+ };
+ }
+
+ onClickAnalysisButton() {
+ this.dispatchEvent(
+ new CustomEvent("ReanalysisRequested", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ Glean.shopping.surfaceReanalyzeClicked.record();
+ }
+
+ onClickProductAvailable() {
+ this.dispatchEvent(
+ new CustomEvent("ReportedProductAvailable", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ handleNoThanksClick() {
+ RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
+ RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false);
+ this.dispatchEvent(
+ new CustomEvent("HideKeepClosedMessage", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ Glean.shopping.surfaceNoThanksButtonClicked.record();
+ }
+
+ handleKeepClosedClick() {
+ RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
+ RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false);
+ RPMSetPref(SHOPPING_AUTO_OPEN_SIDEBAR_PREF, false);
+ this.dispatchEvent(
+ new CustomEvent("HideKeepClosedMessage", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ Glean.shopping.surfaceYesKeepClosedButtonClicked.record();
+ }
+
+ staleWarningTemplate() {
+ return html`<message-bar>
+ <article id="message-bar-container" aria-labelledby="header">
+ <span
+ data-l10n-id="shopping-message-bar-warning-stale-analysis-message-2"
+ ></span>
+ <button
+ id="message-bar-reanalysis-button"
+ class="small-button shopping-button"
+ data-l10n-id="shopping-message-bar-warning-stale-analysis-button"
+ @click=${this.onClickAnalysisButton}
+ ></button>
+ </article>
+ </message-bar>`;
+ }
+
+ genericErrorTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="warning"
+ data-l10n-id="shopping-message-bar-generic-error"
+ >
+ </moz-message-bar>`;
+ }
+
+ notEnoughReviewsTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="warning"
+ data-l10n-id="shopping-message-bar-warning-not-enough-reviews"
+ >
+ </moz-message-bar>`;
+ }
+
+ productNotAvailableTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="warning"
+ data-l10n-id="shopping-message-bar-warning-product-not-available"
+ >
+ <button
+ slot="actions"
+ id="message-bar-report-product-available-btn"
+ class="small-button shopping-button"
+ data-l10n-id="shopping-message-bar-warning-product-not-available-button2"
+ @click=${this.onClickProductAvailable}
+ ></button>
+ </moz-message-bar>`;
+ }
+
+ thanksForReportingTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="info"
+ data-l10n-id="shopping-message-bar-thanks-for-reporting"
+ >
+ </moz-message-bar>`;
+ }
+
+ productNotAvailableReportedTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="warning"
+ data-l10n-id="shopping-message-bar-warning-product-not-available-reported"
+ >
+ </moz-message-bar>`;
+ }
+
+ analysisInProgressTemplate() {
+ return html`<message-bar
+ style=${styleMap({
+ "--analysis-progress-pcent": `${this.progress}%`,
+ })}
+ >
+ <article
+ id="message-bar-container"
+ aria-labelledby="header"
+ type="analysis"
+ >
+ <strong
+ id="header"
+ data-l10n-id="shopping-message-bar-analysis-in-progress-with-amount"
+ data-l10n-args="${JSON.stringify({
+ percentage: Math.round(this.progress),
+ })}"
+ ></strong>
+ <span
+ data-l10n-id="shopping-message-bar-analysis-in-progress-message2"
+ ></span>
+ </article>
+ </message-bar>`;
+ }
+
+ reanalysisInProgressTemplate() {
+ return html`<message-bar
+ style=${styleMap({
+ "--analysis-progress-pcent": `${this.progress}%`,
+ })}
+ >
+ <article
+ id="message-bar-container"
+ aria-labelledby="header"
+ type="re-analysis"
+ >
+ <span
+ id="header"
+ data-l10n-id="shopping-message-bar-analysis-in-progress-with-amount"
+ data-l10n-args="${JSON.stringify({
+ percentage: Math.round(this.progress),
+ })}"
+ ></span>
+ </article>
+ </message-bar>`;
+ }
+
+ pageNotSupportedTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="warning"
+ data-l10n-id="shopping-message-bar-page-not-supported"
+ >
+ </moz-message-bar>`;
+ }
+
+ thankYouForFeedbackTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading"
+ type="success"
+ dismissable
+ data-l10n-id="shopping-survey-thanks"
+ >
+ </moz-message-bar>`;
+ }
+
+ keepClosedTemplate() {
+ return html`<moz-message-bar
+ data-l10n-attrs="heading, message"
+ type="info"
+ data-l10n-id="shopping-message-bar-keep-closed-header"
+ >
+ <moz-button-group slot="actions">
+ <button
+ id="no-thanks-button"
+ class="small-button shopping-button"
+ data-l10n-id="shopping-message-bar-keep-closed-dismiss-button"
+ @click=${this.handleNoThanksClick}
+ ></button>
+ <button
+ id="yes-keep-closed-button"
+ class="primary small-button shopping-button"
+ data-l10n-id="shopping-message-bar-keep-closed-accept-button"
+ @click=${this.handleKeepClosedClick}
+ ></button>
+ </moz-button-group>
+ </moz-message-bar>`;
+ }
+
+ render() {
+ let messageBarTemplate = this.#MESSAGE_TYPES_RENDER_TEMPLATE_MAPPING.get(
+ this.type
+ )();
+ if (messageBarTemplate) {
+ if (this.type == "stale") {
+ Glean.shopping.surfaceStaleAnalysisShown.record();
+ }
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-message-bar.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-page.css"
+ />
+ ${messageBarTemplate}
+ `;
+ }
+ return null;
+ }
+}
+
+customElements.define("shopping-message-bar", ShoppingMessageBar);
diff --git a/browser/components/shopping/content/shopping-page.css b/browser/components/shopping/content/shopping-page.css
new file mode 100644
index 0000000000..b6fd43704f
--- /dev/null
+++ b/browser/components/shopping/content/shopping-page.css
@@ -0,0 +1,28 @@
+/* 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/. */
+
+:root {
+ --shopping-header-background: light-dark(#f9f9fb, #2b2a33);
+ background-color: var(--shopping-header-background);
+ font: menu;
+}
+
+@media (prefers-contrast) {
+ button.shopping-button:enabled,
+ button.ghost-button:not(.semi-transparent):enabled {
+ background-color: ButtonFace;
+ border-color: ButtonText;
+ color: ButtonText;
+
+ &:hover {
+ background-color: SelectedItemText;
+ border-color: SelectedItem;
+ color: SelectedItem;
+ }
+
+ &:hover:active {
+ border-color: ButtonText;
+ }
+ }
+}
diff --git a/browser/components/shopping/content/shopping-sidebar.js b/browser/components/shopping/content/shopping-sidebar.js
new file mode 100644
index 0000000000..6873682ae6
--- /dev/null
+++ b/browser/components/shopping/content/shopping-sidebar.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+"use strict";
+
+// This is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
+ const SHOPPING_SIDEBAR_WIDTH_PREF =
+ "browser.shopping.experience2023.sidebarWidth";
+ class ShoppingSidebar extends MozXULElement {
+ #browser;
+ #initialized;
+
+ static get markup() {
+ return `
+ <browser
+ class="shopping-sidebar"
+ autoscroll="false"
+ disablefullscreen="true"
+ disablehistory="true"
+ flex="1"
+ message="true"
+ manualactiveness="true"
+ remoteType="privilegedabout"
+ maychangeremoteness="true"
+ remote="true"
+ src="about:shoppingsidebar"
+ type="content"
+ />
+ `;
+ }
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.initialize();
+ }
+
+ initialize() {
+ if (this.#initialized) {
+ return;
+ }
+ this.resizeObserverFn = this.resizeObserverFn.bind(this);
+ this.appendChild(this.constructor.fragment);
+ this.#browser = this.querySelector(".shopping-sidebar");
+
+ let previousWidth = Services.prefs.getIntPref(
+ SHOPPING_SIDEBAR_WIDTH_PREF,
+ 0
+ );
+ if (previousWidth > 0) {
+ this.style.width = `${previousWidth}px`;
+ }
+
+ this.resizeObserver = new ResizeObserver(this.resizeObserverFn);
+ this.resizeObserver.observe(this);
+
+ this.#initialized = true;
+ }
+
+ resizeObserverFn(entries) {
+ for (let entry of entries) {
+ if (entry.contentBoxSize[0].inlineSize < 1) {
+ return;
+ }
+
+ Services.prefs.setIntPref(
+ SHOPPING_SIDEBAR_WIDTH_PREF,
+ entry.contentBoxSize[0].inlineSize
+ );
+ }
+ }
+ }
+
+ customElements.define("shopping-sidebar", ShoppingSidebar);
+}
diff --git a/browser/components/shopping/content/shopping.ftl b/browser/components/shopping/content/shopping.ftl
new file mode 100644
index 0000000000..282c65656b
--- /dev/null
+++ b/browser/components/shopping/content/shopping.ftl
@@ -0,0 +1,7 @@
+# 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/.
+
+### This file is not in a locales directory to prevent it from
+### being translated as the feature is still in heavy development
+### and strings are likely to change often.
diff --git a/browser/components/shopping/content/shopping.html b/browser/components/shopping/content/shopping.html
new file mode 100644
index 0000000000..1c5d627869
--- /dev/null
+++ b/browser/components/shopping/content/shopping.html
@@ -0,0 +1,53 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ role="document"
+ id="shopping"
+ class="system-font-size"
+>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src resource: chrome:; object-src 'none'; img-src blob: chrome:;"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link rel="localization" href="toolkit/global/mozFiveStar.ftl" />
+ <link rel="localization" href="toolkit/global/mozSupportLink.ftl" />
+ <link rel="localization" href="toolkit/global/notification.ftl" />
+ <link rel="localization" href="preview/shopping.ftl" />
+ <link rel="localization" href="browser/shopping.ftl" />
+ <link rel="localization" href="toolkit/global/mozMessageBar.ftl" />
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/shopping-page.css"
+ />
+
+ <script src="chrome://global/content/elements/message-bar.js"></script>
+ <script
+ type="module"
+ src="chrome://browser/content/shopping/onboarding.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/shopping/shopping-container.mjs"
+ ></script>
+ <title data-l10n-id="shopping-page-title"></title>
+ </head>
+ <body>
+ <shopping-container>
+ <div
+ id="multi-stage-message-root"
+ class="onboardingContainer shopping"
+ slot="multi-stage-message-slot"
+ ></div>
+ </shopping-container>
+ </body>
+</html>
diff --git a/browser/components/shopping/content/unanalyzed.css b/browser/components/shopping/content/unanalyzed.css
new file mode 100644
index 0000000000..d2e2b0b4b0
--- /dev/null
+++ b/browser/components/shopping/content/unanalyzed.css
@@ -0,0 +1,41 @@
+/* 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 url("chrome://global/skin/in-content/common.css");
+
+#unanalyzed-product-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+#unanalyzed-product-icon {
+ max-width: 264px;
+ max-height: 290px;
+ width: 100%;
+ content: url("chrome://browser/content/shopping/assets/unanalyzedLight.avif");
+
+ @media (prefers-color-scheme: dark) {
+ content: url("chrome://browser/content/shopping/assets/unanalyzedDark.avif");
+ }
+}
+
+#unanalyzed-product-message-content {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.5;
+
+ > h2 {
+ font-size: inherit;
+ }
+
+ > p {
+ margin-block: 0.25rem;
+ }
+}
+
+#unanalyzed-product-analysis-button {
+ width: 100%;
+}
diff --git a/browser/components/shopping/content/unanalyzed.mjs b/browser/components/shopping/content/unanalyzed.mjs
new file mode 100644
index 0000000000..0be85b65e4
--- /dev/null
+++ b/browser/components/shopping/content/unanalyzed.mjs
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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 { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/shopping/shopping-card.mjs";
+
+class UnanalyzedProductCard extends MozLitElement {
+ static properties = {
+ productURL: { type: String, reflect: true },
+ };
+
+ static get queries() {
+ return {
+ analysisButtonEl: "#unanalyzed-product-analysis-button",
+ };
+ }
+
+ onClickAnalysisButton() {
+ this.dispatchEvent(
+ new CustomEvent("NewAnalysisRequested", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ Glean.shopping.surfaceAnalyzeReviewsNoneAvailableClicked.record();
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/shopping/unanalyzed.css"
+ />
+ <shopping-card>
+ <div id="unanalyzed-product-wrapper" slot="content">
+ <img id="unanalyzed-product-icon" role="presentation" alt=""></img>
+ <div id="unanalyzed-product-message-content">
+ <h2
+ data-l10n-id="shopping-unanalyzed-product-header-2"
+ ></h2>
+ <p data-l10n-id="shopping-unanalyzed-product-message-2"></p>
+ </div>
+ <button
+ id="unanalyzed-product-analysis-button"
+ class="primary"
+ data-l10n-id="shopping-unanalyzed-product-analyze-button"
+ @click=${this.onClickAnalysisButton}
+ ></button>
+ </div>
+ </shopping-card>
+ `;
+ }
+}
+
+customElements.define("unanalyzed-product-card", UnanalyzedProductCard);