Keeping Users Up-to-Date: A Guide to Mobile App Version Control
Tags: flutter, typescript, mobile
Published: 2024-04-18 21:27:08

In the fast-paced world of mobile apps, keeping users on the same page can be tricky. App store review times can create delays, meaning some users might have the latest update while others are still waiting. This can lead to a fragmented user experience. To address this, we use a clever system called version control. Let's explore how it works!


**Why App Store Review Times Matter**

App stores like Apple App Store and Google Play have review processes that can take time. This creates a window where users on different versions experience the app differently. Some users might have access to the latest update while others might still be on an older version waiting for the review to complete.

**Why Users Might Have Higher Versions Than Expected**

There are a couple of reasons why a user might have a version number seemingly higher than the one stored in our database. For instance, a user may have already downloaded the most recent app from the store. We update our version after a certain period to avoid confusion. If we update immediately after submitting the app to the stores, some users may see a force/soft update dialog unnecessarily.

**How Our Version Control System Works**
Our version control system has three key parts:

- _Database Version Tracking:_ We maintain a central database to track all app versions and relevant information.
- _Hardcoded App Version:_ We store the current app version directly within the app's code. While some methods allow retrieving the version dynamically, we prefer the simplicity and control of hardcoding.
- _Splash Screen API Call:_ Upon launching the app, the splash screen initiates an API call to our backend with the current version number attached.For example, the API call might look like this:
`http://localhost:3031/api/version/current-app-version/a.b.c.d
`(Replace a.b.c.d with the actual app version)

**What the Backend Responds With**
The backend processes the API request and sends back a response containing crucial update information:
`{
  "data": {
    "latestVersion": "a.b.c.e",
    "isForce": true,
    "isSoft": false
  },
  "message": null,
  "stack": null
}
`

The response includes the latest app version (latestVersion) and flags indicating whether the update is mandatory (isForce) or optional (isSoft).
Upon receiving the backend response, our Flutter app processes the update flags (isForce and isSoft) to provide the user with appropriate prompts:

- Force Update Prompt: If isForce is true, the app displays a dialog urging the user to update immediately. This dialog typically includes a prominent call-to-action button redirecting the user to the app store for the update.

- Soft Update Prompt: If isForce is false and isSoft is true, the app presents a dialog offering the user the option to update the app from the store. Additionally, a "Skip" button allows users to continue using the app without updating.

If both are false, the user will proceed to the app without any interruption. 

**Case study: User Versions and Actions:**
User A (v2.0.1.16): Proceeds to the app, as their version exceeds the latest database version (v2.0.1.15).
User B (v2.0.1.15): Proceeds to the app, as their version matches the database's latest version (v2.0.1.15).
User C (v2.0.1.14): Receives a soft update warning, as their version is newer than the last force update version (v2.0.1.12).
User D (v2.0.1.12): Receives a soft update warning, as their version matches the last force update version (v2.0.1.12).
User E (v2.0.1.11): Receives a force update warning, as their version is significantly older than the last force update version (v2.0.1.12).
User F (v2.0.1.9): Receives a force update warning, as their version is significantly older than the last force update version (v2.0.1.12).

This example showcases how our version control system tailors update prompts based on individual user versions.

**Bonus (code from the backend)**

```
const currentAppVersion = catchAsyncErr(async (req: Request, res: Response) => {

    const userVersion = req.params.versionNumber;

    try {
        const latestVersionDoc = await AppVersion.findOne().sort({ createdAt: -1 });


        if (!latestVersionDoc) {
            return apiResponse(res, httpStatus.NOT_FOUND, {
                message: "Latest version not found",
            });
        }

        const latestVersion = latestVersionDoc.version;

        let responsePayload;
        if (userVersion >= latestVersion) {
            responsePayload = { latestVersion: latestVersion, isForce: false, isSoft: false };
            return apiResponse(res, httpStatus.OK, { data: responsePayload });
        }

        const lastForceVersion = await AppVersion.findOne({ isForce: true })
            .sort({ createdAt: -1 })
            .exec();

        if (userVersion < (lastForceVersion?.version || "0.0.0.0")) {
            responsePayload = { latestVersion: latestVersion, isForce: true, isSoft: false };
            return apiResponse(res, httpStatus.OK, { data: responsePayload });

        }

        return apiResponse(res, httpStatus.OK, { data: { latestVersion: latestVersion, isForce: false, isSoft: true } });
    } catch (error) {
        console.error("Error retrieving latest version", error);
        return apiResponse(res, httpStatus.INTERNAL_SERVER_ERROR, {
            message: "Internal Server Error",
        });
    }
});
```

**Conclusion**
A robust version control system is the backbone of our app's update strategy. By keeping track of app versions, implementing well-defined update prompts, and ensuring a smooth user experience, we can effectively deliver the latest features, bug fixes, and security improvements to all users. This fosters a more engaged and satisfied user base.