Sergey Maskalik

Sergey Maskalik's blog

Focusing on improving tiny bit every day

This article describes how to use Google Play Developer API with the Google API Client library with Java to refund Google Play In-App Purchases. Hopefully this information and the example script provided will save someone a bit of time.

Unlike Apple App Store, the Google Play platform offers an ability to refund in-app purchase orders, which can be accomplished using one of the following:

If you need to refund more than a handful of orders, the REST API is a life saver. Google also provides a library that makes it very easy to get started. The only downside is there aren’t many examples and the documentation just points to the Javadoc references. Having gone through that exercise, here is my own example on how to refund orders using v3 of the client library.

Prerequisite

You will need a Google Service account that has permissions to manage orders.

  1. Go to the API Access page on the Google Play Console.
  2. Under Service Accounts, click Create Service Account.
  3. Follow the instructions on the page to create your service account.
  4. Once you’ve created the service account on the Google Developers Console, click Done. The API Access page automatically refreshes, and your service account will be listed.
  5. Click Grant Access to provide the service account the rights to manage orders.

If you get lost you can also try this guide from Google’s docs.

Installation

Add the Google API Client library to your project using your favorite build tool.

Maven

Add the following lines to your pom.xml file:

<project>
  <dependencies>
    <dependency>
      <groupId>com.google.apis</groupId>
      <artifactId>google-api-services-androidpublisher</artifactId>
      <version>v3-rev20200223-1.30.9</version>
    </dependency>
  </dependencies>
</project>

Gradle

repositories {
  mavenCentral()
}
dependencies {
  compile 'com.google.apis:google-api-services-androidpublisher:v3-rev20200223-1.30.9'
}

Example Script

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

class Scratch {
    public static void main(String[] args) {
        enableLogging();

        Set<String> SCOPES = Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER);
        JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance();
        NetHttpTransport httpTransport = new NetHttpTransport();
        GoogleCredential credential = getGoogleCredentials("private service key goes here")
                                        .createScoped(SCOPES);
        AndroidPublisher androidPublisher = new AndroidPublisher
                .Builder(httpTransport, jsonFactory, credential)
                .setApplicationName("com.yourname.app")
                .build();
        try {
            androidPublisher.orders()
              .refund("com.yourname.app","GPA.3333-7777-6666-44444")
              .execute();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    private static GoogleCredential getGoogleCredentials(String privateKey) {
        try {
            return GoogleCredential.fromStream(
              new ByteArrayInputStream(privateKey.getBytes()));
        } catch (IOException e) {
            throw new IllegalArgumentException("Private key for Google Pay is invalid", e);
        }
    }

    public static void enableLogging() {
        Logger logger = Logger.getLogger(HttpTransport.class.getName());
        logger.setLevel(Level.CONFIG);
        logger.addHandler(new Handler() {
            @Override
            public void close() throws SecurityException {
            }
            @Override
            public void flush() {
            }
            @Override
            public void publish(LogRecord record) {
                // Default ConsoleHandler will print >= INFO to System.err.
                if (record.getLevel().intValue() < Level.INFO.intValue()) {
                    System.out.println(record.getMessage());
                }
            }
        });
    }
}

In the above script:

