Bug 562790: Support paid results in the add-ons search results. r=Unfocused, a=blocks-betaN
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 07 Jan 2011 09:09:09 -0800
changeset 60127 54b7f0bf58ff
parent 60126 546bfeae9ea5
child 60128 73056e7f094d
push id17874
push userdtownsend@mozilla.com
push dateFri, 07 Jan 2011 17:10:22 +0000
treeherdermozilla-central@54b7f0bf58ff [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, blocks-betaN
bugs562790
milestone2.0b9pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 562790: Support paid results in the add-ons search results. r=Unfocused, a=blocks-betaN
toolkit/mozapps/extensions/AddonRepository.jsm
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/content/extensions.xul
toolkit/mozapps/extensions/test/browser/Makefile.in
toolkit/mozapps/extensions/test/browser/browser_purchase.js
toolkit/mozapps/extensions/test/browser/browser_purchase.xml
toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml
toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
--- a/toolkit/mozapps/extensions/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/AddonRepository.jsm
@@ -216,16 +216,32 @@ AddonSearchResult.prototype = {
   contributionURL: null,
 
   /**
    * The suggested contribution amount
    */
   contributionAmount: null,
 
   /**
+   * The URL to visit in order to purchase the add-on
+   */
+  purchaseURL: null,
+
+  /**
+   * The numerical cost of the add-on in some currency, for sorting purposes
+   * only
+   */
+  purchaseAmount: null,
+
+  /**
+   * The display cost of the add-on, for display purposes only
+   */
+  purchaseDisplayAmount: null,
+
+  /**
    * The rating of the add-on, 0-5
    */
   averageRating: null,
 
   /**
    * The number of reviews for this add-on
    */
   reviewCount: null,
@@ -273,21 +289,27 @@ AddonSearchResult.prototype = {
 
   /**
    * The Date that the add-on was most recently updated
    */
   updateDate: null,
 
   /**
    * True or false depending on whether the add-on is compatible with the
-   * current version and platform of the application
+   * current version of the application
    */
   isCompatible: true,
 
   /**
+   * True or false depending on whether the add-on is compatible with the
+   * current platform
+   */
+  isPlatformCompatible: true,
+
+  /**
    * True if the add-on has a secure means of updating
    */
   providesUpdatesSecurely: true,
 
   /**
    * The current blocklist state of the add-on
    */
   blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
@@ -923,16 +945,27 @@ var AddonRepository = {
         case "contribution_data":
           let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
           let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
           if (meetDevelopers != null && suggestedAmount != null) {
             addon.contributionURL = meetDevelopers;
             addon.contributionAmount = suggestedAmount;
           }
           break
+        case "payment_data":
+          let link = this._getDescendantTextContent(node, "link");
+          let amountTag = this._getUniqueDescendant(node, "amount");
+          let amount = parseFloat(amountTag.getAttribute("amount"));
+          let displayAmount = this._getTextContent(amountTag);
+          if (link != null && amount != null && displayAmount != null) {
+            addon.purchaseURL = link;
+            addon.purchaseAmount = amount;
+            addon.purchaseDisplayAmount = displayAmount;
+          }
+          break
         case "rating":
           let averageRating = parseInt(this._getTextContent(node));
           if (averageRating >= 0)
             addon.averageRating = Math.min(5, averageRating);
           break;
         case "reviews":
           let url = this._getTextContent(node);
           let num = parseInt(node.getAttribute("num"));
@@ -941,16 +974,23 @@ var AddonRepository = {
             addon.reviewCount = num;
           }
           break;
         case "status":
           let repositoryStatus = parseInt(node.getAttribute("id"));
           if (!isNaN(repositoryStatus))
             addon.repositoryStatus = repositoryStatus;
           break;
+        case "all_compatible_os":
+          let nodes = node.getElementsByTagName("os");
+          addon.isPlatformCompatible = Array.some(nodes, function(aNode) {
+            let text = aNode.textContent.toLowerCase().trim();
+            return text == "all" || text == Services.appinfo.OS.toLowerCase();
+          });
+          break;
         case "install":
           // No os attribute means the xpi is compatible with any os
           if (node.hasAttribute("os")) {
             let os = node.getAttribute("os").trim().toLowerCase();
             // If the os is not ALL and not the current OS then ignore this xpi
             if (os != "all" && os != Services.appinfo.OS.toLowerCase())
               break;
           }
@@ -1025,18 +1065,23 @@ var AddonRepository = {
       if (result == null)
         continue;
 
       // Ignore add-on missing a required attribute
       let requiredAttributes = ["id", "name", "version", "type", "creator"];
       if (requiredAttributes.some(function(aAttribute) !result.addon[aAttribute]))
         continue;
 
-      // Add only if there was an xpi compatible with this OS
-      if (!result.xpiURL)
+      // Add only if the add-on is compatible with the platform
+      if (!result.addon.isPlatformCompatible)
+        continue;
+
+      // Add only if there was an xpi compatible with this OS or there was a
+      // way to purchase the add-on
+      if (!result.xpiURL && !result.addon.purchaseURL)
         continue;
 
       results.push(result);
       // Ignore this add-on from now on by adding it to the skip array
       aSkip.ids.push(result.addon.id);
     }
 
     // Immediately report success if no AddonInstall instances to create
@@ -1052,19 +1097,24 @@ var AddonRepository = {
       let addon = aResult.addon;
       let callback = function(aInstall) {
         addon.install = aInstall;
         pendingResults--;
         if (pendingResults == 0)
           self._reportSuccess(results, aTotalResults);
       }
 
-      AddonManager.getInstallForURL(aResult.xpiURL, callback,
-                                    "application/x-xpinstall", aResult.xpiHash,
-                                    addon.name, addon.iconURL, addon.version);
+      if (aResult.xpiURL) {
+        AddonManager.getInstallForURL(aResult.xpiURL, callback,
+                                      "application/x-xpinstall", aResult.xpiHash,
+                                      addon.name, addon.iconURL, addon.version);
+      }
+      else {
+        callback(null);
+      }
     });
   },
 
   // Begins a new search if one isn't currently executing
   _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults) {
     if (this._searching || aURI == null || aMaxResults <= 0) {
       aCallback.searchFailed();
       return;
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -943,16 +943,26 @@ var gViewController = {
 
         if (gViewController.currentViewObj == gDetailView)
           gViewController.popState(doInstall);
         else
           doInstall();
       }
     },
 
+    cmd_purchaseItem: {
+      isEnabled: function(aAddon) {
+        if (!aAddon)
+          return false;
+        return !!aAddon.purchaseURL;
+      },
+      doCommand: function(aAddon) {
+        openURL(aAddon.purchaseURL);
+      }
+    },
 
     cmd_uninstallItem: {
       isEnabled: function(aAddon) {
         if (!aAddon)
           return false;
         return hasPermission(aAddon, "uninstall");
       },
       doCommand: function(aAddon) {
@@ -1176,29 +1186,29 @@ function createItem(aObj, aIsInstall, aI
   // the binding handles the rest
   item.setAttribute("value", aObj.id);
 
   return item;
 }
 
 function sortElements(aElements, aSortBy, aAscending) {
   const DATE_FIELDS = ["updateDate"];
-  const INTEGER_FIELDS = ["size", "relevancescore"];
+  const NUMERIC_FIELDS = ["size", "relevancescore", "purchaseAmount"];
 
   function dateCompare(a, b) {
     var aTime = a.getTime();
     var bTime = b.getTime();
     if (aTime < bTime)
       return -1;
     if (aTime > bTime)
       return 1;
     return 0;
   }
 
-  function intCompare(a, b) {
+  function numberCompare(a, b) {
     return a - b;
   }
 
   function stringCompare(a, b) {
     return a.localeCompare(b);
   }
 
   function getValue(aObj) {
@@ -1213,18 +1223,18 @@ function sortElements(aElements, aSortBy
       return null;
 
     return addon[aSortBy];
   }
 
   var sortFunc = stringCompare;
   if (DATE_FIELDS.indexOf(aSortBy) != -1)
     sortFunc = dateCompare;
-  else if (INTEGER_FIELDS.indexOf(aSortBy) != -1)
-    sortFunc = intCompare;
+  else if (NUMERIC_FIELDS.indexOf(aSortBy) != -1)
+    sortFunc = numberCompare;
 
   aElements.sort(function(a, b) {
     if (!aAscending)
       [a, b] = [b, a];
 
     var aValue = getValue(a);
     var bValue = getValue(b);
 
@@ -1731,16 +1741,17 @@ var gSearchView = {
   },
 
   show: function(aQuery, aRequest) {
     gEventManager.registerInstallListener(this);
 
     this.showEmptyNotice(false);
     this.showAllResultsLink(0);
     this.showLoading(true);
+    this._sorters.showprice = false;
 
     gHeader.searchQuery = aQuery;
     aQuery = aQuery.trim().toLocaleLowerCase();
     if (this._lastQuery == aQuery) {
       this.updateView();
       gViewController.notifyViewChanged();
       return;
     }
@@ -1765,18 +1776,21 @@ var gSearchView = {
         if (aQuery.length > 0) {
           score = self.getMatchScore(aObj, aQuery);
           if (score == 0 && !aIsRemote)
             return;
         }
 
         let item = createItem(aObj, aIsInstall, aIsRemote);
         item.setAttribute("relevancescore", score);
-        if (aIsRemote)
+        if (aIsRemote) {
           gCachedAddons[aObj.id] = aObj;
+          if (aObj.purchaseURL)
+            self._sorters.showprice = true;
+        }
 
         elements.push(item);
       });
     }
 
     function finishSearch(createdCount) {
       if (elements.length > 0) {
         sortElements(elements, self._sorters.sortBy, self._sorters.ascending);
@@ -2239,16 +2253,24 @@ var gDetailView = {
       var amount = document.getElementById("detail-contrib-suggested");
       amount.value = gStrings.ext.formatStringFromName("contributionAmount2",
                                                        [aAddon.contributionAmount],
                                                        1);
     } else {
       contributions.hidden = true;
     }
 
+    if ("purchaseURL" in aAddon && aAddon.purchaseURL) {
+      var purchase = document.getElementById("detail-purchase-btn");
+      purchase.label = gStrings.ext.formatStringFromName("cmd.purchaseAddon.label",
+                                                         [aAddon.purchaseDisplayAmount],
+                                                         1);
+      purchase.accesskey = gStrings.ext.GetStringFromName("cmd.purchaseAddon.accesskey");
+    }
+
     var updateDateRow = document.getElementById("detail-dateUpdated");
     if (aAddon.updateDate) {
       var date = formatDate(aAddon.updateDate);
       updateDateRow.value = date;
     } else {
       updateDateRow.value = null;
     }
 
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -243,40 +243,50 @@
     <content orient="horizontal">
       <xul:button anonid="name-btn" class="sorter"
                   label="&sort.name.label;" tooltiptext="&sort.name.tooltip;"
                   oncommand="this.parentNode._handleChange('name');"/>
       <xul:button anonid="date-btn" class="sorter"
                   label="&sort.dateUpdated.label;"
                   tooltiptext="&sort.dateUpdated.tooltip;"
                   oncommand="this.parentNode._handleChange('updateDate');"/>
+      <xul:button anonid="price-btn" class="sorter" hidden="true"
+                  label="&sort.price.label;"
+                  tooltiptext="&sort.price.tooltip;"
+                  oncommand="this.parentNode._handleChange('purchaseAmount');"/>
       <xul:button anonid="relevance-btn" class="sorter" hidden="true"
                   label="&sort.relevance.label;"
                   tooltiptext="&sort.relevance.tooltip;"
                   oncommand="this.parentNode._handleChange('relevancescore');"/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         if (!this.hasAttribute("sortby"))
           this.setAttribute("sortby", "name");
 
         if (this.getAttribute("showrelevance") == "true")
           this._btnRelevance.hidden = false;
 
+        if (this.getAttribute("showprice") == "true")
+          this._btnPrice.hidden = false;
+
         this._refreshState();
       ]]></constructor>
 
       <field name="handler">null</field>
       <field name="_btnName">
         document.getAnonymousElementByAttribute(this, "anonid", "name-btn");
       </field>
       <field name="_btnDate">
         document.getAnonymousElementByAttribute(this, "anonid", "date-btn");
       </field>
+      <field name="_btnPrice">
+        document.getAnonymousElementByAttribute(this, "anonid", "price-btn");
+      </field>
       <field name="_btnRelevance">
         document.getAnonymousElementByAttribute(this, "anonid", "relevance-btn");
       </field>
 
       <property name="sortBy">
         <getter><![CDATA[
           return this.getAttribute("sortby");
         ]]></getter>
@@ -296,16 +306,38 @@
           val = !!val;
           if (val != this.ascending) {
             this.setAttribute("ascending", val);
             this._refreshState();
           }
         ]]></setter>
       </property>
 
+      <property name="showrelevance">
+        <getter><![CDATA[
+          return (this.getAttribute("showrelevance") == "true");
+        ]]></getter>
+        <setter><![CDATA[
+          val = !!val;
+          this.setAttribute("showrelevance", val);
+          this._btnRelevance.hidden = !val;
+        ]]></setter>
+      </property>
+
+      <property name="showprice">
+        <getter><![CDATA[
+          return (this.getAttribute("showprice") == "true");
+        ]]></getter>
+        <setter><![CDATA[
+          val = !!val;
+          this.setAttribute("showprice", val);
+          this._btnPrice.hidden = !val;
+        ]]></setter>
+      </property>
+
       <method name="setSort">
         <parameter name="aSort"/>
         <parameter name="aAscending"/>
         <body><![CDATA[
           var sortChanged = false;
           if (aSort != this.sortBy) {
             this.setAttribute("sortby", aSort);
             sortChanged = true;
@@ -320,17 +352,17 @@
           if (sortChanged)
             this._refreshState();
         ]]></body>
       </method>
 
       <method name="_handleChange">
         <parameter name="aSort"/>
         <body><![CDATA[
-          const ASCENDING_SORT_FIELDS = ["name"];
+          const ASCENDING_SORT_FIELDS = ["name", "purchaseAmount"];
 
           // Toggle ascending if sort by is not changing, otherwise
           // name sorting defaults to ascending, others to descending
           if (aSort == this.sortBy)
             this.ascending = !this.ascending;
           else
             this.setSort(aSort, ASCENDING_SORT_FIELDS.indexOf(aSort) >= 0);
         ]]></body>
@@ -352,16 +384,24 @@
           if (sortBy == "updateDate") {
             this._btnDate.checkState = checkState;
             this._btnDate.checked = true;
           } else {
             this._btnDate.checkState = 0;
             this._btnDate.checked = false;
           }
 
+          if (sortBy == "purchaseAmount") {
+            this._btnPrice.checkState = checkState;
+            this._btnPrice.checked = true;
+          } else {
+            this._btnPrice.checkState = 0;
+            this._btnPrice.checked = false;
+          }
+
           if (sortBy == "relevancescore") {
             this._btnRelevance.checkState = checkState;
             this._btnRelevance.checked = true;
           } else {
             this._btnRelevance.checkState = 0;
             this._btnRelevance.checked = false;
           }
 
@@ -479,47 +519,54 @@
   </binding>
 
 
   <!-- Install status - Displays the status of an install/upgrade. -->
   <binding id="install-status">
     <content>
       <xul:label anonid="message"/>
       <xul:progressmeter anonid="progress" class="download-progress"/>
+      <xul:button anonid="purchase-remote-btn" hidden="true"
+                  class="addon-control"
+                  oncommand="document.getBindingParent(this).purchaseRemote();"/>
       <xul:button anonid="install-remote-btn" hidden="true"
                   class="addon-control install" label="&addon.install.label;"
                   tooltiptext="&addon.install.tooltip;"
                   oncommand="document.getBindingParent(this).installRemote();"/>
       <xul:button anonid="restart-install-btn" hidden="true"
                   class="addon-control install" label="&addon.install.label;"
                   tooltiptext="&addon.install.tooltip;"
                   oncommand="document.getBindingParent(this).restartInstall();"/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         if (this.mInstall)
           this.initWithInstall(this.mInstall);
         else if (this.mControl.mAddon.install)
           this.initWithInstall(this.mControl.mAddon.install);
-        else if (this.mAddon)
+        else
           this.refreshState();
       ]]></constructor>
 
       <destructor><![CDATA[
         if (this.mInstall)
           this.mInstall.removeListener(this);
       ]]></destructor>
 
       <field name="_message">
         document.getAnonymousElementByAttribute(this, "anonid", "message");
       </field>
       <field name="_progress">
         document.getAnonymousElementByAttribute(this, "anonid", "progress");
       </field>
+      <field name="_purchaseRemote">
+        document.getAnonymousElementByAttribute(this, "anonid",
+                                                "purchase-remote-btn");
+      </field>
       <field name="_installRemote">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "install-remote-btn");
       </field>
       <field name="_restartNeeded">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "restart-needed");
       </field>
@@ -544,16 +591,17 @@
           this.mInstall.addListener(this);
         ]]></body>
       </method>
 
       <method name="refreshState">
         <body><![CDATA[
           var showInstallRemote = false;
           var showRestartInstall = false;
+          var showPurchase = false;
 
           if (this.mInstall) {
 
             switch (this.mInstall.state) {
               case AddonManager.STATE_AVAILABLE:
                 if (this.mControl.getAttribute("remote") != "true")
                   break;
 
@@ -581,18 +629,27 @@
                 this.showMessage("installFailed", true);
                 break;
               case AddonManager.STATE_CANCELLED:
                 this.showMessage("installCancelled", true);
                 showRestartInstall = true;
                 break;
             }
 
+          } else if (this.mControl.mAddon.purchaseURL) {
+            this._progress.hidden = true;
+            showPurchase = true;
+            this._purchaseRemote.label =
+              gStrings.ext.formatStringFromName("addon.purchase.label",
+                [this.mControl.mAddon.purchaseDisplayAmount], 1);
+            this._purchaseRemote.tooltiptext =
+              gStrings.ext.GetStringFromName("addon.purchase.tooltip");
           }
 
+          this._purchaseRemote.hidden = !showPurchase;
           this._installRemote.hidden = !showInstallRemote;
           this._restartInstall.hidden = !showRestartInstall;
         ]]></body>
       </method>
 
       <method name="showMessage">
         <parameter name="aMsgId"/>
         <parameter name="aHideProgress"/>
@@ -603,16 +660,22 @@
           var msg = gStrings.ext.GetStringFromName(aMsgId);
           if (aHideProgress)
             this._message.value = msg;
           else
             this._progress.status = msg;
         ]]></body>
       </method>
 
