├── .github └── workflows │ ├── push.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── index.d.ts ├── index.js ├── package.json ├── src ├── providers │ ├── android.js │ └── ios.js ├── utils.js └── versions.js ├── tests ├── index.test.js └── version.test.js └── yarn.lock /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | concurrency: 3 | group: push 4 | on: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | issues: write 14 | pull-requests: write 15 | id-token: write 16 | steps: 17 | - name: Check out repository code 18 | uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | 22 | - name: Setup node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: yarn 27 | 28 | - name: Install dependencies 29 | run: yarn --frozen-lockfile 30 | 31 | - name: Test 32 | run: yarn test 33 | 34 | - name: Install latest npm 35 | run: npm install -g npm@latest 36 | 37 | - name: Release 38 | env: 39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 41 | NPM_CONFIG_PROVENANCE: true 42 | run: yarn release 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [ push ] 3 | jobs: 4 | Jest: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out code 8 | uses: actions/checkout@v4 9 | - name: Setup node 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - name: Install dependencies 14 | run: yarn 15 | - name: Run tests 16 | run: yarn test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | backend 3 | tests 4 | babel.config.js 5 | .github 6 | .idea 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Flexible Agency 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Check Version 2 | 3 | [![NPM](https://img.shields.io/npm/v/react-native-check-version.svg?style=flat)](https://npmjs.com/package/react-native-check-version) 4 | [![GitHub license](https://img.shields.io/github/license/includable/react-native-check-version.svg)](https://github.com/includable/react-native-check-version/blob/master/LICENSE) 5 | 6 | --- 7 | 8 | An easy way to check if there's an update available for the current app in the App Store or Google Play. 9 | 10 | Note that you need [react-native-device-info](https://github.com/rebeccahughes/react-native-device-info) to be 11 | installed for this library to function as expected, or you need to manually supply the `bundleId` and 12 | `currentVersion` values in the options object. 13 | 14 | ## Installation 15 | 16 | ``` 17 | yarn add react-native-check-version react-native-device-info 18 | ``` 19 | 20 | ## Basic usage 21 | 22 | Use the `checkVersion` method to get information: 23 | 24 | ```js 25 | import { checkVersion } from "react-native-check-version"; 26 | 27 | const version = await checkVersion(); 28 | console.log("Got version info:", version); 29 | 30 | if (version.needsUpdate) { 31 | console.log(`App has a ${version.updateType} update pending.`); 32 | } 33 | ``` 34 | 35 | ## API 36 | 37 | ### Function usage 38 | 39 | `checkVersion()` accepts an _optional_ options object, which may contain the following keys: 40 | 41 | - string `platform`: platform to check for, defaults to React Native's `Platform.OS` 42 | - string `country`: App Store specific country, defaults to `us` 43 | - string `bundleId`: bundle identifier to check, defaults to the value retrieved using react-native-device-info 44 | - string `currentVersion`: version to check against, defaults to the currently installed version 45 | 46 | ### Response object 47 | 48 | `checkVersion()` returns a Promise, which when resolved will return an object with the following properties: 49 | 50 | - string `version`: latest version number of the app 51 | - string `released`: (iOS only) ISO 8601 release date of that version 52 | - string `url`: download URL for the latest version 53 | - string `notes`: release notes of latest version 54 | - boolean `needsUpdate`: whether the latest version number is higher than the currently installed one 55 | - string `updateType`: `major`, `minor` or `patch`, based on how big the difference is between the currently installed 56 | version and the available version 57 | 58 | ## Changelog 59 | 60 | - `v1.1.0`: Use built-in `fetch` rather than Axios library. 61 | - `v1.0.18`: Update headers for Google Play HTTP request. 62 | - `v1.0.14`: Updated Android logic to use new Google Play endpoints. 63 | - `v1.0.13`: Added a try-catch within the main `checkVersion` function to prevent error responses from the HTTP requests 64 | to throw errors. 65 | - `v1.0.12`: Replaced the [custom backend](https://github.com/flexible-agency/react-native-check-version/issues/30) used 66 | previously by doing calls directly within the app. Please note Google Play has updated their web pages, making older 67 | versions not functional. 68 | 69 | ## Authors 70 | 71 | This library is developed by Includable, a creative app development agency. 72 | 73 | - Thomas Schoffelen 74 | 75 |

76 | 77 | --- 78 | 79 |
80 | 81 | Get professional support for this package → 82 | 83 |
84 | 85 | Custom consulting sessions available for implementation support or feature development. 86 | 87 |
88 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { PlatformOSType } from "react-native"; 2 | 3 | export type CheckVersionUpdateType = "major" | "minor" | "patch"; 4 | 5 | export interface CheckVersionOptions { 6 | endpoint?: string; 7 | platform?: PlatformOSType; 8 | bundleId?: string; 9 | currentVersion?: string; 10 | country?: string; 11 | } 12 | 13 | export interface CheckVersionResponse { 14 | version: string; 15 | released?: Date; // iOS only 16 | url: string; 17 | notes?: string; // iOS only 18 | needsUpdate: boolean; 19 | updateType?: CheckVersionUpdateType; 20 | platform?: PlatformOSType; 21 | bundleId?: string; 22 | lastChecked?: string; 23 | country?: string; 24 | error?: Error; 25 | } 26 | 27 | declare const checkVersion: ( 28 | options?: CheckVersionOptions 29 | ) => Promise; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { Platform, NativeModules } from "react-native"; 2 | 3 | import { lookupVersion } from "./src/utils"; 4 | import { versionCompare } from "./src/versions"; 5 | 6 | const DEFAULT_COUNTRY = "us"; 7 | 8 | export const checkVersion = async(options = {}) => { 9 | // Get options object 10 | const platform = options.platform || Platform.OS; 11 | const country = options.country || DEFAULT_COUNTRY; 12 | const bundleId = options.bundleId || (NativeModules.RNDeviceInfo 13 | ? NativeModules.RNDeviceInfo.bundleId 14 | : null); 15 | const currentVersion = options.currentVersion || (NativeModules.RNDeviceInfo 16 | ? NativeModules.RNDeviceInfo.appVersion 17 | : ""); 18 | 19 | // Check if we have retrieved a bundle ID 20 | if (!bundleId && !("RNDeviceInfo" in NativeModules)) { 21 | throw Error( 22 | "[react-native-check-version] Missing react-native-device-info dependency, " + 23 | "please manually specify a bundleId in the options object." 24 | ); 25 | } 26 | 27 | try { 28 | const data = await lookupVersion(platform, bundleId, country); 29 | const version = versionCompare(currentVersion, data.version); 30 | return { platform, bundleId, ...data, ...version }; 31 | } catch (e) { 32 | // On error - return default object 33 | return { 34 | platform, 35 | bundleId, 36 | version: null, 37 | needsUpdate: false, 38 | notes: "", 39 | url: null, 40 | lastChecked: (new Date()).toISOString(), 41 | country, 42 | error: e 43 | }; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-check-version", 3 | "version": "1.1.1", 4 | "description": "Check if the user is running the latest version of the app", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "test": "jest", 9 | "lint": "prettier --write .", 10 | "release": "semantic-release" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.21.3", 14 | "babel-jest": "^29.5.0", 15 | "jest": "^29.5.0", 16 | "jest-environment-node": "^29.5.0", 17 | "metro-react-native-babel-preset": "^0.77.0", 18 | "node-fetch": "^2.7.0", 19 | "prettier": "^2.8.8", 20 | "react-native": "^0.70.0", 21 | "semantic-release": "^24.1.1" 22 | }, 23 | "peerDependencies": { 24 | "react-native": ">=0.40.0", 25 | "react-native-device-info": ">=0.20.0" 26 | }, 27 | "jest": { 28 | "preset": "react-native", 29 | "testEnvironment": "node" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/tschoffelen/react-native-check-version.git" 34 | }, 35 | "keywords": [ 36 | "react", 37 | "native", 38 | "version", 39 | "checker", 40 | "check", 41 | "react-native", 42 | "update", 43 | "ios", 44 | "android" 45 | ], 46 | "author": "Thomas Schoffelen ", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/tschoffelen/react-native-check-version/issues" 50 | }, 51 | "homepage": "https://github.com/tschoffelen/react-native-check-version#readme" 52 | } 53 | -------------------------------------------------------------------------------- /src/providers/android.js: -------------------------------------------------------------------------------- 1 | export const getAndroidVersion = async (bundleId, country) => { 2 | const url = `https://play.google.com/store/apps/details?id=${bundleId}&hl=${country}`; 3 | let res; 4 | try { 5 | res = await fetch(url, { 6 | headers: { 7 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36", 8 | 'sec-fetch-site': 'same-origin' 9 | } 10 | }); 11 | } catch (e) { 12 | throw e; 13 | } 14 | 15 | if (!res.ok) { 16 | 17 | if (res.status === 404) { 18 | throw new Error( 19 | `App with bundle ID "${bundleId}" not found in Google Play.` 20 | ); 21 | } 22 | throw res.statusText 23 | } 24 | 25 | const text = await res.text(); 26 | const version = text.match(/\[\[\[['"]((\d+\.)+\d+)['"]\]\],/)[1]; 27 | const notes = text.match(/
(.*?)<\/div>/)?.[1]; 28 | const updateAt = text.match(/
(.*?)<\/div>/)?.[1]; 29 | 30 | return { 31 | version: version || null, 32 | releasedAt: (new Date()).toISOString(), 33 | updateAt: updateAt || "", 34 | notes: notes || "", 35 | url: `https://play.google.com/store/apps/details?id=${bundleId}&hl=${country}`, 36 | lastChecked: (new Date()).toISOString() 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/providers/ios.js: -------------------------------------------------------------------------------- 1 | export const getIosVersion = async (bundleId, country) => { 2 | // Adds a random number to the end of the URL to prevent caching 3 | const url = `https://itunes.apple.com/lookup?bundleId=${bundleId}&country=${country}&_=${new Date().valueOf()}`; 4 | 5 | let res = await fetch(url); 6 | 7 | const data = await res.json(); 8 | 9 | if (!data || !("results" in data)) { 10 | throw new Error("Unknown error connecting to iTunes."); 11 | } 12 | if (!data.results.length) { 13 | throw new Error("App for this bundle ID not found."); 14 | } 15 | 16 | res = data.results[0]; 17 | 18 | return { 19 | version: res.version || null, 20 | released: res.currentVersionReleaseDate || res.releaseDate || null, 21 | notes: res.releaseNotes || "", 22 | url: res.trackViewUrl || res.artistViewUrl || res.sellerUrl || null, 23 | country, 24 | lastChecked: (new Date()).toISOString() 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { getIosVersion } from "./providers/ios"; 2 | import { getAndroidVersion } from "./providers/android"; 3 | 4 | export const lookupVersion = async(platform, bundleId, country = "us") => { 5 | switch (platform) { 6 | case "ios": 7 | return getIosVersion(bundleId, country); 8 | case "android": 9 | return getAndroidVersion(bundleId, country); 10 | default: 11 | throw new Error("Unsupported platform defined."); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/versions.js: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | 3 | export const parseVersion = (version) => { 4 | return semver.parse(semver.coerce(version), true); 5 | }; 6 | 7 | export const diffLoose = (version1, version2) => { 8 | if (version1 === version2) { 9 | return null; 10 | } 11 | 12 | const v1 = parseVersion(version1); 13 | const v2 = parseVersion(version2); 14 | if (semver.lt(v2, v1) || semver.eq(v1, v2, true)) { 15 | return null; 16 | } 17 | 18 | let prefix = ""; 19 | let defaultResult = null; 20 | if (v1.prerelease.length || v2.prerelease.length) { 21 | prefix = "pre"; 22 | defaultResult = "prerelease"; 23 | } 24 | for (let key in v1) { 25 | if (v1.hasOwnProperty(key) && ["major", "minor", "patch"].includes(key) && v1[key] !== v2[key]) { 26 | return prefix + key; 27 | } 28 | } 29 | return defaultResult; 30 | }; 31 | 32 | export const versionCompare = (currentVersion, latestVersion) => { 33 | if (!latestVersion) { 34 | return { 35 | needsUpdate: false, 36 | updateType: null, 37 | notice: "Error: could not get latest version" 38 | }; 39 | } 40 | 41 | try { 42 | const updateType = diffLoose(currentVersion, latestVersion); 43 | return { 44 | needsUpdate: !!updateType, 45 | updateType 46 | }; 47 | } catch (e) { 48 | let needsUpdate = currentVersion !== latestVersion && (latestVersion > currentVersion); 49 | if (!latestVersion.includes(".") || latestVersion.split(".").length < 3) { 50 | // Not a valid semver, so don't ever ask to update 51 | needsUpdate = false; 52 | } 53 | const updateType = needsUpdate ? "minor" : null; 54 | return { 55 | needsUpdate, 56 | updateType, 57 | notice: e.message.replace(/^Invalid Version:/, "Not a valid semver version:") 58 | }; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import { checkVersion } from "../index"; 2 | 3 | global.fetch = jest.requireActual('node-fetch') 4 | 5 | describe("checkVersion", () => { 6 | test("can get version for valid bundle ID for Android", async () => { 7 | const data = await checkVersion({ 8 | platform: "android", 9 | bundleId: "com.streetartcities.map", 10 | currentVersion: "1.0.0" 11 | }); 12 | 13 | expect(data.platform).toEqual("android"); 14 | expect(data.bundleId).toEqual("com.streetartcities.map"); 15 | expect(data.needsUpdate).toEqual(true); 16 | }); 17 | 18 | test("can get version for unknown bundle ID for Android with error property", async () => { 19 | const data = await checkVersion({ 20 | platform: "android", 21 | bundleId: "com.notarealapp.unknown", 22 | currentVersion: "1.0.0" 23 | }); 24 | 25 | expect(data.platform).toEqual("android"); 26 | expect(data.bundleId).toEqual("com.notarealapp.unknown"); 27 | expect(data.error.toString()).toEqual('Error: App with bundle ID "com.notarealapp.unknown" not found in Google Play.'); 28 | }); 29 | 30 | 31 | test("can get version for valid bundle ID for iOS", async () => { 32 | const data = await checkVersion({ 33 | platform: "ios", 34 | bundleId: "nl.hoyapp.mobile", 35 | currentVersion: "1.0.0" 36 | }); 37 | 38 | expect(data.platform).toEqual("ios"); 39 | expect(data.bundleId).toEqual("nl.hoyapp.mobile"); 40 | expect(data.needsUpdate).toEqual(true); 41 | }); 42 | 43 | test("can get version for unknown bundle ID for iOS with error property", async () => { 44 | const data = await checkVersion({ 45 | platform: "ios", 46 | bundleId: "com.notarealapp.unknown", 47 | currentVersion: "1.0.0" 48 | }); 49 | 50 | expect(data.platform).toEqual("ios"); 51 | expect(data.bundleId).toEqual("com.notarealapp.unknown"); 52 | expect(data.error.toString()).toEqual("Error: App for this bundle ID not found."); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/version.test.js: -------------------------------------------------------------------------------- 1 | import { versionCompare } from "../src/versions"; 2 | 3 | describe("Version compare", () => { 4 | it("deals with real semvers", () => { 5 | expect(versionCompare("1.1.2", "1.1.3")).toEqual({ 6 | needsUpdate: true, 7 | updateType: "patch" 8 | }); 9 | expect(versionCompare("1.1.2", "1.1.2")).toEqual({ 10 | needsUpdate: false, 11 | updateType: null 12 | }); 13 | expect(versionCompare("1.2.0", "1.1.3")).toEqual({ 14 | needsUpdate: false, 15 | updateType: null 16 | }); 17 | expect(versionCompare("1.1.1", "1.2.0")).toEqual({ 18 | needsUpdate: true, 19 | updateType: "minor" 20 | }); 21 | expect(versionCompare("0.9.0", "1.2.0")).toEqual({ 22 | needsUpdate: true, 23 | updateType: "major" 24 | }); 25 | }); 26 | 27 | it("deals with simple decimal versions", () => { 28 | expect(versionCompare("1.1", "1.2")).toEqual({ 29 | needsUpdate: true, 30 | updateType: "minor" 31 | }); 32 | expect(versionCompare("1.1", "1.1")).toEqual({ 33 | needsUpdate: false, 34 | updateType: null 35 | }); 36 | expect(versionCompare("1.2", "1.1")).toEqual({ 37 | needsUpdate: false, 38 | updateType: null 39 | }); 40 | expect(versionCompare("0.9", "1.2")).toEqual({ 41 | needsUpdate: true, 42 | updateType: "major" 43 | }); 44 | }); 45 | 46 | it("deals with single-number versions", () => { 47 | expect(versionCompare("11", "12")).toEqual({ 48 | needsUpdate: true, 49 | updateType: "major" 50 | }); 51 | expect(versionCompare("11", "11")).toEqual({ 52 | needsUpdate: false, 53 | updateType: null 54 | }); 55 | expect(versionCompare("12", "11")).toEqual({ 56 | needsUpdate: false, 57 | updateType: null 58 | }); 59 | expect(versionCompare("9", "12")).toEqual({ 60 | needsUpdate: true, 61 | updateType: "major" 62 | }); 63 | expect(versionCompare("944", "1201")).toEqual({ 64 | needsUpdate: true, 65 | updateType: "major" 66 | }); 67 | }); 68 | }); 69 | --------------------------------------------------------------------------------