  • Refund API returns 204 HTTP Status Code (No content) if a refund was successful.
  • Additional logging is enabled by calling enableLogging() . This provides debug logging of NetHttpTransport request/response.

Hopefully this example script will save someone a bit of time.

Implementing cross-platform purchases in iOS is not very well documented and can cause some anxiety before submitting your app for Apple’s approval. In this post I’m hoping to provide some basic guidance and explanation about which type of in-app products are best suited for that task.

The use case I’m describing is when a user’s purchased inventory is tied to an organizational (non-apple) user account and is tracked by an external service. For example, when a user buys a song from your mobile app on iOS or Android device, and then logs in to a web version of your app, the song should be instantly available to play in his/her collection. It also works the other way around, where you can buy something on the web, and it should be available on a mobile device. The purchased inventory and the list of available products is independent of the mobile platform. The bottom line: , it will be the responsibility of your inventory service to make sure that items which were already purchased can only be purchased once and not be displayed to the users.

One of the in-app purchase types that is not compatible with cross-platform payments is the App Store’s non-consumable type. Apple developer documentation defines it as:

Non-consumables are purchased once and do not expire, such as additional filters in a photo app.

This type also comes with a an important requirement:

If your app sells non-consumable, auto-renewable subscription or non-renewing subscription products, you must provide users with the ability to restore them.”

If you want to sell a cross-platform product that can only be purchased once, after reading the Apple definition, you may think that non-consumable type is the answer, but the additional requirement may confuse you.

There is also one place (App Store Review Guidelines) where documentation mentions multi platform services, but it doesn’t say anything about non-consumable types.

3.1.3(b) Multiplatform Services: Apps that operate across multiple platforms may allow users to access content, subscriptions, or features they have acquired in your app on other platforms or your web site, including consumable items in multiplatform games, provided those items are also available as in-app purchases within the app.

One of the features of App Store’s StoreKit is the ability to manage inventory of purchased items, meaning it will be responsible for filtering out items that were already purchased. This is handy if a user has lost or bought a new iOS device because it offers the ability to restore purchased items. However, when you let Apple manage your inventory, the purchased inventory will be tied to a user’s iTunes account and not your cross-platform user account. It’s also problematic if a user purchases an item outside of Apple’s platform; Apple would not be able to restore it.

When the inventory is managed by an external service there is no need for the second purchase management system (apple). The inventory travels with your organizational user account, available after login, and does not need to be restored to the device. Therefore, even though Apple does not mention it, non-consumable types are not compatible with the multiplatform purchase inventory.

This is why you can only use a consumable purchase type when you have an external purchased inventory. As soon as a user makes a purchase you top up his/her organizational account with content and consume the item. This inventory service is also used to filter out items that were already purchased because Apple is not a system of record anymore.

Making all your in-app purchase products consumables, even for items that can only be consumed once, might make you nervous, especially if you haven’t gone through the apple approval process before. But logically there is no other way. So hopefully this post save you some anxiety.

In his book, Atomic Habits, author James Clear tells a story about a struggling British cycling team that for over a hundred years won only one gold Olympic medal and did not win a single Tour de France, cycling’s biggest race . After the team hired a new performance director, he brought with him a new philosophy, referred to as “the aggregation of marginal gains,” which focused on tiny improvements in every thing that you do. Together with his team, the new director introduced over a hundred small improvements, including redesigning a bicycle seat, testing different fabrics for air dynamics, and rubbing alcohol on the tires to make them grip better. He even brought in a surgeon to teach riders how to wash their hands better so they would not get sick.

All these minor improvements had an incredible impact. Within five years after the new performance director took over, the team won 60% of the gold medals for the cycling events at the 2008 Olympic Games in Beijing.

If we apply the same philosophy to the software engineering teams, what type of tiny improvements could have a big impact in the long run? Here are a few that I think can be a good start.

Incremental Refactoring

By creating a good habit of leaving the code base better than it was before, chances are that our projects would be easier to maintain in the long run. Besides making the code more expressive, easier to read and modify, engineers can also add missing unit tests, cleaning up legacy comments, or format code. I think the trick is to have a balance and sprinkle improvements a little bit over time. Nobody wants to review a huge refactoring for correctness when it’s unrelated to the task assigned. As a bonus if you could separate refactoring in a separate pull request, your reviewers will thank you. Finally, if you are looking to learn more about refactoring, I highly recommend the book, Refactoring: Improving the Design of Existing Code.

Small Bug Fixes Without Tickets

Not every bug fix needs to go through a process of writing up a ticket, getting it estimated, prioritized and planned. If the bug fix is small enough and the engineer has the context around it, it is worth fixing it along with the primary task. The cost of the bug fix at that moment will be significantly lower and will not require a context switch for another engineer or yourself a few months later. Finally, customers will appreciate quick fixes if it affects them.

Document Root Cause Analysis for Major Incidents

Every failure is a great opportunity to learn from mistakes. Before we can do that, we must first gather all available information about the incident, ask five whys, and create an action plan on how it can be prevented in the future. It’s also valuable to share this information with the stakeholders and team, because they might have a different perspective and offer valuable insights that might not be obvious to the author. In addition to improving processes, documenting incidents is also valuable for tracking purposes; it helps to determine if the team is improving over the long term.

Reviewing Logs After Deployments

Reviewing error logs after each deployment can help identify issues caused by newly released code and get them resolved right away. It’s much cheaper to fix bugs when the context is fresh in the author’s mind. And it could help to save a lot of time and frustration for customers, support and the engineering team. Some bugs could require a data back-fill or other escalation, and if left unchecked, can create a lot of unnecessary work.

Quick Responses to Questions and Requests

When researchers compiled a huge database of the digital habits of teams at Microsoft, they found that the clearest warning sign of an ineffective manager was being slow to answer emails. Responding in a timely manner shows that you are conscientious — organized, dependable and hardworking. And that matters. In a comprehensive analysis of people in hundreds of occupations, conscientiousness was the single best personality predictor of job performance. - Adam Grant No, You Can’t Ignore Email. It’s Rude.

According to Adam Grant, an organizational psychologist, quick responses are the best predictor of job performance. Because our customers and peers are our top priority, it makes sense to try to get a little bit quicker at responding to emails or messages. I can vouch for this myself. I certainly appreciate when someone on my team reviews my code review request right away; it prevents me from switching off to a different task and allows me to get more stuff done.

README Files

Putting together a README file for projects will save time when someone else, or yourself in the future, has to get started on the project. You can find suggestions on how to make good README files here. Small investments into better documentation will pay off in the future.

Do Not Stop Here

This was not meant to be an exhaustive list, just a starting sample of things that may or may not work for your projects. Most important are not the improvements by themselves, but the willingness to adopt a philosophy of continuous aggregation of improvements that will make a difference in the long run.