+      <method name="purchaseRemote">
+        <body><![CDATA[
+          openURL(this.mControl.mAddon.purchaseURL);
+        ]]></body>
+      </method>
+
       <method name="installRemote">
         <body><![CDATA[
           if (this.mControl.getAttribute("remote") != "true")
             return;
 
           if (this.mControl.mAddon.eula) {
             var data = {
               addon: this.mControl.mAddon,
@@ -1201,18 +1264,18 @@
                                          .getTooltip(this.mAddon);
             this._removeBtn.setAttribute("tooltiptext", tooltip);
           } else {
             this._removeBtn.hidden = true;
           }
 
           this.setAttribute("active", this.mAddon.isActive);
 
-          var showProgress = this.mAddon.install &&
-                             this.mAddon.install.state != AddonManager.STATE_INSTALLED;
+          var showProgress = this.mAddon.purchaseURL || (this.mAddon.install &&
+                             this.mAddon.install.state != AddonManager.STATE_INSTALLED);
           this._showStatus(showProgress ? "progress" : "none");
         ]]></body>
       </method>
 
       <method name="_updateUpgradeInfo">
         <body><![CDATA[
           // Only update the version string if we're displaying the upgrade info
           if (this.hasAttribute("upgrade"))
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -131,16 +131,17 @@
               oncommand="gViewController.doCommand(event.target.id);">
     <command id="cmd_showItemDetails"/>
     <command id="cmd_findItemUpdates"/>
     <command id="cmd_showItemPreferences"/>
     <command id="cmd_showItemAbout"/>
     <command id="cmd_enableItem"/>
     <command id="cmd_disableItem"/>
     <command id="cmd_installItem"/>
+    <command id="cmd_purchaseItem"/>
     <command id="cmd_uninstallItem"/>
     <command id="cmd_cancelUninstallItem"/>
     <command id="cmd_cancelOperation"/>
     <command id="cmd_contribute"/>
   </commandset>
 
   <!-- main header -->
   <hbox id="header" align="center">
@@ -637,16 +638,18 @@
                     <button id="detail-disable-btn" class="addon-control disable"
                             label="&cmd.disableAddon.label;"
                             accesskey="&cmd.disableAddon.accesskey;"
                             command="cmd_disableItem"/>
                     <button id="detail-uninstall-btn" class="addon-control remove"
                             label="&cmd.uninstallAddon.label;"
                             accesskey="&cmd.uninstallAddon.accesskey;"
                             command="cmd_uninstallItem"/>
+                    <button id="detail-purchase-btn" class="addon-control purchase"
+                            command="cmd_purchaseItem"/>
                     <button id="detail-install-btn" class="addon-control install"
                             label="&cmd.installAddon.label;"
                             accesskey="&cmd.installAddon.accesskey;"
                             command="cmd_installItem"/>
                   </hbox>
                 </vbox>
               </hbox>
             </vbox>
--- a/toolkit/mozapps/extensions/test/browser/Makefile.in
+++ b/toolkit/mozapps/extensions/test/browser/Makefile.in
@@ -72,16 +72,17 @@ include $(DEPTH)/config/autoconf.mk
   browser_sorting.js \
   browser_uninstalling.js \
   browser_install.js \
   browser_recentupdates.js \
   browser_manualupdates.js \
   browser_globalwarnings.js \
   browser_eula.js \
   browser_updateid.js \
+  browser_purchase.js \
   $(NULL)
 
 _TEST_FILES = \
   head.js \
   browser_bug557956.js \
   browser_updatessl.js \
   browser_installssl.js \
   $(NULL)
@@ -94,16 +95,17 @@ include $(DEPTH)/config/autoconf.mk
   browser_bug591465.xml \
   browser_searching.xml \
   browser_searching_empty.xml \
   browser_updatessl.rdf \
   browser_install.rdf \
   browser_install.xml \
   browser_install1_3.xpi \
   browser_eula.xml \
+  browser_purchase.xml \
   discovery.html \
   redirect.sjs \
   releaseNotes.xhtml \
   $(NULL)
 
 # Disabled browser_bug586574.js due to bug 596174
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_purchase.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that marketplace results show up in searches, are sorted right and
+// attempting to buy links through to the right webpage
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_purchase.xml";
+
+var gManagerWindow;
+
+function test() {
+  // Turn on searching for this test
+  Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+  Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+
+  waitForExplicitFinish();
+
+  open_manager("addons://list/extension", function(aWindow) {
+    gManagerWindow = aWindow;
+
+    waitForFocus(function() {
+      var searchBox = gManagerWindow.document.getElementById("header-search");
+      searchBox.value = "foo";
+
+      EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+      EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+      wait_for_view_load(gManagerWindow, function() {
+        var remoteFilter = gManagerWindow.document.getElementById("search-filter-remote");
+        EventUtils.synthesizeMouseAtCenter(remoteFilter, { }, gManagerWindow);
+
+        run_next_test();
+      });
+    }, aWindow);
+  });
+}
+
+function end_test() {
+  close_manager(gManagerWindow, function() {
+    // Will have created an install so cancel it
+    AddonManager.getAllInstalls(function(aInstalls) {
+      is(aInstalls.length, 1, "Should have been one install created");
+      aInstalls[0].cancel();
+
+      finish();
+    });
+  });
+}
+
+function get_node(parent, anonid) {
+  return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function get_install_btn(parent) {
+  var installStatus = get_node(parent, "install-status");
+  return get_node(installStatus, "install-remote-btn");
+}
+
+function get_purchase_btn(parent) {
+  var installStatus = get_node(parent, "install-status");
+  return get_node(installStatus, "purchase-remote-btn");
+}
+
+// Tests that the expected results appeared
+add_test(function() {
+  var list = gManagerWindow.document.getElementById("search-list");
+  var items = Array.filter(list.childNodes, function(e) {
+    return e.tagName == "richlistitem";
+  });
+
+  is(items.length, 5, "Should be 5 results");
+
+  is(get_node(items[0], "name").value, "Ludicrously Expensive Add-on", "Add-on 0 should be in expected position");
+  is_element_hidden(get_install_btn(items[0]), "Add-on 0 install button should be hidden");
+  is_element_visible(get_purchase_btn(items[0]), "Add-on 0 purchase button should be visible");
+  is(get_purchase_btn(items[0]).label, "Purchase for $101\u2026", "Add-on 0 should have the right price");
+
+  is(get_node(items[1], "name").value, "Cheap Add-on", "Add-on 1 should be in expected position");
+  is_element_hidden(get_install_btn(items[1]), "Add-on 1 install button should be hidden");
+  is_element_visible(get_purchase_btn(items[1]), "Add-on 1 purchase button should be visible");
+  is(get_purchase_btn(items[1]).label, "Purchase for $0.99\u2026", "Add-on 2 should have the right price");
+
+  is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+  is_element_hidden(get_install_btn(items[2]), "Add-on 2 install button should be hidden");
+  is_element_visible(get_purchase_btn(items[2]), "Add-on 2 purchase button should be visible");
+  is(get_purchase_btn(items[2]).label, "Purchase for $1\u2026", "Add-on 3 should have the right price");
+
+  is(get_node(items[3], "name").value, "Free Add-on", "Add-on 3 should be in expected position");
+  is_element_visible(get_install_btn(items[3]), "Add-on 3 install button should be visible");
+  is_element_hidden(get_purchase_btn(items[3]), "Add-on 3 purchase button should be hidden");
+
+  is(get_node(items[4], "name").value, "More Expensive Add-on", "Add-on 4 should be in expected position");
+  is_element_hidden(get_install_btn(items[4]), "Add-on 4 install button should be hidden");
+  is_element_visible(get_purchase_btn(items[4]), "Add-on 4 purchase button should be visible");
+  is(get_purchase_btn(items[4]).label, "Purchase for $1.01\u2026", "Add-on 4 should have the right price");
+
+  run_next_test();
+});
+
+// Tests that sorting by price works
+add_test(function() {
+  var list = gManagerWindow.document.getElementById("search-list");
+
+  var sorters = gManagerWindow.document.getElementById("search-sorters");
+  var priceSorter = get_node(sorters, "price-btn");
+  info("Changing sort order");
+  EventUtils.synthesizeMouseAtCenter(priceSorter, { }, gManagerWindow);
+
+  var items = Array.filter(list.childNodes, function(e) {
+    return e.tagName == "richlistitem";
+  });
+
+  is(get_node(items[0], "name").value, "Free Add-on", "Add-on 0 should be in expected position");
+  is(get_node(items[1], "name").value, "Cheap Add-on", "Add-on 1 should be in expected position");
+  is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+  is(get_node(items[3], "name").value, "More Expensive Add-on", "Add-on 3 should be in expected position");
+  is(get_node(items[4], "name").value, "Ludicrously Expensive Add-on", "Add-on 4 should be in expected position");
+
+  info("Changing sort order");
+  EventUtils.synthesizeMouseAtCenter(priceSorter, { }, gManagerWindow);
+
+  var items = Array.filter(list.childNodes, function(e) {
+    return e.tagName == "richlistitem";
+  });
+
+  is(get_node(items[0], "name").value, "Ludicrously Expensive Add-on", "Add-on 0 should be in expected position");
+  is(get_node(items[1], "name").value, "More Expensive Add-on", "Add-on 1 should be in expected position");
+  is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+  is(get_node(items[3], "name").value, "Cheap Add-on", "Add-on 3 should be in expected position");
+  is(get_node(items[4], "name").value, "Free Add-on", "Add-on 4 should be in expected position");
+
+  run_next_test();
+});
+
+// Tests that clicking the buy button works from the list
+add_test(function() {
+  gBrowser.addEventListener("load", function() {
+    if (gBrowser.currentURI.spec == "about:blank")
+      return;
+    gBrowser.removeEventListener("load", arguments.callee, true);
+
+    is(gBrowser.currentURI.spec, TESTROOT + "releaseNotes.xhtml?addon5", "Should have loaded the right page");
+
+    gBrowser.removeCurrentTab();
+
+    if (gUseInContentUI) {
+      is(gBrowser.currentURI.spec, "about:addons", "Should be back to the add-ons manager");
+      run_next_test();
+    }
+    else {
+      waitForFocus(run_next_test, gManagerWindow);
+    }
+  }, true);
+
+  var list = gManagerWindow.document.getElementById("search-list");
+  EventUtils.synthesizeMouseAtCenter(get_purchase_btn(list.firstChild), { }, gManagerWindow);
+});
+
+// Tests that clicking the buy button from the details view works
+add_test(function() {
+  gBrowser.addEventListener("load", function() {
+    if (gBrowser.currentURI.spec == "about:blank")
+      return;
+    gBrowser.removeEventListener("load", arguments.callee, true);
+
+    is(gBrowser.currentURI.spec, TESTROOT + "releaseNotes.xhtml?addon4", "Should have loaded the right page");
+
+    gBrowser.removeCurrentTab();
+
+    if (gUseInContentUI) {
+      is(gBrowser.currentURI.spec, "about:addons", "Should be back to the add-ons manager");
+      run_next_test();
+    }
+    else {
+      waitForFocus(run_next_test, gManagerWindow);
+    }
+  }, true);
+
+  var list = gManagerWindow.document.getElementById("search-list");
+  var item = list.firstChild.nextSibling;
+  list.ensureElementIsVisible(item);
+  EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+
+  wait_for_view_load(gManagerWindow, function() {
+    var btn = gManagerWindow.document.getElementById("detail-purchase-btn");
+    is_element_visible(btn, "Purchase button should be visible");
+
+    EventUtils.synthesizeMouseAtCenter(btn, { }, gManagerWindow);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_purchase.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+  <addon>
+    <name>Ludicrously Expensive Add-on</name>
+    <type id='1'>Extension</type>
+    <guid>addon5@tests.mozilla.org</guid>
+    <version>1.0</version>
+    <authors>
+      <author>
+        <name>Test Creator</name>
+        <link>http://example.com/creator.html</link>
+      </author>
+    </authors>
+    <status id='4'>Public</status>
+    <summary>Test summary</summary>
+    <description>Test description</description>
+    <compatible_applications>
+      <application>
+        <name>Firefox</name>
+        <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+      <application>
+        <name>SeaMonkey</name>
+        <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+    </compatible_applications>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <payment_data>
+      <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon5</link>
+      <amount amount="101">$101</amount>
+    </payment_data>
+  </addon>
+  <addon>
+    <name>Cheap Add-on</name>
+    <type id='1'>Extension</type>
+    <guid>addon2@tests.mozilla.org</guid>
+    <version>1.0</version>
+    <authors>
+      <author>
+        <name>Test Creator</name>
+        <link>http://example.com/creator.html</link>
+      </author>
+    </authors>
+    <status id='4'>Public</status>
+    <summary>Test summary</summary>
+    <description>Test description</description>
+    <compatible_applications>
+      <application>
+        <name>Firefox</name>
+        <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+      <application>
+        <name>SeaMonkey</name>
+        <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+    </compatible_applications>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <payment_data>
+      <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon2</link>
+      <amount amount="0.99">$0.99</amount>
+    </payment_data>
+  </addon>
+  <addon>
+    <name>Reasonable Add-on</name>
+    <type id='1'>Extension</type>
+    <guid>addon3@tests.mozilla.org</guid>
+    <version>1.0</version>
+    <authors>
+      <author>
+        <name>Test Creator</name>
+        <link>http://example.com/creator.html</link>
+      </author>
+    </authors>
+    <status id='4'>Public</status>
+    <summary>Test summary</summary>
+    <description>Test description</description>
+    <compatible_applications>
+      <application>
+        <name>Firefox</name>
+        <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+      <application>
+        <name>SeaMonkey</name>
+        <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+    </compatible_applications>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <payment_data>
+      <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon3</link>
+      <amount amount="1">$1</amount>
+    </payment_data>
+  </addon>
+  <addon>
+    <name>Free Add-on</name>
+    <type id='1'>Extension</type>
+    <guid>addon1@tests.mozilla.org</guid>
+    <version>1.0</version>
+    <authors>
+      <author>
+        <name>Test Creator</name>
+        <link>http://example.com/creator.html</link>
+      </author>
+    </authors>
+    <status id='4'>Public</status>
+    <summary>Test summary</summary>
+    <description>Test description</description>
+    <compatible_applications>
+      <application>
+        <name>Firefox</name>
+        <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+      <application>
+        <name>SeaMonkey</name>
+        <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+    </compatible_applications>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <install size="1">http://example.com/addon1.xpi</install>
+  </addon>
+  <addon>
+    <name>More Expensive Add-on</name>
+    <type id='1'>Extension</type>
+    <guid>addon4@tests.mozilla.org</guid>
+    <version>1.0</version>
+    <authors>
+      <author>
+        <name>Test Creator</name>
+        <link>http://example.com/creator.html</link>
+      </author>
+    </authors>
+    <status id='4'>Public</status>
+    <summary>Test summary</summary>
+    <description>Test description</description>
+    <compatible_applications>
+      <application>
+        <name>Firefox</name>
+        <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+      <application>
+        <name>SeaMonkey</name>
+        <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+        <min_version>0</min_version>
+        <max_version>*</max_version>
+      </application>
+    </compatible_applications>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <payment_data>
+      <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon4</link>
+      <amount amount="1.01">$1.01</amount>
+    </payment_data>
+  </addon>
+</searchresults>
--- a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml
@@ -673,16 +673,106 @@
         <appID>xpcshell@tests.mozilla.org</appID>
         <min_version>1</min_version>
         <max_version>1</max_version>
       </application>
     </compatible_applications>
     <install>http://localhost:4444/addons/test_AddonRepository_2.xpi</install>
   </addon>
 
+  <!-- Passes because the add-on has the right payment info -->
+  <addon>
+    <name>PASS</name>
+    <type id="1">Extension</type>
+    <guid>purchase1@tests.mozilla.org</guid>
+    <version>2.0</version>
+    <authors>
+      <author>
+        <name>Test Creator - Last Passing</name>
+        <link>http://localhost:4444/creatorLastPassing.html</link>
+      </author>
+    </authors>
+    <status id="4">Public</status>
+    <all_compatible_os>
+      <os>ALL</os>
+    </all_compatible_os>
+    <compatible_applications>
+      <application>
+        <appID>xpcshell@tests.mozilla.org</appID>
+        <min_version>1</min_version>
+        <max_version>1</max_version>
+      </application>
+    </compatible_applications>
+    <rating>5</rating>
+    <payment_data>
+      <link>http://localhost:4444/purchaseURL1</link>
+      <amount amount="5">$5</amount>
+    </payment_data>
+  </addon>
+
+  <!-- Passes because the add-on has the right payment info -->
+  <addon>
+    <name>PASS</name>
+    <type id="1">Extension</type>
+    <guid>purchase2@tests.mozilla.org</guid>
+    <version>2.0</version>
+    <authors>
+      <author>
+        <name>Test Creator - Last Passing</name>
+        <link>http://localhost:4444/creatorLastPassing.html</link>
+      </author>
+    </authors>
+    <status id="4">Public</status>
+    <all_compatible_os>
+      <os>XPCShell</os>
+    </all_compatible_os>
+    <compatible_applications>
+      <application>
+        <appID>xpcshell@tests.mozilla.org</appID>
+        <min_version>1</min_version>
+        <max_version>1</max_version>
+      </application>
+    </compatible_applications>
+    <rating>5</rating>
+    <payment_data>
+      <link>http://localhost:4444/purchaseURL2</link>
+      <amount amount="10.0">$10</amount>
+    </payment_data>
+  </addon>
+
+  <!-- Fails because the add-on doesn't match the platform -->
+  <addon>
+    <name>FAIL</name>
+    <type id="1">Extension</type>
+    <guid>purchase3@tests.mozilla.org</guid>
+    <version>2.0</version>
+    <authors>
+      <author>
+        <name>Test Creator - Last Passing</name>
+        <link>http://localhost:4444/creatorLastPassing.html</link>
+      </author>
+    </authors>
+    <status id="4">Public</status>
+    <all_compatible_os>
+      <os>FOO</os>
+    </all_compatible_os>
+    <compatible_applications>
+      <application>
+        <appID>xpcshell@tests.mozilla.org</appID>
+        <min_version>1</min_version>
+        <max_version>1</max_version>
+      </application>
+    </compatible_applications>
+    <rating>5</rating>
+    <payment_data>
+      <link>http://localhost:4444/purchaseURL3</link>
+      <amount amount="10">$10</amount>
+    </payment_data>
+  </addon>
+
   <!-- Passes because the Addon that has a matching XPI URL
        has a state = STATE_AVAILABLE (non-active install). This is the
        last passing add-on. -->
   <addon>
     <name>PASS</name>
     <type id="1">Extension</type>
     <guid>test-lastPassing@tests.mozilla.org</guid>
     <version>2.0</version>
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -30,17 +30,18 @@ const INSTALL_URL3  = "/addons/test_Addo
 // Properties of an individual add-on that should be checked
 // Note: name is checked separately
 var ADDON_PROPERTIES = ["id", "type", "version", "creator", "developers",
                         "description", "fullDescription", "developerComments",
                         "eula", "iconURL", "screenshots", "homepageURL",
                         "supportURL", "contributionURL", "contributionAmount",
                         "averageRating", "reviewCount", "reviewURL",
                         "totalDownloads", "weeklyDownloads", "dailyUsers",
-                        "sourceURI", "repositoryStatus", "size", "updateDate"];
+                        "sourceURI", "repositoryStatus", "size", "updateDate",
+                        "purchaseURL", "purchaseAmount", "purchaseDisplayAmount"];
 
 // Results of getAddonsByIDs
 var GET_RESULTS = [{
   id:                     "test1@tests.mozilla.org",
   type:                   "extension",
   version:                "1.1",
   creator:                {
                             name: "Test Creator 1",
@@ -168,16 +169,42 @@ var SEARCH_RESULTS = [{
   totalDownloads:         2222,
   weeklyDownloads:        3333,
   dailyUsers:             4444,
   sourceURI:              BASE_URL + "/test3.xpi",
   repositoryStatus:       4,
   size:                   5555,
   updateDate:             new Date(1265033045000)
 }, {
+  id:                     "purchase1@tests.mozilla.org",
+  type:                   "extension",
+  version:                "2.0",
+  creator:                {
+                            name: "Test Creator - Last Passing",
+                            url:  BASE_URL + "/creatorLastPassing.html"
+                          },
+  averageRating:          5,
+  repositoryStatus:       4,
+  purchaseURL:            "http://localhost:4444/purchaseURL1",
+  purchaseAmount:         5,
+  purchaseDisplayAmount:  "$5"
+}, {
+  id:                     "purchase2@tests.mozilla.org",
+  type:                   "extension",
+  version:                "2.0",
+  creator:                {
+                            name: "Test Creator - Last Passing",
+                            url:  BASE_URL + "/creatorLastPassing.html"
+                          },
+  averageRating:          5,
+  repositoryStatus:       4,
+  purchaseURL:            "http://localhost:4444/purchaseURL2",
+  purchaseAmount:         10,
+  purchaseDisplayAmount:  "$10"
+}, {
   id:                     "test-lastPassing@tests.mozilla.org",
   type:                   "extension",
   version:                "2.0",
   creator:                {
                             name: "Test Creator - Last Passing",
                             url:  BASE_URL + "/creatorLastPassing.html"
                           },
   averageRating:          5,
@@ -238,20 +265,20 @@ function check_results(aActualAddons, aE
   // Additional tests
   aActualAddons.forEach(function(aActualAddon) {
     // Separately check name so better messages are outputted when failure
     if (aActualAddon.name == "FAIL")
       do_throw(aActualAddon.id + " - " + aActualAddon.description);
     if (aActualAddon.name != "PASS")
       do_throw(aActualAddon.id + " - " + "invalid add-on name " + aActualAddon.name);
 
-    do_check_eq(aActualAddon.install == null, !!aInstallNull);
+    do_check_eq(aActualAddon.install == null, !!aInstallNull || !aActualAddon.sourceURI);
 
     // Check that sourceURI property consistent within actual addon
-    if (!aInstallNull)
+    if (aActualAddon.install)
       do_check_eq(aActualAddon.install.sourceURI.spec, aActualAddon.sourceURI.spec);
   });
 }
 
 // Complete a search, also testing cancelSearch() and isSearching
 function complete_search(aSearch, aSearchCallback) {
   var failCallback = {
     searchSucceeded: function(addons, length, total) {