Managing in-app updates in React native applications without any native code

Mritul
6 min readMay 4, 2024

--

I’m writing this article because I had no single tutorial that I could follow to manage in-app updates in React native applications so had to look around and gather stuff from different places to successfully implement it. So let us begin🚀.

*This is going to be a long one because I’ve tried to include every single step (click by click) you need to follow .

The problem…

Well the main issue with update management is checking the latest store version to decide if we should force the user to update or make the update a flexible one. So we can go about it in 3 ways:

  • Scrape Play Store/ App Store page of your application and get the latest semantic(v1.0.3 and stuff) version. But the problem with this is play store does not give access to this out of the box and you have to login on a device just to see the version and this introduces unnecessary hassles while scraping 😔.
  • Maintain the latest app version in the backend and make an API call to check the version from the React Native application. But again, this is not automated and you have to keep updating the backend each time an update is released 😤.
  • Now the best way to go about it, is what we will be seeing in this article making life easier for me, you and everyone else 🎉.

A tiny note

The semantic version of your iOS application on App Store is publicly available so we can just scrape that and straight up compare it with the current version installed in the users device inside of the React Native app and use that to decide if the update is a MANDATORY one or a FLEXIBLE one.

So this guide is to automate things for Android specifically.

The workflow

The steps involved are a few :

  1. Make a service account on Google Developer Console that can access our Google Play Console application through the Android Publisher developer API linked here (fret not ! We have a Python SDK for this, so we need not make separate API requests 😉).
  2. Write a Python script that would publish your app release bundle (the .aab you get after building your Android application). I’ll cover why we cannot use the Play Console directly below, no worries 😄.
  3. Finally, we use the sp-react-native-in-app-updates library that lets us access the Play Core API without any native code and pure JS (this is the ONLY library that does this. Glad it exists 🙏). With this, we can handle Play Store updates directly from inside of the application.

So why wait, let’s dive in !

Step 1 :- Make a service account and provide access to your application from Play Console

Here is a reference on how to make one to authenticate with Google’s API. But yes, in brief:

  1. Make a service account on your Google Developer Console page. (Make a project if you do not have one already)
  2. Have the email ID of the service account handy. We’ll need it later.
  3. Also, go to your service account you just created and get the JSON key downloaded.

4. Now in the developer console, go to API & Services and enable the Google Play Android Developer API.

5. Now head to Play Console and provide the permissions

Click the 3 dots and Invite Users.

Now provide the email of the service account we created earlier and go below, add your application, and give the Account Permissions listed below :

  • View app information and download bulk reports (read-only)
  • View financial data, orders, and cancellation survey responses
  • Manage orders and subscriptions
  • Release to production, exclude devices, and use Play App Signing
  • Release apps to testing tracks
  • Manage testing tracks and edit tester lists
  • Manage store presence

Step 2 :- The Python script to upload the app bundle and release on Play Store

First of all, why can’t I just upload the bundle from the Play Console ? Why through the API ?

Alright so, it’s because Play Console lets you set something called the Update Priority (a number from 0 — 5 which we can then use in the React Native application to decide if the update is MANDATORY or OPTIONAL) and it can only be done through the API for now.

Install the necessary libraries

pip install google-api-python-client oauth2client

Just run the code below with your app-release.aab and your service account key JSON file in the same directory and you’ve successfully released an update with desired priority.

DEVELOPER_ID = # You can find this in the URL when you access your Play Console (looks like this 51223359276202169081)
SERVICE_ACCOUNT_EMAIL =
PACKAGE_NAME = # com.company.appname
SERVICE_ACCOUNT_KEY_FILE =
SCOPES = [
"https://www.googleapis.com/auth/androidmanagement",
"https://www.googleapis.com/auth/androidpublisher",
]
AAB_FILE = "app-release.aab"
TRACK = "internal" # Choose the preferred one
UPDATE_PRIORITY = 0 # 5 - MANDATORY, 0,1,2,3,4 - OPTIONAL is what I've considered

from googleapiclient.discovery import build
from oauth2client import client, service_account
import mimetypes
import socket

socket.setdefaulttimeout(7 * 24 * 60 * 60)
mimetypes.add_type("application/octet-stream", ".aab")


def uploadBundle():
credentials = service_account.ServiceAccountCredentials.from_json_keyfile_name(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)

service = build("androidpublisher", "v3", credentials=credentials)

try:
print("Sending edit request...")

edit_request = service.edits().insert(body={}, packageName=PACKAGE_NAME)
result = edit_request.execute()
edit_id = result["id"]

print("Uploading AAB...")

