react-native-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/package.json ADDED
@@ -0,0 +1,170 @@
1
+ {
2
+ "name": "react-native-3rddigital-appupdate",
3
+ "version": "1.0.0",
4
+ "description": "A React Native library for seamless over-the-air (OTA) updates with version checks, automatic bundle download, and customizable user prompts for iOS and Android.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "build": "bob build",
36
+ "example": "yarn workspace react-native-app-update-example",
37
+ "test": "jest",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "clean": "del-cli lib",
41
+ "prepare": "bob build",
42
+ "release": "release-it --only-version"
43
+ },
44
+ "keywords": [
45
+ "react-native",
46
+ "ios",
47
+ "android"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/latest3rddigital/react-native-app-update.git"
52
+ },
53
+ "author": "Sagar Bhavsar <sagar@3rddigital.com> (https://github.com/latest3rddigital/react-native-app-update)",
54
+ "license": "MIT",
55
+ "bugs": {
56
+ "url": "https://github.com/latest3rddigital/react-native-app-update/issues"
57
+ },
58
+ "homepage": "https://github.com/latest3rddigital/react-native-app-update#readme",
59
+ "publishConfig": {
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "devDependencies": {
63
+ "@commitlint/config-conventional": "^19.8.1",
64
+ "@eslint/compat": "^1.3.2",
65
+ "@eslint/eslintrc": "^3.3.1",
66
+ "@eslint/js": "^9.35.0",
67
+ "@evilmartians/lefthook": "^1.12.3",
68
+ "@react-native/babel-preset": "0.81.1",
69
+ "@react-native/eslint-config": "^0.81.1",
70
+ "@release-it/conventional-changelog": "^10.0.1",
71
+ "@types/jest": "^29.5.14",
72
+ "@types/react": "^19.1.12",
73
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
74
+ "commitlint": "^19.8.1",
75
+ "del-cli": "^6.0.0",
76
+ "eslint": "^9.35.0",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-plugin-ft-flow": "^3.0.11",
79
+ "eslint-plugin-jest": "^29.0.1",
80
+ "eslint-plugin-prettier": "^5.5.4",
81
+ "eslint-plugin-react-native": "^5.0.0",
82
+ "jest": "^29.7.0",
83
+ "prettier": "^3.6.2",
84
+ "react": "19.1.0",
85
+ "react-native": "0.81.4",
86
+ "react-native-builder-bob": "^0.40.13",
87
+ "release-it": "^19.0.4",
88
+ "typescript": "^5.9.2"
89
+ },
90
+ "peerDependencies": {
91
+ "react": "*",
92
+ "react-native": "*"
93
+ },
94
+ "workspaces": [
95
+ "example"
96
+ ],
97
+ "packageManager": "yarn@3.6.1",
98
+ "jest": {
99
+ "preset": "react-native",
100
+ "modulePathIgnorePatterns": [
101
+ "<rootDir>/example/node_modules",
102
+ "<rootDir>/lib/"
103
+ ]
104
+ },
105
+ "commitlint": {
106
+ "extends": [
107
+ "@commitlint/config-conventional"
108
+ ]
109
+ },
110
+ "release-it": {
111
+ "git": {
112
+ "commitMessage": "chore: release ${version}",
113
+ "tagName": "v${version}"
114
+ },
115
+ "npm": {
116
+ "publish": true
117
+ },
118
+ "github": {
119
+ "release": true
120
+ },
121
+ "plugins": {
122
+ "@release-it/conventional-changelog": {
123
+ "preset": {
124
+ "name": "angular"
125
+ }
126
+ }
127
+ }
128
+ },
129
+ "prettier": {
130
+ "quoteProps": "consistent",
131
+ "singleQuote": true,
132
+ "tabWidth": 2,
133
+ "trailingComma": "es5",
134
+ "useTabs": false
135
+ },
136
+ "react-native-builder-bob": {
137
+ "source": "src",
138
+ "output": "lib",
139
+ "targets": [
140
+ [
141
+ "module",
142
+ {
143
+ "esm": true
144
+ }
145
+ ],
146
+ [
147
+ "typescript",
148
+ {
149
+ "project": "tsconfig.build.json"
150
+ }
151
+ ]
152
+ ]
153
+ },
154
+ "create-react-native-library": {
155
+ "languages": "js",
156
+ "type": "library",
157
+ "version": "0.54.3"
158
+ },
159
+ "dependencies": {
160
+ "@inquirer/prompts": "^7.8.6",
161
+ "axios": "^1.12.2",
162
+ "form-data": "^4.0.4",
163
+ "react-native-blob-util": "^0.22.2",
164
+ "react-native-device-info": "^14.1.1",
165
+ "react-native-ota-hot-update": "^2.3.4"
166
+ },
167
+ "bin": {
168
+ "appupdate": "./scripts/bundle.js"
169
+ }
170
+ }
@@ -0,0 +1,186 @@
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: {
43
+ ...form.getHeaders(),
44
+ Authorization: `Bearer ${config.API_TOKEN}`,
45
+ },
46
+ });
47
+ console.log(
48
+ `✅ ${platform} bundle uploaded successfully! Response:`,
49
+ JSON.stringify(res.data, null, 2)
50
+ );
51
+ } catch (err) {
52
+ console.error(`❌ ${platform} bundle upload failed!`);
53
+ if (err.response) {
54
+ console.error('Status:', err.response.status);
55
+ console.error('Data:', err.response.data);
56
+ } else {
57
+ console.error('Message:', err.message);
58
+ }
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ function buildAndroid() {
64
+ console.log('📦 Building Android bundle...');
65
+ const outputPath = path.join('android', 'index.android.bundle.zip');
66
+ run(
67
+ `mkdir -p android/output && ` +
68
+ `react-native bundle --platform android --dev false --entry-file index.js ` +
69
+ `--bundle-output android/output/index.android.bundle --assets-dest android/output ` +
70
+ `--sourcemap-output android/sourcemap.js && ` +
71
+ `cd android && find output -type f | zip index.android.bundle.zip -@ && ` +
72
+ `zip sourcemap.zip sourcemap.js && cd .. && rm -rf android/output && rm -rf android/sourcemap.js`
73
+ );
74
+ console.log(`✅ Android bundle created at ${outputPath}`);
75
+ return outputPath;
76
+ }
77
+
78
+ function buildIOS() {
79
+ console.log('📦 Building iOS bundle...');
80
+ const outputPath = path.join('ios', 'main.jsbundle.zip');
81
+ run(
82
+ `mkdir -p ios/output && ` +
83
+ `react-native bundle --platform ios --dev false --entry-file index.js ` +
84
+ `--bundle-output ios/output/main.jsbundle --assets-dest ios/output ` +
85
+ `--sourcemap-output ios/sourcemap.js && ` +
86
+ `cd ios && find output -type f | zip main.jsbundle.zip -@ && ` +
87
+ `zip sourcemap.zip sourcemap.js && cd .. && rm -rf ios/output && rm -rf ios/sourcemap.js`
88
+ );
89
+ console.log(`✅ iOS bundle created at ${outputPath}`);
90
+ return outputPath;
91
+ }
92
+
93
+ async function getConfig(platform) {
94
+ console.log(`\n⚙️ Enter configuration for ${platform.toUpperCase()}\n`);
95
+
96
+ const API_TOKEN = await input({
97
+ message: `(${platform}) Enter API Token:`,
98
+ validate: (val) => (val.trim() ? true : 'API Token is required'),
99
+ });
100
+
101
+ const PROJECT_ID = await input({
102
+ message: `(${platform}) Enter Project ID:`,
103
+ validate: (val) => (val.trim() ? true : 'Project ID is required'),
104
+ });
105
+
106
+ const ENVIRONMENT = await select({
107
+ message: `(${platform}) Select Environment:`,
108
+ choices: [
109
+ { name: 'development', value: 'development' },
110
+ { name: 'production', value: 'production' },
111
+ ],
112
+ });
113
+
114
+ const VERSION = await input({
115
+ message: `(${platform}) Enter App Version (e.g. 1.0.0):`,
116
+ validate: (val) => (val.trim() ? true : 'Version is required'),
117
+ });
118
+
119
+ const BUILD_NUMBER = await input({
120
+ message: `(${platform}) Enter Build Number:`,
121
+ validate: (val) =>
122
+ !isNaN(val) && val.trim() !== '' ? true : 'Must be a number',
123
+ });
124
+
125
+ const FORCE_UPDATE = await confirm({
126
+ message: `(${platform}) Force Update?`,
127
+ default: false,
128
+ });
129
+
130
+ return {
131
+ API_TOKEN,
132
+ PROJECT_ID,
133
+ ENVIRONMENT,
134
+ VERSION,
135
+ BUILD_NUMBER,
136
+ FORCE_UPDATE,
137
+ };
138
+ }
139
+
140
+ (async () => {
141
+ try {
142
+ const platform = process.argv[2];
143
+ if (!platform) {
144
+ console.log('❌ Please specify a platform: android | ios | all');
145
+ process.exit(1);
146
+ }
147
+
148
+ if (platform === 'android') {
149
+ const config = await getConfig('android');
150
+ const androidFile = buildAndroid();
151
+ await uploadBundle({
152
+ filePath: androidFile,
153
+ platform: 'android',
154
+ config,
155
+ });
156
+ } else if (platform === 'ios') {
157
+ const config = await getConfig('ios');
158
+ const iosFile = buildIOS();
159
+ await uploadBundle({ filePath: iosFile, platform: 'ios', config });
160
+ } else if (platform === 'all') {
161
+ const androidConfig = await getConfig('android');
162
+ const androidFile = buildAndroid();
163
+ await uploadBundle({
164
+ filePath: androidFile,
165
+ platform: 'android',
166
+ config: androidConfig,
167
+ });
168
+
169
+ const iosConfig = await getConfig('ios');
170
+ const iosFile = buildIOS();
171
+ await uploadBundle({
172
+ filePath: iosFile,
173
+ platform: 'ios',
174
+ config: iosConfig,
175
+ });
176
+ } else {
177
+ console.log('❌ Invalid option. Use: android | ios | all');
178
+ process.exit(1);
179
+ }
180
+
181
+ console.log('\n🎉 All tasks completed successfully!\n');
182
+ } catch (err) {
183
+ console.error('❌ Fatal error:', err.message);
184
+ process.exit(1);
185
+ }
186
+ })();
@@ -0,0 +1,171 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ type TextStyle,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+
13
+ export type DialogOptions = {
14
+ title?: string;
15
+ message?: string;
16
+ cancelText?: string;
17
+ confirmText?: string;
18
+ onCancel?: () => void;
19
+ onConfirm?: () => void;
20
+ titleStyle?: TextStyle;
21
+ messageStyle?: TextStyle;
22
+ cancelButtonStyle?: ViewStyle;
23
+ confirmButtonStyle?: ViewStyle;
24
+ cancelTextStyle?: TextStyle;
25
+ confirmTextStyle?: TextStyle;
26
+ overlayColor?: string;
27
+ };
28
+
29
+ let showDialog: (options: DialogOptions) => void;
30
+ let hideDialog: () => void;
31
+
32
+ export const AppAlertDialog = () => {
33
+ const [visible, setVisible] = useState(false);
34
+ const [options, setOptions] = useState<DialogOptions>({});
35
+
36
+ showDialog = (opts: DialogOptions) => {
37
+ setOptions(opts);
38
+ setVisible(true);
39
+ };
40
+
41
+ hideDialog = () => {
42
+ setVisible(false);
43
+ };
44
+
45
+ const handleBackPress = () => {
46
+ if (visible) {
47
+ hideDialog();
48
+ options.onCancel?.();
49
+ }
50
+ };
51
+
52
+ return (
53
+ <Modal
54
+ transparent
55
+ visible={visible}
56
+ animationType="fade"
57
+ onRequestClose={handleBackPress}
58
+ >
59
+ <Pressable
60
+ style={[
61
+ styles.overlay,
62
+ { backgroundColor: options.overlayColor || 'rgba(0,0,0,0.3)' },
63
+ ]}
64
+ onPress={() => {
65
+ hideDialog();
66
+ options.onCancel?.();
67
+ }}
68
+ >
69
+ <Pressable style={styles.dialogBox} onPress={() => {}}>
70
+ <Text style={[styles.title, options.titleStyle]}>
71
+ {options.title || 'Alert'}
72
+ </Text>
73
+ <Text style={[styles.message, options.messageStyle]}>
74
+ {options.message || ''}
75
+ </Text>
76
+
77
+ <View style={styles.buttonRow}>
78
+ <TouchableOpacity
79
+ onPress={() => {
80
+ hideDialog();
81
+ options.onCancel?.();
82
+ }}
83
+ style={[
84
+ styles.button,
85
+ styles.cancelButton,
86
+ options.cancelButtonStyle,
87
+ ]}
88
+ >
89
+ <Text style={[styles.cancelText, options.cancelTextStyle]}>
90
+ {options.cancelText || 'Cancel'}
91
+ </Text>
92
+ </TouchableOpacity>
93
+
94
+ <TouchableOpacity
95
+ onPress={() => {
96
+ hideDialog();
97
+ options.onConfirm?.();
98
+ }}
99
+ style={[
100
+ styles.button,
101
+ styles.confirmButton,
102
+ options.confirmButtonStyle,
103
+ ]}
104
+ >
105
+ <Text style={[styles.confirmText, options.confirmTextStyle]}>
106
+ {options.confirmText || 'OK'}
107
+ </Text>
108
+ </TouchableOpacity>
109
+ </View>
110
+ </Pressable>
111
+ </Pressable>
112
+ </Modal>
113
+ );
114
+ };
115
+
116
+ const styles = StyleSheet.create({
117
+ overlay: {
118
+ flex: 1,
119
+ justifyContent: 'center',
120
+ alignItems: 'center',
121
+ zIndex: 9999,
122
+ },
123
+ dialogBox: {
124
+ width: '80%',
125
+ backgroundColor: '#fff',
126
+ borderRadius: 12,
127
+ padding: 20,
128
+ alignItems: 'center',
129
+ },
130
+ title: {
131
+ fontSize: 18,
132
+ fontWeight: '600',
133
+ marginBottom: 10,
134
+ textAlign: 'center',
135
+ },
136
+ message: {
137
+ fontSize: 14,
138
+ color: '#4B5563',
139
+ textAlign: 'center',
140
+ marginBottom: 20,
141
+ },
142
+ buttonRow: {
143
+ flexDirection: 'row',
144
+ justifyContent: 'space-between',
145
+ width: '100%',
146
+ },
147
+ button: {
148
+ flex: 1,
149
+ paddingVertical: 10,
150
+ borderRadius: 8,
151
+ alignItems: 'center',
152
+ marginHorizontal: 5,
153
+ },
154
+ cancelButton: {
155
+ backgroundColor: '#E5E7EB',
156
+ },
157
+ confirmButton: {
158
+ backgroundColor: '#2563EB',
159
+ },
160
+ cancelText: {
161
+ color: '#374151',
162
+ fontWeight: '500',
163
+ },
164
+ confirmText: {
165
+ color: '#fff',
166
+ fontWeight: '500',
167
+ },
168
+ });
169
+
170
+ AppAlertDialog.showMessage = (options: DialogOptions) => showDialog?.(options);
171
+ AppAlertDialog.hide = () => hideDialog?.();
@@ -0,0 +1,84 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ export type LoaderOptions = {
12
+ text?: string;
13
+ color?: string;
14
+ backgroundColor?: string;
15
+ textColor?: string;
16
+ containerStyle?: ViewStyle;
17
+ textStyle?: TextStyle;
18
+ };
19
+
20
+ let showLoader: (options?: LoaderOptions) => void;
21
+ let hideLoader: () => void;
22
+
23
+ export const AppLoader = () => {
24
+ const [visible, setVisible] = useState(false);
25
+ const [options, setOptions] = useState<LoaderOptions>({});
26
+
27
+ showLoader = (opts?: LoaderOptions) => {
28
+ setOptions(opts || {});
29
+ setVisible(true);
30
+ };
31
+
32
+ hideLoader = () => {
33
+ setVisible(false);
34
+ };
35
+
36
+ if (!visible) return null;
37
+
38
+ return (
39
+ <View
40
+ style={[
41
+ styles.overlay,
42
+ { backgroundColor: options.backgroundColor || 'rgba(0,0,0,0.3)' },
43
+ ]}
44
+ >
45
+ <View style={[styles.loaderBox, options.containerStyle]}>
46
+ <ActivityIndicator size="large" color={options.color || '#2563EB'} />
47
+ {options.text && (
48
+ <Text
49
+ style={[
50
+ styles.text,
51
+ { color: options.textColor || '#fff' },
52
+ options.textStyle,
53
+ ]}
54
+ >
55
+ {options.text}
56
+ </Text>
57
+ )}
58
+ </View>
59
+ </View>
60
+ );
61
+ };
62
+
63
+ const styles = StyleSheet.create({
64
+ overlay: {
65
+ ...StyleSheet.absoluteFillObject,
66
+ justifyContent: 'center',
67
+ alignItems: 'center',
68
+ zIndex: 9999,
69
+ },
70
+ loaderBox: {
71
+ padding: 20,
72
+ borderRadius: 10,
73
+ backgroundColor: '#1F2937',
74
+ alignItems: 'center',
75
+ },
76
+ text: {
77
+ marginTop: 12,
78
+ fontSize: 14,
79
+ fontWeight: '500',
80
+ },
81
+ });
82
+
83
+ AppLoader.show = (options?: LoaderOptions) => showLoader?.(options);
84
+ AppLoader.hide = () => hideLoader?.();
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { AppAlertDialog } from './AppAlertDialog';
3
+ import { AppLoader } from './AppLoader';
4
+
5
+ export const OTAProvider = ({ children }: { children?: React.ReactNode }) => (
6
+ <>
7
+ {children}
8
+ <AppLoader />
9
+ <AppAlertDialog />
10
+ </>
11
+ );