HOME‎ > ‎Announcements‎ > ‎

I've started a little experiment...

posted Sep 21, 2014, 7:58 AM by Romain Vialard   [ updated Apr 6, 2016, 4:44 AM by Stéphanie Chhay ]

From the Apps Script Gallery to the Add-on Store

Last spring, Google decided to ditch the old Apps Script gallery and announced the new Add-ons stores. I have published a lot of scripts in the old gallery and I was pleased to see big changes in the way we can make our scripts available to a lot of users. 

In the past, Google provided no statistics at all, so if you wanted to see how many users were using your script, you had to put something in your code to handle that and it was hard to compare your script to others in the gallery. Now you can quickly see how popular is your add-on, people can rate it, comment and easily ask for help.


More important, installing a script from the gallery actually meant making a one-time copy of the code. So there was no easy way to push updates to users and it was not easy to provide support as everybody was using a different version of the code. Now everybody is using the same version of an add-on and each time I push a big fix or a new feature, people automatically benefit from it. In the process, we lost the ability to see the source code of the scripts we decide to install, but I don't think it's a real issue. People who wants to open source their add-on still can and Google review each add-on before their publication, to make sure they won't do anything bad.

As part of the Add-ons launch, I released a new version of Yet Another Mail Merge. I don't know how many people were using the old version available in the gallery but the add-on version is quite popular. Too popular in fact as I began to receive a lot of feedbacks and requests for help.


As said before, I have created a lot of scripts, gadgets and libraries, always on my free time, and I usually can only count on myself to support them all. The move to the add-on store made scripts installable & usable by non-techie users (the store is easier to access and prettier, with lots of screenshots, ratings,...) and they need extra support. Even if your app is really easy to use. Even if you have good documentation. Simply take a look at the support forums for Gmail, Drive and so on, and you will understand :)

From 100% free app to paid plans

Ok, so I didn't know exactly what to do. Yet Another Mail Merge was beginning to be a bit too time consuming to maintain and I still had other popular scripts to move to the Add-on store. My wife was ready to kill me and my users weren't really happy (lots of bad ratings).

At the same time, I was talking with a lot of people who were convinced it was not possible to make money on this new store. I wasn't so sure and I wanted to prove them wrong. But at the same time, I wanted to keep YAMM mainly free for all users. So I started to use Google Analytics to keep track on the global number of emails sent every day with the add-on. More than 300 000 emails sent every day in July. I was impressed. And 97% of users were using YAMM to send less than 200 emails in one batch (note that gmail.com users are restricted to 100 recipients per day by Google).

So I decided to target the remaining 3% (the most heavy users) and reduce their quota in the free version from up to 1500 to 200 recipients per day and to create paid plans for those who want to get more quota, have better support and help me improve & support the app.


Bringing in-app purchases to add-ons

The add-on store is built on top of the Chrome Web Store and you can monetize items in the Chrome Web Store. But Google doesn't offer tools to monetize add-ons... (yet).
I wanted to make it as easy as possible for people to buy a plan. Without having to register for a service like Google Wallet. That's why I used PayPal (buyers can check out without a PayPal account). Plus PayPal has APIs and the IPN service (Instant Payment Notification) that I was able to link to an Apps Script web app.
Thus, I have the following process automated:
  1. I check if the user is eligible to one of my paid plans
  2. If he is, I display a notification in the app with a button to open the page listing the paid plans
  3. In this page, I've added customized "Buy Now" buttons from PayPal (you can even link a small form to your buttons, letting me ask users which Google Account they want to upgrade)
  4. Once someone has paid, PayPal automatically send all purchase information to an Apps Script Web App via the IPN service
  5. The Apps Script web app stores all info in a Sheet, add the Google Account to a Script Property (my white-list of paying users) and send a confirmation by email

Does it work?

Yes. I have started this experiment at the beginning of August. For now on, purchases are pretty stable (multiple purchases every day, including some enterprise plans at $100) and I earn enough to keep spending time on improvements. Since August, I released a lot of bug fixes and ratings on my add-on have improved. Very few people have complained about the limitations of the free version and I spend more time to answer people's questions. I have also used Google Moderator to ask users what the missing features are and I'm now building them.

Conclusion

  • It is possible to sell apps built 100% with Google Apps Script.
  • People are ready to buy items on the add-on stores.
  • It's easy enough to implement in-app purchases.
  • I will certainly continue to propose apps mostly free, with some premium features / more quota for paying users.




Annex

Example of Apps Script Web Service receiving Instant Payment Notification from PayPal


// https://www.paypal.com/fr/cgi-bin/webscr?cmd=_display-ipns-history

function doPost(e) {
  var ss = SpreadsheetApp.openById('XXX');
  var sheet = ss.getSheets()[0];
  var data = sheet.getDataRange().getValues();
  var uniqueId = e.parameter.txn_id;
  // Check Paypal status
  if(e.parameter.payment_status == "Refunded") {
    uniqueId = e.parameter.parent_txn_id;
  }
  // Check if already exist in database
  for(var i = 0; i < data.length; i++) {
    if(data[i][11] == uniqueId) {
      if(e.parameter.payment_status == "Refunded") {
        sheet.getRange(i+1, 4).setValue("Refunded");
      }
      else {
        ///////////////////////////////////////////////////////////////////////////////
        // HTTP POSTs the complete, unaltered message back to PayPal
        // This message must contain the same fields, in the same order, 
        // as the original IPN from PayPal, all preceded by cmd=_notify-validate. 
        // Further, this message must use the same encoding as the original.
        //
        // PayPal sends a single word back - 
        // either VERIFIED (if the message matches the original) 
        // or INVALID (if the message does not match the original).
        ///////////////////////////////////////////////////////////////////////////////
        var params = {
          method: "post"
        }
        var resp = UrlFetchApp.fetch("https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate&"+e.postData.contents, params);
      }
      return ContentService.createTextOutput('').setMimeType(ContentService.MimeType.TEXT);
    }
  }
  // If not yet in database, add entry
  var rowData = [];
  rowData.push(new Date(e.parameter.payment_date));
  rowData.push(e.parameter.item_number);
  rowData.push(e.parameter.option_selection1);
  rowData.push(e.parameter.payment_status);
  rowData.push(e.parameter.payment_gross);
  rowData.push(e.parameter.mc_currency);
  rowData.push(e.parameter.payment_fee);
  rowData.push(e.parameter.first_name);
  rowData.push(e.parameter.last_name);
  rowData.push(e.parameter.payer_email);
  rowData.push(e.parameter.residence_country);
  rowData.push(e.parameter.txn_id);
  rowData.push(JSON.stringify(e));
  sheet.appendRow(rowData);
  return ContentService.createTextOutput('').setMimeType(ContentService.MimeType.TEXT);
}