aab_response = (
service.edits()
.bundles()
.upload(editId=edit_id, packageName=PACKAGE_NAME, media_body=AAB_FILE)
.execute()
)

print(aab_response)

print("Version code %d has been uploaded" % aab_response["versionCode"])

print("Updating track info...")

track_response = (
service.edits()
.tracks()
.update(
editId=edit_id,
track=TRACK,
packageName=PACKAGE_NAME,
body={
"releases": [
{
"versionCodes": [str(aab_response["versionCode"])],
"status": "completed",
"inAppUpdatePriority": UPDATE_PRIORITY,
}
]
},
)
.execute()
)

print(track_response)

print(
"Track %s is set with releases: %s"
% (track_response["track"], str(track_response["releases"]))
)

print("Committing...")

commit_request = (
service.edits().commit(editId=edit_id, packageName=PACKAGE_NAME).execute()
)

print('Edit "%s" has been committed' % (commit_request["id"]))

except client.AccessTokenRefreshError:
print(
"The credentials have been revoked or expired, please re-run the "
"application to re-authorize"
)




if __name__ == "__main__":
uploadBundle()

Step 3 :- Final react native code

Head to your React Native application and install the sp-react-native-in-app-updates library.

Write the necessary util functions in say, a utils.ts file from which you can later import.

const inAppUpdates = new SpInAppUpdates(
false // isDebug
);


export const checkUpdate = async()=>{
try {
console.log("Checking for updates....")
const hasUpdate = await inAppUpdates.checkNeedsUpdate()
if(!hasUpdate.shouldUpdate) return null
if(Platform.OS==="android"){
// @ts-ignore
const storeUpdatePriority = hasUpdate.other.updatePriority // Check in backend as android core doesnt let you see semver and it lets only version code
if(storeUpdatePriority==5) return "mandatory"
return "optional"
}
} catch (err) {
console.log("An error occurred while checking for updates")
console.log(err)
}
}

export const startUpdate = async ()=>{
try {
// According to version see if you should force update or not
let updateOptions: StartUpdateOptions = {};
if (Platform.OS === 'android') {
// android only, on iOS the user will be promped to go to your app store page
updateOptions = {
updateType: IAUUpdateKind.FLEXIBLE,
};
}
await inAppUpdates.startUpdate(updateOptions);
} catch (err) {
console.log("An error occurred while starting update")
console.log(err)
}
}

Then create a CheckUpdateScreen that is loaded first in the application :

import { View } from 'react-native'
import React, { useEffect } from 'react'
import { checkUpdate, showToast } from '../../utils/helpers'
import { ActivityIndicator } from 'react-native-paper'

const CheckUpdateScreen = ({navigation}) => {

useEffect(()=>{
(async()=>{
try {
const updateType = await checkUpdate()
if(updateType){
navigation.navigate("UpdateScreen",{updateType})
}else{
navigation.navigate("YourDesiredScreen")
}
} catch (err) {
showToast("An error occurred while checking for update","error")
}
})()
},[])
return (
<View className='flex-1 bg-white items-center justify-center'>
<ActivityIndicator size={40}></ActivityIndicator>
</View>
)
}

export default CheckUpdateScreen

And finally, the Update Screen where you prompt the user an update and start the update from inside of the the app.

import { Text, TouchableOpacity, View, Button } from 'react-native'
import React from 'react'
import { SafeAreaView } from 'react-native-safe-area-context'
import {startUpdate } from '../../utils/helpers'

const UpdateScreen = ({navigation,route}) => {
const updateType = route.params.updateType
usePreventBack(navigation)

const updateApp = async()=>{
try {
await startUpdate()
} catch (err) {
console.log("An error occurred while updating")
}
}

const returnBack = ()=>{
navigation.navigate("YourDesiredScreen")
}

return (
<SafeAreaView className={`flex-1 justify-center items-center px-10`}>
<Text className={`text-[26px] text-center mb-5`}>Update available</Text>
<Text className={`text-[13px] text-center opacity-50`}>Get the latest version of our app to get the best experience</Text>
<View className='flex flex-row items-center mt-10'>
<Button text='Update' onPress={updateApp}/>
{updateType==="optional" &&
<TouchableOpacity className='ml-5' onPress={returnBack}><Text className='text-dark-blue text-[20px] font-inter-medium'>LATER</Text></TouchableOpacity>
}
</View>
</SafeAreaView>
)
}

export default UpdateScreen

Hope this was intuitive to follow and useful. Drop some claps on your way 😄 !

--

--

Mritul

I develop web and mobile apps sharing what I know at the same time