Hey guys, In this article, I want to discuss implementing Android in app updates in react-native project.
If you don’t know about what are In App Updates. Please consider reading this article from google.
It is just a new way of updating apps, managed by google. Whenever there is a new update for your app.
In App Updates feature shows Popup or Screen asking your users to update app depending upon your urgency requirements.
Basically, there are two ways you can make your users to update app, one is Flexible (Shows Popup) and another is Immediate (Shows Screen) way.
And you can use Staleness, to decide how urgent the update is. Or you can also use Priority number to decide the urgency. Staleness is nothing but “how much time has passed since the user was notified of an update through the Google Play Store“, we use in days.
Priority is just a number between 0 to 5, defaults to 0. You can assume if the number is High, urgency is also high.
So, how are we going to implement it in react-native ? Unfortunately, at the time of writing this article I did not find any react-native package that does In App Updates. So we need to implement it ourselves.
Since the In App Update feature is complete JAVA/KOTLIN specific code. We will have to bridge the feature to react-native as module. And later we can use it normally like any other react native package.
Since I am more into JAVA, I will continue to code in java. If you are not familiar with java, or with complete JavaScript background, you can just skip this article and go to my Github link to quickly integrate into your project with few steps.
Before we start coding in Java, kindly read the documentation for coding native modules in Android for react-native.
Let’s not waste time and start coding…
Create a react-native project or open an existing project. And check if the app is running.
if the app is working fine, then open android folder of the project using Android studio. And let android studio build your project and run the app from android studio to double check everything is working fine.
Once your minimum functionality app is working, Open android
folder in your react-native project with Android Studio
and add implementation 'com.google.android.play:core:1.7.3'
at the end of dependencies section of the build.gradle(app)
file. Like below,
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
.......
implementation 'com.google.android.play:core:1.7.3' // add it at the end
}
Click sync after adding the dependency.
Then create a file InAppUpdateModule.java in the same directory of MainActivity.java (android/app/src/main/java/com/your-app-name/).
In the created file, extend class InAppUpdateModule with ReactContextBaseJavaModule. Once you extend, you will have to implement getName method and constructor with ReactApplicationContext as argument. Here is the code snippet.
public class InAppUpdateModule extends ReactContextBaseJavaModule {
InAppUpdateModule(ReactApplicationContext context) {
super(context);
}
@NonNull
@Override
public String getName() {
return "InAppUpdate";
}
}
getName is exact name of the module by which you access from the JavaScript code. In the above code we are returning “InAppUpdate” and we use the same name to access this module.
After that, let’s add few class properties like AppUpdateManager (responsible for app update process), ReactApplicationContext (it is Application context from react), and constants STALE_DAYS (number of days indicates staleness) and MY_REQUEST_CODE( it is request code of onActivityForResult).
private AppUpdateManager appUpdateManager;
private static ReactApplicationContext reactContext;
private static final int STALE_DAYS = 5;
private static final int MY_REQUEST_CODE = 0;
Next, we need the life cycle events from LifecycleEventListener and install status from InstallStateUpdatedListener(Listener to detect the progress of the installation/update). So let’s implement them.
public class InAppUpdateModule extends ReactContextBaseJavaModule
implements InstallStateUpdatedListener, LifecycleEventListener {
private AppUpdateManager appUpdateManager;
private static ReactApplicationContext reactContext;
private static final int STALE_DAYS = 5;
private static final int MY_REQUEST_CODE = 0;
InAppUpdateModule(ReactApplicationContext context) {
super(context);
}
@NonNull
@Override
public String getName() {
return "InAppUpdate";
}
@Override
public void onStateUpdate(InstallState state) {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
}
}
@Override
public void onHostResume() {
}
@Override
public void onHostPause() {
}
@Override
public void onHostDestroy() {
}
}
We also need a ActivityEventListener (responsible for listening to Activity events like onActivityForResult). Here is the code snippet add it to the above class.
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
if (requestCode == MY_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
System.out.println("Update flow failed! Result code: " + resultCode);
// If the update is cancelled or fails,
// you can request to start the update again.
}
}
}
};
Now, let’s connect the ActivityEventListener and LifecycleEventListener in the constructor of the class like below.
InAppUpdateModule(ReactApplicationContext context) {
super(context);
reactContext = context;
reactContext.addActivityEventListener(mActivityEventListener);
reactContext.addLifecycleEventListener(this);
}
After that, let’s implement checkUpdate method which is the trigger point of checking update. We need to annotate it with @ReactMethod to access it from the JavaScript as a method.
@ReactMethod
public void checkUpdate() {
appUpdateManager = AppUpdateManagerFactory.create(reactContext);
appUpdateManager.registerListener(this);
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.clientVersionStalenessDays() != null
&& appUpdateInfo.clientVersionStalenessDays() > STALE_DAYS
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}else{
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
}
});
}
The above code checks the availability of new version in the play store using appUpdateManager and it’s listener addOnSuccessListener. If the update is available in the play store, then it get’s triggered and after that it checks Staleness by using constant STALE_DAYS (in our case it is 5).
If the days are less then 5, FLEXIBLE way of updating app is triggerred or else IMMEDIATE way is triggered.
We also registered for InstallStateUpdatedListener using appUpdateManager in the above code.
Now create a method for snackbar to communicate with the user that update is downloaded and need to install it. Here is the code snippet.
private void popupSnackbarForCompleteUpdate() {
Snackbar snackbar =
Snackbar.make(Objects.requireNonNull(reactContext
.getCurrentActivity())
.findViewById(android.R.id.content).
getRootView(),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("RESTART", view -> appUpdateManager.completeUpdate());
snackbar.setActionTextColor(Color.GREEN);
snackbar.show();
}
Let’s call above method from onStateUpdate. Here is the code.
@Override
public void onStateUpdate(InstallState state) {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
}
And lastly, we need to resume the updating process even if the user leaves the and comes back. So let’s add that code in onHostResume. Here is the code.
@Override
public void onHostResume() {
if (appUpdateManager != null) {
appUpdateManager
.getAppUpdateInfo()
.addOnSuccessListener(
appUpdateInfo -> {
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
});
}
}
So that over all code for InAppUpdateModule.java looks like this. Make sure you import necessary classes by using short code ALT+ENTER.
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.Color;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManager;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.InstallState;
import com.google.android.play.core.install.InstallStateUpdatedListener;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.InstallStatus;
import com.google.android.play.core.install.model.UpdateAvailability;
import com.google.android.play.core.tasks.Task;
import java.util.Objects;
import static android.app.Activity.RESULT_OK;
public class InAppUpdateModule extends ReactContextBaseJavaModule
implements InstallStateUpdatedListener, LifecycleEventListener {
private AppUpdateManager appUpdateManager;
private static ReactApplicationContext reactContext;
private static final int STALE_DAYS = 5;
private static final int MY_REQUEST_CODE = 0;
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
if (requestCode == MY_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
System.out.println("Update flow failed! Result code: " + resultCode);
// If the update is cancelled or fails,
// you can request to start the update again.
}
}
}
};
InAppUpdateModule(ReactApplicationContext context) {
super(context);
reactContext = context;
reactContext.addActivityEventListener(mActivityEventListener);
reactContext.addLifecycleEventListener(this);
}
@NonNull
@Override
public String getName() {
return "InAppUpdate";
}
@ReactMethod
public void checkUpdate() {
appUpdateManager = AppUpdateManagerFactory.create(reactContext);
appUpdateManager.registerListener(this);
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.clientVersionStalenessDays() != null
&& appUpdateInfo.clientVersionStalenessDays() > STALE_DAYS
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}else{
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
}
});
}
@Override
public void onStateUpdate(InstallState state) {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
}
private void popupSnackbarForCompleteUpdate() {
Snackbar snackbar =
Snackbar.make(Objects.requireNonNull(reactContext
.getCurrentActivity())
.findViewById(android.R.id.content).
getRootView(),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("RESTART", view -> appUpdateManager.completeUpdate());
snackbar.setActionTextColor(Color.GREEN);
snackbar.show();
}
@Override
public void onHostResume() {
if (appUpdateManager != null) {
appUpdateManager
.getAppUpdateInfo()
.addOnSuccessListener(
appUpdateInfo -> {
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
reactContext.getCurrentActivity(),
MY_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void onHostPause() {
}
@Override
public void onHostDestroy() {
if (appUpdateManager != null) {
appUpdateManager.unregisterListener(this);
}
}
}
We have completed writing our Native code to bridge to JavaScript, all we need to do now is to write a package for it and add it to the MainApplication.java. Let’s do that by creating InAppUpdatePackage.java in the same directory of MainActivity.java. And paste the below code.
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class InAppUpdatePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new InAppUpdateModule(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
After adding the above code, open MainApplication.java and add it as a pacakge in getPackages method like below.
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new InAppUpdatePackage());
return packages;
}
Now you should be able to access this module in JavaScript, so create a file named InAppUpdate.js in your react native project and paste the following code.
import {NativeModules} from 'react-native'
module.exports = NativeModules.InAppUpdate
That’s it guys, now you should be able to use checkUpdate method from Java in JavaScript. Import InAppUpdate file and use it wherever you want. Here is how you call the checkUpdate method.
import InAppUpdate from './InAppUpdate'
InAppUpdate.checkUpdate()
You can easily place the above code in your component and check if the update exists, if the update exists it will show Popup or Screen based on Stalesness. The above code shows FLEXIBLE WAY(Popup) for 5 days and after that it will show IMMEDIATE WAY(SCREEN).
Thanks guys, I hope this article helps you.
Brilliant article – exactly what I needed to complete my ticket!
Thanks for the useful tutorial! I’ve followed the steps and finally while debugging using the link from internal app sharing, its redirected to playstore with my app opened but Install button is missing in it! My app is supported only in US. I’m trying out from a different country. Could that be the reason? Kindly clarify, Thanks
You need to install app (say v1) with in app updates added from app store internal link. Then update app in play store say v2. Now open app v1 see if you get an update of v2. (https://developer.android.com/guide/playcore/in-app-updates/test) read it from there. If the app is restricted to particular country then it is not possible to install it from another country. That is normal.
Thanks for the clarification!
InAppUpdateModule.java:68: error
i am getting this error
: lambda expressions are not supported in -source 1.7
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
^
(use -source 8 or higher to enable lambda expressions)
1 error
kindly use java 1.8 read it here https://developer.android.com/studio/write/java8-support
how to pass data from react to java package in order to control flexible or immediate update, rather than doing it by STALE_DAYS?
Try changing the code in InAppUpdateModule.java from
public void checkUpdate()
to
public void checkUpdate(int staleDays)
and replace STALE_DAYS with staleDays in the above function.
And in React Native
componentDidMount () {
InAppUpdate.checkUpdate(5) // pass staleDays here Ex: 5
}
Good luck