Blog
Publish your PWA as an Android app into the Google Play Store
29/05/2020

You could make an Android app. Learn some Java or Kotlin, learn how the Android UI system works and read thousands of tutorials and developer guides. But… what if you already have a Progressive Web App? You can package the PWA as an Android app in just a few hours. In this blog post I will show you how we did it with budgetimize.com.

I assume that you already have a PWA deployed on the web, and some basic understanding how to build Android applications.
Google Play

The theory: Trusted Web Activity

We will use the Trusted Web Activity protocol to open your PWA within a native Android app. Under the hood, your PWA is still running in a browser using custom tabs, but your users won’t notice it. There is no URL bar and users can download it from Google Play. The PWA itself is still loaded from your URL, so any updates you make to it are also applied to your Android app, without having to publish it again to the Google Play Store.

The basics

Open Android studio and create a new empty project. Add the following dependencies to your build.gradle file:

dependencies {
  implementation 'androidx.browser:browser:1.2.0'
  implementation
  'com.google.androidbrowserhelper:androidbrowserhelper:1.3.0'
} 

Now edit the <application> section of yourAndroidManifest.xml . Make sure to replace example.com with the URL of your PWA. The first URL in <meta-data> is the resource that will be opened when you start the app. The first <intent-filter> will add a launcher icon to the app drawer, while the second one will allow users to open URLs of your PWA with your Android app.

<activity android:name=".MainActivity">
  <meta-data
    android:name="android.support.customtabs.trusted.DEFAULT_URL"
    android:value="https://www.example.com" />
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data
        android:host="www.example.com"
        android:scheme="https" />
  </intent-filter>
</activity>

Now create a new Activity called MainActivity with the following Kotlin code. For now, it’s just an empty activity.

package com.packagename

import com.google.androidbrowserhelper.trusted.LauncherActivity

class MainActivity : LauncherActivity() {}

You can now build your Android application and install it on an Android device. However, the app stills shows a URL bar on top of your application.

Hide the URL bar

For security reasons, Chrome will always show the URL bar unless an app as specific permission to hide it for a certain domain. You will have to create a Digital Asset Link between your PWA and the Android app. Sounds complex, but its very simple.

Make sure that the following URL /.well-known/assetlinks.json serves the following JSON document with the correct fingerprint and package name. The fingerprint is the fingerprint of the certificate that you sign your application with.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.packagename",
      "sha256_cert_fingerprints ["36:...:F6"]
    }
  }
]

If you now start the app again, the URL bar will not be shown and your app looks just like a native Android app.

That’s it, you can now deploy this app to the Google Play Store.

Advanced: Google Play Billing

If your PWA has the option to buy a premium version, you will have a bit more work. Google only allows applications published in the Google Play Store to use Play Billing for in-app purchases.

To make this work we will add a new native Android Activity that will be opened when users want to pay and send the result back to the PWA.

First add the new Activity to the <application> section of your AndroidManifest.xml. Replace appname with the name of your app or any other random string.

<activity android:name=".BillingActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data
        android:host="subscribe"
        android:scheme="appname" />
  </intent-filter>
</activity>

If you want to open the activity, you can redirect your PWA to appname://subscribe (use the correct values you used in the manifest). This will open the native Android Activity where you can use any functionality from the Android API. In this example, we will open the Play Billing process.

Create a new activity called BillingActivity. In the activity you can implement the Google Play Billing logic, just as in any regular Android App. Use the Android Developer guide to see which options you want to use.

Once you implemented BillingActivity you can make purchases from within your app. But, your PWA has no way of knowing that the user has actually purchased an in-app purchase or subscription. Let’s fix that.

Open the MainActivity you created earlier and add the following function:

override fun getLaunchingUrl(): Uri? {
  val uri: Uri.Builder = super.getLaunchingUrl().buildUpon()
  val sharedPref = getPreferences(Context.MODE_PRIVATE)
  val purchaseToken = sharedPref.getString("purchaseToken", "")
  if (purchaseToken != "") {
    uri.appendQueryParameter("purchaseToken", purchaseToken)
  }
  return uri.build()
}

This will check if there is a purchase token stored in the shared preferences and append it to the URL when there is one available. You will have to use the Android Developer guide to check for purchases or subscriptions each time you start the app and store the purchase token of it in shared preferences. This allows for faster startups as your app doesn’t need to wait for Google Play to receive your users purchases.

Now you can edit your PWA to read the purchase token from the URL and sent it to your server. You server can verify this token and give the user the premium version or any other reward.