capacitor-3rddigital-appupdate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # capacitor-3rddigital-appupdate
2
+
3
+ A Capacitor + React library for **seamless Over-The-Air (OTA) updates** with:
4
+
5
+ - 🔄 Automatic version checks
6
+ - 📥 Bundle download & installation (iOS & Android)
7
+ - ⚡ Configurable user prompts (dialogs)
8
+ - 🛠️ CLI tool for building & uploading bundles to your update server
9
+
10
+ ## 🚀 Installation
11
+
12
+ ```sh
13
+ npm install capacitor-3rddigital-appupdate
14
+ # or
15
+ yarn add capacitor-3rddigital-appupdate
16
+ ```
17
+
18
+ This package has peer dependencies that also need to be installed:
19
+
20
+ ```sh
21
+ npm install @capacitor/core @capacitor/device @capgo/capacitor-updater
22
+ ```
23
+
24
+ Run pod install for iOS:
25
+
26
+ ```sh
27
+ cd ios && pod install
28
+ ```
29
+
30
+ ## 📦 Usage in Your App
31
+
32
+ - Use the useCapacitorUpdater hook to check for updates and handle modal UI:
33
+
34
+ ```sh
35
+ import React from "react";
36
+ import { UpdaterModal, useCapacitorUpdater } from "capacitor-3rddigital-appupdate";
37
+
38
+ const App = () => {
39
+ const { isUpdateModalVisible, updateInfo, handleUpdate, setUpdateModalVisible } = useCapacitorUpdater({
40
+ iosPackage: "com.example.ios",
41
+ androidPackage: "com.example.android",
42
+ key: "example-key",
43
+ });
44
+
45
+ return (
46
+ <div>
47
+ {/* Your app content */}
48
+ {isUpdateModalVisible && updateInfo && (
49
+ <UpdaterModal
50
+ visible={isUpdateModalVisible}
51
+ updateInfo={updateInfo}
52
+ onConfirm={() => handleUpdate()}
53
+ onCancel={() => setUpdateModalVisible(false)}
54
+ />
55
+ )}
56
+ </div>
57
+ );
58
+ };
59
+
60
+ export default App;
61
+ ```
62
+
63
+ ## ⚙️ API Reference
64
+
65
+ 🔹 useCapacitorUpdater(options?: { iosPackage?: string; androidPackage?: string })
66
+
67
+ - Checks the server for available updates and manages the modal prompt.
68
+
69
+ Returns:
70
+
71
+ | Key | Type | Description |
72
+ | ----------------------- | ---------------- | ----------------------------------- |
73
+ | `updateInfo` | `UpdateInfo` | Metadata about the available update |
74
+ | `isUpdateModalVisible` | `boolean` | Whether the update modal is visible |
75
+ | `setUpdateModalVisible` | `(bool) => void` | Show/hide modal manually |
76
+ | `handleUpdate` | `() => void` | Downloads and installs the update |
77
+
78
+ 🔹 UpdaterModal
79
+
80
+ - Global modal component for prompting users to update.
81
+
82
+ Props:
83
+
84
+ | Key | Type | Default | Description |
85
+ | ------------- | ---------- | -------------------- | ----------------------------------- |
86
+ | `visible` | boolean | ❌ | Show/hide modal |
87
+ | `updateInfo` | UpdateInfo | ❌ | Update metadata |
88
+ | `onConfirm` | function | ❌ | Callback when user confirms update |
89
+ | `onCancel` | function | ❌ | Callback when user cancels update |
90
+ | `customUI` | function | ❌ | Custom render for the modal UI |
91
+ | `title` | string | `"Update Available"` | Modal title |
92
+ | `message` | string | `undefined` | Modal message |
93
+ | `confirmText` | string | `"Update"` | Confirm button text |
94
+ | `cancelText` | string | `"Cancel"` | Cancel button text |
95
+ | `styles` | object | `{}` | Style overrides for modal & buttons |
96
+
97
+ ## 🖥️ CLI Tool – appupdate
98
+
99
+ - This package provides a CLI for building & uploading OTA bundles.
100
+
101
+ Build & Upload
102
+
103
+ ```sh
104
+ npx appupdate android
105
+ npx appupdate ios
106
+ npx appupdate all
107
+ ```
108
+
109
+ You will be prompted for:
110
+
111
+ - API Token
112
+ - Project ID
113
+ - Environment (development / production)
114
+ - Version
115
+ - Build Number
116
+ - Force Update (true/false)
117
+
118
+ What it does
119
+
120
+ - Builds your React web app
121
+ - Creates Capgo zip bundle
122
+ - Uploads bundle + metadata to your update server (https://dev.3rddigital.com/appupdate-api/api/)
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { UpdaterModalProps } from "./types.js";
3
+ export declare const UpdaterModal: React.FC<UpdaterModalProps>;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const UpdaterModal = ({ visible, updateInfo, onConfirm, onCancel, customUI, title = "Update Available", message, confirmText = "Update", cancelText = "Cancel", styles = {}, }) => {
3
+ if (!visible || !updateInfo)
4
+ return null;
5
+ if (customUI)
6
+ return customUI(updateInfo, onConfirm, onCancel);
7
+ const { overlay = {}, container = {}, title: titleStyle = {}, message: messageStyle = {}, buttonRow = {}, confirmButton = {}, cancelButton = {}, } = styles;
8
+ return (_jsx("div", { style: {
9
+ position: "fixed",
10
+ inset: 0,
11
+ background: "rgba(0,0,0,0.5)",
12
+ display: "flex",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ zIndex: 9999,
16
+ ...overlay,
17
+ }, children: _jsxs("div", { style: {
18
+ background: "#fff",
19
+ borderRadius: 8,
20
+ padding: 20,
21
+ width: 320,
22
+ textAlign: "center",
23
+ ...container,
24
+ }, children: [_jsx("h3", { style: { margin: "0 0 10px", ...titleStyle }, children: title }), _jsx("p", { style: { marginBottom: 20, ...messageStyle }, children: message ||
25
+ `A new version (${updateInfo.availableVersion}) is available.` }), _jsxs("div", { style: {
26
+ marginTop: 16,
27
+ display: "flex",
28
+ gap: 10,
29
+ justifyContent: "center",
30
+ ...buttonRow,
31
+ }, children: [_jsx("button", { onClick: onConfirm, style: {
32
+ background: "#007bff",
33
+ color: "#fff",
34
+ border: "none",
35
+ padding: "8px 16px",
36
+ borderRadius: 4,
37
+ cursor: "pointer",
38
+ ...confirmButton,
39
+ }, children: confirmText }), _jsx("button", { onClick: onCancel, style: {
40
+ background: "#ccc",
41
+ border: "none",
42
+ padding: "8px 16px",
43
+ borderRadius: 4,
44
+ cursor: "pointer",
45
+ ...cancelButton,
46
+ }, children: cancelText })] })] }) }));
47
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./UpdaterModal.js";
3
+ export * from "./useCapacitorUpdater.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./UpdaterModal.js";
3
+ export * from "./useCapacitorUpdater.js";
@@ -0,0 +1,26 @@
1
+ export interface UpdateInfo {
2
+ availableVersion: number;
3
+ url: string;
4
+ forceUpdate: boolean;
5
+ bundleId: string;
6
+ }
7
+ export interface UpdaterModalProps {
8
+ visible: boolean;
9
+ updateInfo: UpdateInfo | null;
10
+ onConfirm: () => void;
11
+ onCancel: () => void;
12
+ customUI?: (info: UpdateInfo, onConfirm: () => void, onCancel: () => void) => React.ReactNode;
13
+ title?: string;
14
+ message?: string;
15
+ confirmText?: string;
16
+ cancelText?: string;
17
+ styles?: {
18
+ overlay?: React.CSSProperties;
19
+ container?: React.CSSProperties;
20
+ title?: React.CSSProperties;
21
+ message?: React.CSSProperties;
22
+ buttonRow?: React.CSSProperties;
23
+ confirmButton?: React.CSSProperties;
24
+ cancelButton?: React.CSSProperties;
25
+ };
26
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { UpdateInfo } from "./types.js";
2
+ export declare function useCapacitorUpdater(options?: {
3
+ iosPackage?: string;
4
+ androidPackage?: string;
5
+ apiKey?: string;
6
+ }): {
7
+ updateInfo: UpdateInfo | null;
8
+ isUpdateModalVisible: boolean;
9
+ setUpdateModalVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
10
+ handleUpdate: (info?: UpdateInfo | null) => Promise<void>;
11
+ };
@@ -0,0 +1,98 @@
1
+ import { Capacitor, CapacitorHttp } from "@capacitor/core";
2
+ import { Device } from "@capacitor/device";
3
+ import { CapacitorUpdater } from "@capgo/capacitor-updater";
4
+ import { message } from "antd";
5
+ import { useEffect, useState } from "react";
6
+ const API_BASE_URL = "https://dev.3rddigital.com/appupdate-api/api";
7
+ export function useCapacitorUpdater(options) {
8
+ const [isUpdateModalVisible, setUpdateModalVisible] = useState(false);
9
+ const [updateInfo, setUpdateInfo] = useState(null);
10
+ useEffect(() => {
11
+ (async () => {
12
+ if (!Capacitor.isNativePlatform())
13
+ return;
14
+ try {
15
+ await CapacitorUpdater.notifyAppReady();
16
+ const response = await CapacitorHttp.get({
17
+ url: `${API_BASE_URL}/projects/get-bundle`,
18
+ params: {
19
+ key: options?.apiKey ?? "",
20
+ iosPackage: options?.iosPackage ?? "",
21
+ androidPackage: options?.androidPackage ?? "",
22
+ },
23
+ });
24
+ const platformData = Capacitor.getPlatform() === "android"
25
+ ? response.data.android
26
+ : response.data.ios;
27
+ const { version: availableVersion = 0, url, forceUpdate = false, bundleId, } = platformData;
28
+ const currentBundle = await CapacitorUpdater.current();
29
+ const currentVersion = currentBundle.bundle.version === "builtin"
30
+ ? 0
31
+ : Number(currentBundle.bundle.version);
32
+ if (availableVersion > currentVersion) {
33
+ const info = {
34
+ availableVersion,
35
+ url,
36
+ forceUpdate,
37
+ bundleId,
38
+ };
39
+ setUpdateInfo(info);
40
+ setUpdateModalVisible(true);
41
+ if (forceUpdate)
42
+ await handleUpdate(info);
43
+ }
44
+ }
45
+ catch (err) {
46
+ console.warn("[CapacitorUpdater] Failed to fetch update:", err);
47
+ message.error("Failed to check for updates.");
48
+ }
49
+ })();
50
+ }, [options?.apiKey, options?.iosPackage, options?.androidPackage]);
51
+ const handleUpdate = async (info = updateInfo) => {
52
+ if (!info)
53
+ return;
54
+ setUpdateModalVisible(false);
55
+ const hideLoader = message.loading("Downloading update...", 0);
56
+ try {
57
+ const data = await CapacitorUpdater.download({
58
+ version: info.availableVersion.toString(),
59
+ url: info.url,
60
+ });
61
+ await CapacitorUpdater.set(data);
62
+ await CapacitorHttp.post({
63
+ url: `${API_BASE_URL}/bundles/${info.bundleId}/count`,
64
+ headers: { "Content-Type": "application/json" },
65
+ data: { status: "success" },
66
+ });
67
+ hideLoader();
68
+ message.success("Update installed successfully!");
69
+ console.log("[CapacitorUpdater] Update installed");
70
+ }
71
+ catch (err) {
72
+ const deviceinfo = await Device.getInfo();
73
+ await CapacitorHttp.post({
74
+ url: `${API_BASE_URL}/bundles/${info.bundleId}/count`,
75
+ headers: { "Content-Type": "application/json" },
76
+ data: {
77
+ status: "failure",
78
+ error: err?.message ?? "Failed to install update.",
79
+ deviceInfo: {
80
+ model: deviceinfo.model,
81
+ brand: deviceinfo.manufacturer,
82
+ systemName: deviceinfo.operatingSystem,
83
+ systemVersion: deviceinfo.osVersion,
84
+ },
85
+ },
86
+ });
87
+ hideLoader();
88
+ message.error("Failed to install update.");
89
+ console.error("[CapacitorUpdater] Failed to install update:", err);
90
+ }
91
+ };
92
+ return {
93
+ updateInfo,
94
+ isUpdateModalVisible,
95
+ setUpdateModalVisible,
96
+ handleUpdate,
97
+ };
98
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "capacitor-3rddigital-appupdate",
3
+ "version": "1.0.0",
4
+ "description": "Auto OTA update handler for Capacitor + React apps",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "clean": "rm -rf dist",
10
+ "prepublishOnly": "npm run clean && npm run build",
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/latest3rddigital/capacitor-3rddigital-appupdate.git"
16
+ },
17
+ "keywords": [
18
+ "capacitor",
19
+ "react",
20
+ "ota",
21
+ "update",
22
+ "hot-reload"
23
+ ],
24
+ "author": "Sagar Bhavsar",
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "bugs": {
28
+ "url": "https://github.com/latest3rddigital/capacitor-3rddigital-appupdate/issues"
29
+ },
30
+ "homepage": "https://github.com/latest3rddigital/capacitor-3rddigital-appupdate#readme",
31
+ "dependencies": {
32
+ "@capacitor/core": "^7.4.3",
33
+ "@capacitor/device": "^7.0.2",
34
+ "@capgo/capacitor-updater": "^7.18.2",
35
+ "@inquirer/prompts": "^7.8.6",
36
+ "antd": "^5.27.4",
37
+ "axios": "^1.12.2",
38
+ "form-data": "^4.0.4"
39
+ },
40
+ "peerDependencies": {
41
+ "react": "^18 || ^19",
42
+ "react-dom": "^18 || ^19"
43
+ },
44
+ "devDependencies": {
45
+ "@types/react": "^19.2.2",
46
+ "@types/react-dom": "^19.2.1",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "bin": {
50
+ "appupdate": "./scripts/bundle.cjs"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "src"
55
+ ]
56
+ }
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ const { execSync } = require('child_process');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const axios = require('axios');
6
+ const FormData = require('form-data');
7
+ const { input, select, confirm } = require('@inquirer/prompts');
8
+
9
+ const API_BASE_URL = 'https://dev.3rddigital.com/appupdate-api/api/';
10
+
11
+ function run(command) {
12
+ try {
13
+ console.log(`\n➡️ Running: ${command}\n`);
14
+ execSync(command, { stdio: 'inherit' });
15
+ } catch (err) {
16
+ console.error(`❌ Command failed: ${command}`);
17
+ console.error(err.message);
18
+ process.exit(1);
19
+ }
20
+ }
21
+
22
+ async function uploadBundle({ filePath, platform, config }) {
23
+ console.log(`📤 Uploading ${platform} bundle to server...`);
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ console.error(`❌ File not found: ${filePath}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const fileStream = fs.createReadStream(filePath);
31
+ const form = new FormData();
32
+ form.append('bundle', fileStream);
33
+ form.append('projectId', config.PROJECT_ID);
34
+ form.append('environment', config.ENVIRONMENT);
35
+ form.append('platform', platform);
36
+ form.append('version', config.VERSION);
37
+ form.append('buildNumber', String(config.BUILD_NUMBER));
38
+ form.append('forceUpdate', String(config.FORCE_UPDATE));
39
+
40
+ try {
41
+ const res = await axios.post(`${API_BASE_URL}/bundles`, form, {
42
+ headers: { ...form.getHeaders(), Authorization: `Bearer ${config.API_TOKEN}` },
43
+ });
44
+ console.log(`✅ ${platform} bundle uploaded! Response:`, JSON.stringify(res.data, null, 2));
45
+ } catch (err) {
46
+ console.error(`❌ ${platform} bundle upload failed!`);
47
+ if (err.response) {
48
+ console.error('Status:', err.response.status);
49
+ console.error('Data:', err.response.data);
50
+ } else {
51
+ console.error('Message:', err.message);
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ async function getCommonConfig() {
58
+ console.log(`\n⚙️ Enter common configuration for the app\n`);
59
+
60
+ const API_TOKEN = await input({
61
+ message: `Enter API Token:`,
62
+ validate: (val) => (val.trim() ? true : 'API Token required'),
63
+ });
64
+
65
+ const PROJECT_ID = await input({
66
+ message: `Enter Project ID:`,
67
+ validate: (val) => (val.trim() ? true : 'Project ID required'),
68
+ });
69
+
70
+ const ENVIRONMENT = await select({
71
+ message: `Select Environment:`,
72
+ choices: [
73
+ { name: 'development', value: 'development' },
74
+ { name: 'production', value: 'production' },
75
+ ],
76
+ });
77
+
78
+ return { API_TOKEN, PROJECT_ID, ENVIRONMENT };
79
+ }
80
+
81
+ async function getPlatformConfig(platform) {
82
+ console.log(`\n⚙️ Enter configuration for ${platform.toUpperCase()}\n`);
83
+
84
+ const VERSION = await input({
85
+ message: `(${platform}) Enter App Version (e.g. 1.0.0):`,
86
+ validate: (val) => (val.trim() ? true : 'Version required'),
87
+ });
88
+
89
+ const BUILD_NUMBER = await input({
90
+ message: `(${platform}) Enter Build Number:`,
91
+ validate: (val) => (!isNaN(val) && val.trim() !== '' ? true : 'Must be a number'),
92
+ });
93
+
94
+ const FORCE_UPDATE = await confirm({ message: `(${platform}) Force Update?`, default: false });
95
+
96
+ return { VERSION, BUILD_NUMBER, FORCE_UPDATE };
97
+ }
98
+
99
+ function getAppId() {
100
+ const configPath = path.join(process.cwd(), 'capacitor.config.ts');
101
+ if (!fs.existsSync(configPath)) {
102
+ console.error('❌ capacitor.config.ts not found!');
103
+ process.exit(1);
104
+ }
105
+ const content = fs.readFileSync(configPath, 'utf-8');
106
+ const match = content.match(/appId:\s*['"`](.*?)['"`]/);
107
+ if (!match) {
108
+ console.error('❌ Could not extract appId from capacitor.config.ts');
109
+ process.exit(1);
110
+ }
111
+ return match[1];
112
+ }
113
+
114
+ function getAppVersion() {
115
+ const pkgPath = path.join(process.cwd(), 'package.json');
116
+ if (!fs.existsSync(pkgPath)) {
117
+ console.error('❌ package.json not found!');
118
+ process.exit(1);
119
+ }
120
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
121
+ return pkg.version || '0.0.0';
122
+ }
123
+
124
+ function getLatestCapgoZip() {
125
+ const appId = getAppId();
126
+ const version = getAppVersion();
127
+ const expectedPrefix = `${appId}_${version}`;
128
+
129
+ const files = fs.readdirSync(process.cwd());
130
+ const zipFiles = files.filter((f) => f.endsWith('.zip') && f.startsWith(expectedPrefix));
131
+
132
+ if (!zipFiles.length) {
133
+ console.error(`❌ No Capgo bundle zip found matching: ${expectedPrefix}`);
134
+ process.exit(1);
135
+ }
136
+
137
+ zipFiles.sort((a, b) => fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime());
138
+ return path.join(process.cwd(), zipFiles[0]);
139
+ }
140
+
141
+ function buildBundle() {
142
+ console.log('📦 Building web app and Capgo bundle...');
143
+
144
+ // Build web app once
145
+ run('npm run build');
146
+
147
+ // Create Capgo zip
148
+ run('npx @capgo/cli@latest bundle zip');
149
+
150
+ // Detect generated zip
151
+ const outputPath = getLatestCapgoZip();
152
+ console.log(`✅ Bundle created at ${outputPath}`);
153
+ return outputPath;
154
+ }
155
+
156
+ (async () => {
157
+ try {
158
+ const platformArg = process.argv[2];
159
+ if (!platformArg) {
160
+ console.error('❌ Please specify a platform: android | ios | all');
161
+ process.exit(1);
162
+ }
163
+
164
+ // Get common config once
165
+ const commonConfig = await getCommonConfig();
166
+
167
+ // Build bundle once
168
+ const bundleFile = buildBundle();
169
+
170
+ // Android upload
171
+ if (platformArg === 'android' || platformArg === 'all') {
172
+ const androidConfig = await getPlatformConfig('android');
173
+ await uploadBundle({ filePath: bundleFile, platform: 'android', config: { ...commonConfig, ...androidConfig } });
174
+ }
175
+
176
+ // iOS upload
177
+ if (platformArg === 'ios' || platformArg === 'all') {
178
+ const iosConfig = await getPlatformConfig('ios');
179
+ await uploadBundle({ filePath: bundleFile, platform: 'ios', config: { ...commonConfig, ...iosConfig } });
180
+ }
181
+
182
+ console.log('\n🎉 All tasks completed successfully!');
183
+ } catch (err) {
184
+ console.error('❌ Fatal error:', err.message);
185
+ process.exit(1);
186
+ }
187
+ })();
@@ -0,0 +1,100 @@
1
+ import React from "react";
2
+ import type { UpdaterModalProps } from "./types.js";
3
+
4
+ export const UpdaterModal: React.FC<UpdaterModalProps> = ({
5
+ visible,
6
+ updateInfo,
7
+ onConfirm,
8
+ onCancel,
9
+ customUI,
10
+ title = "Update Available",
11
+ message,
12
+ confirmText = "Update",
13
+ cancelText = "Cancel",
14
+ styles = {},
15
+ }) => {
16
+ if (!visible || !updateInfo) return null;
17
+
18
+ if (customUI) return customUI(updateInfo, onConfirm, onCancel);
19
+
20
+ const {
21
+ overlay = {},
22
+ container = {},
23
+ title: titleStyle = {},
24
+ message: messageStyle = {},
25
+ buttonRow = {},
26
+ confirmButton = {},
27
+ cancelButton = {},
28
+ } = styles;
29
+
30
+ return (
31
+ <div
32
+ style={{
33
+ position: "fixed",
34
+ inset: 0,
35
+ background: "rgba(0,0,0,0.5)",
36
+ display: "flex",
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ zIndex: 9999,
40
+ ...overlay,
41
+ }}
42
+ >
43
+ <div
44
+ style={{
45
+ background: "#fff",
46
+ borderRadius: 8,
47
+ padding: 20,
48
+ width: 320,
49
+ textAlign: "center",
50
+ ...container,
51
+ }}
52
+ >
53
+ <h3 style={{ margin: "0 0 10px", ...titleStyle }}>{title}</h3>
54
+ <p style={{ marginBottom: 20, ...messageStyle }}>
55
+ {message ||
56
+ `A new version (${updateInfo.availableVersion}) is available.`}
57
+ </p>
58
+
59
+ <div
60
+ style={{
61
+ marginTop: 16,
62
+ display: "flex",
63
+ gap: 10,
64
+ justifyContent: "center",
65
+ ...buttonRow,
66
+ }}
67
+ >
68
+ <button
69
+ onClick={onConfirm}
70
+ style={{
71
+ background: "#007bff",
72
+ color: "#fff",
73
+ border: "none",
74
+ padding: "8px 16px",
75
+ borderRadius: 4,
76
+ cursor: "pointer",
77
+ ...confirmButton,
78
+ }}
79
+ >
80
+ {confirmText}
81
+ </button>
82
+
83
+ <button
84
+ onClick={onCancel}
85
+ style={{
86
+ background: "#ccc",
87
+ border: "none",
88
+ padding: "8px 16px",
89
+ borderRadius: 4,
90
+ cursor: "pointer",
91
+ ...cancelButton,
92
+ }}
93
+ >
94
+ {cancelText}
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ );
100
+ };
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./UpdaterModal.js";
3
+ export * from "./useCapacitorUpdater.js";
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface UpdateInfo {
2
+ availableVersion: number;
3
+ url: string;
4
+ forceUpdate: boolean;
5
+ bundleId: string;
6
+ }
7
+
8
+ export interface UpdaterModalProps {
9
+ visible: boolean;
10
+ updateInfo: UpdateInfo | null;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ customUI?: (
14
+ info: UpdateInfo,
15
+ onConfirm: () => void,
16
+ onCancel: () => void
17
+ ) => React.ReactNode;
18
+ title?: string;
19
+ message?: string;
20
+ confirmText?: string;
21
+ cancelText?: string;
22
+ styles?: {
23
+ overlay?: React.CSSProperties;
24
+ container?: React.CSSProperties;
25
+ title?: React.CSSProperties;
26
+ message?: React.CSSProperties;
27
+ buttonRow?: React.CSSProperties;
28
+ confirmButton?: React.CSSProperties;
29
+ cancelButton?: React.CSSProperties;
30
+ };
31
+ }
@@ -0,0 +1,123 @@
1
+ import { Capacitor, CapacitorHttp } from "@capacitor/core";
2
+ import { Device } from "@capacitor/device";
3
+ import { CapacitorUpdater } from "@capgo/capacitor-updater";
4
+ import { message } from "antd";
5
+ import { useEffect, useState } from "react";
6
+ import type { UpdateInfo } from "./types.js";
7
+
8
+ const API_BASE_URL = "https://dev.3rddigital.com/appupdate-api/api";
9
+
10
+ export function useCapacitorUpdater(options?: {
11
+ iosPackage?: string;
12
+ androidPackage?: string;
13
+ apiKey?: string;
14
+ }) {
15
+ const [isUpdateModalVisible, setUpdateModalVisible] = useState(false);
16
+ const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
17
+
18
+ useEffect(() => {
19
+ (async () => {
20
+ if (!Capacitor.isNativePlatform()) return;
21
+
22
+ try {
23
+ await CapacitorUpdater.notifyAppReady();
24
+
25
+ const response = await CapacitorHttp.get({
26
+ url: `${API_BASE_URL}/projects/get-bundle`,
27
+ params: {
28
+ key: options?.apiKey ?? "",
29
+ iosPackage: options?.iosPackage ?? "",
30
+ androidPackage: options?.androidPackage ?? "",
31
+ },
32
+ });
33
+
34
+ const platformData =
35
+ Capacitor.getPlatform() === "android"
36
+ ? response.data.android
37
+ : response.data.ios;
38
+
39
+ const {
40
+ version: availableVersion = 0,
41
+ url,
42
+ forceUpdate = false,
43
+ bundleId,
44
+ } = platformData;
45
+
46
+ const currentBundle = await CapacitorUpdater.current();
47
+ const currentVersion =
48
+ currentBundle.bundle.version === "builtin"
49
+ ? 0
50
+ : Number(currentBundle.bundle.version);
51
+
52
+ if (availableVersion > currentVersion) {
53
+ const info: UpdateInfo = {
54
+ availableVersion,
55
+ url,
56
+ forceUpdate,
57
+ bundleId,
58
+ };
59
+ setUpdateInfo(info);
60
+ setUpdateModalVisible(true);
61
+
62
+ if (forceUpdate) await handleUpdate(info);
63
+ }
64
+ } catch (err) {
65
+ console.warn("[CapacitorUpdater] Failed to fetch update:", err);
66
+ message.error("Failed to check for updates.");
67
+ }
68
+ })();
69
+ }, [options?.apiKey, options?.iosPackage, options?.androidPackage]);
70
+
71
+ const handleUpdate = async (info = updateInfo) => {
72
+ if (!info) return;
73
+ setUpdateModalVisible(false);
74
+
75
+ const hideLoader = message.loading("Downloading update...", 0);
76
+
77
+ try {
78
+ const data = await CapacitorUpdater.download({
79
+ version: info.availableVersion.toString(),
80
+ url: info.url,
81
+ });
82
+
83
+ await CapacitorUpdater.set(data);
84
+
85
+ await CapacitorHttp.post({
86
+ url: `${API_BASE_URL}/bundles/${info.bundleId}/count`,
87
+ headers: { "Content-Type": "application/json" },
88
+ data: { status: "success" },
89
+ });
90
+
91
+ hideLoader();
92
+ message.success("Update installed successfully!");
93
+ console.log("[CapacitorUpdater] Update installed");
94
+ } catch (err: any) {
95
+ const deviceinfo = await Device.getInfo();
96
+ await CapacitorHttp.post({
97
+ url: `${API_BASE_URL}/bundles/${info.bundleId}/count`,
98
+ headers: { "Content-Type": "application/json" },
99
+ data: {
100
+ status: "failure",
101
+ error: err?.message ?? "Failed to install update.",
102
+ deviceInfo: {
103
+ model: deviceinfo.model,
104
+ brand: deviceinfo.manufacturer,
105
+ systemName: deviceinfo.operatingSystem,
106
+ systemVersion: deviceinfo.osVersion,
107
+ },
108
+ },
109
+ });
110
+
111
+ hideLoader();
112
+ message.error("Failed to install update.");
113
+ console.error("[CapacitorUpdater] Failed to install update:", err);
114
+ }
115
+ };
116
+
117
+ return {
118
+ updateInfo,
119
+ isUpdateModalVisible,
120
+ setUpdateModalVisible,
121
+ handleUpdate,
122
+ };
123
+ }