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 +122 -0
- package/dist/UpdaterModal.d.ts +3 -0
- package/dist/UpdaterModal.js +47 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/dist/useCapacitorUpdater.d.ts +11 -0
- package/dist/useCapacitorUpdater.js +98 -0
- package/package.json +56 -0
- package/scripts/bundle.cjs +187 -0
- package/src/UpdaterModal.tsx +100 -0
- package/src/index.ts +3 -0
- package/src/types.ts +31 -0
- package/src/useCapacitorUpdater.ts +123 -0
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,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
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|