pushkoncept-react-native 0.0.0-stage → 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/OTAUpdate.js +161 -0
- package/README.md +93 -2
- package/index.d.ts +55 -0
- package/index.js +348 -0
- package/package.json +33 -4
package/OTAUpdate.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// React Native OTA SDK
|
|
2
|
+
import { Platform, DevSettings } from 'react-native';
|
|
3
|
+
import RNFS from 'react-native-fs';
|
|
4
|
+
|
|
5
|
+
class OTAClient {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.config = {
|
|
8
|
+
serverUrl: null,
|
|
9
|
+
appId: null,
|
|
10
|
+
appToken: null,
|
|
11
|
+
apiKey: null,
|
|
12
|
+
channel: 'production'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the SDK with your App ID, Server URL, and API Key / App Token.
|
|
18
|
+
* You get the App ID and Token from your OTA Dashboard.
|
|
19
|
+
*/
|
|
20
|
+
configure(options) {
|
|
21
|
+
if (!options.serverUrl || !options.appId) {
|
|
22
|
+
console.error('[OTA] Missing serverUrl or appId in configuration!');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.config = { ...this.config, ...options };
|
|
26
|
+
console.log('[OTA] Configured for App ID:', this.config.appId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns current active OTA version if any, or the default version.
|
|
31
|
+
*/
|
|
32
|
+
async getCurrentVersion(defaultVersion) {
|
|
33
|
+
try {
|
|
34
|
+
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
35
|
+
const savedVersion = await AsyncStorage.getItem('@ota_bundle_version');
|
|
36
|
+
return savedVersion || defaultVersion;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return defaultVersion;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks the server to see if a new update is available.
|
|
44
|
+
* @param {string} currentVersion - The base version of your app (e.g., '1.0.0')
|
|
45
|
+
*/
|
|
46
|
+
async checkForUpdate(currentVersion) {
|
|
47
|
+
if (!this.config.serverUrl) throw new Error('[OTA] You must call OTA.configure() first!');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const activeVersion = await this.getCurrentVersion(currentVersion);
|
|
51
|
+
const deviceId = await this._getDeviceId();
|
|
52
|
+
const url = new URL(`${this.config.serverUrl}/api/update/check`);
|
|
53
|
+
url.searchParams.set('app_id', this.config.appId);
|
|
54
|
+
url.searchParams.set('version', activeVersion);
|
|
55
|
+
url.searchParams.set('app_version', currentVersion);
|
|
56
|
+
url.searchParams.set('channel', this.config.channel);
|
|
57
|
+
url.searchParams.set('device_id', deviceId);
|
|
58
|
+
url.searchParams.set('platform', Platform.OS); // 'ios' or 'android'
|
|
59
|
+
url.searchParams.set('os', Platform.OS); // 'ios' or 'android'
|
|
60
|
+
|
|
61
|
+
const token = this.config.apiKey || this.config.appToken || this.config.token;
|
|
62
|
+
if (token) url.searchParams.set('token', token);
|
|
63
|
+
|
|
64
|
+
const headers = { 'ngrok-skip-browser-warning': 'true' };
|
|
65
|
+
if (token) headers['x-api-key'] = token;
|
|
66
|
+
|
|
67
|
+
const response = await fetch(url.toString(), { headers });
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
return data.update_available ? data : null;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn('[OTA] Update check failed:', error.message);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Downloads and verifies the update bundle.
|
|
79
|
+
*/
|
|
80
|
+
async downloadAndApplyUpdate(update, onProgress) {
|
|
81
|
+
if (!update?.bundle_url) return false;
|
|
82
|
+
|
|
83
|
+
const destPath = RNFS.DocumentDirectoryPath + '/ota_update.bundle';
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await RNFS.downloadFile({
|
|
87
|
+
fromUrl: update.bundle_url,
|
|
88
|
+
toFile: destPath,
|
|
89
|
+
headers: {
|
|
90
|
+
'ngrok-skip-browser-warning': 'true',
|
|
91
|
+
},
|
|
92
|
+
progress: (res) => {
|
|
93
|
+
const pct = Math.round(res.bytesWritten / res.contentLength * 100);
|
|
94
|
+
if (onProgress) onProgress(pct);
|
|
95
|
+
},
|
|
96
|
+
}).promise;
|
|
97
|
+
|
|
98
|
+
if (result.statusCode !== 200) throw new Error(`Download failed: ${result.statusCode}`);
|
|
99
|
+
|
|
100
|
+
if (update.bundle_hash) {
|
|
101
|
+
const fileHash = await RNFS.hash(destPath, 'sha256');
|
|
102
|
+
if (fileHash !== update.bundle_hash) {
|
|
103
|
+
await RNFS.unlink(destPath);
|
|
104
|
+
throw new Error('Hash mismatch - bundle corrupted');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await this._reportDownload(update.release_id, 'success');
|
|
109
|
+
|
|
110
|
+
// ✅ Bundle path save karo taake next launch par use ho
|
|
111
|
+
try {
|
|
112
|
+
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
113
|
+
await AsyncStorage.setItem('@ota_bundle_path', destPath);
|
|
114
|
+
await AsyncStorage.setItem('@ota_bundle_version', update.version);
|
|
115
|
+
} catch(e) {}
|
|
116
|
+
|
|
117
|
+
// ✅ App reload karo naya bundle apply karne ke liye
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
try { DevSettings.reload(); } catch(e) {}
|
|
120
|
+
}, 500);
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('[OTA] Download failed:', error.message);
|
|
126
|
+
await this._reportDownload(update.release_id, 'failed');
|
|
127
|
+
try { await RNFS.unlink(destPath); } catch(e) {}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async _reportDownload(releaseId, status) {
|
|
133
|
+
try {
|
|
134
|
+
await fetch(`${this.config.serverUrl}/api/update/download-${status}`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
'ngrok-skip-browser-warning': 'true', // ngrok fix
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({ release_id: releaseId, platform: Platform.OS }),
|
|
141
|
+
});
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async _getDeviceId() {
|
|
146
|
+
try {
|
|
147
|
+
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
148
|
+
let deviceId = await AsyncStorage.getItem('@ota_device_id');
|
|
149
|
+
if (!deviceId) {
|
|
150
|
+
deviceId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
151
|
+
await AsyncStorage.setItem('@ota_device_id', deviceId);
|
|
152
|
+
}
|
|
153
|
+
return deviceId;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return 'unknown';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Export a single instance to be used throughout the app
|
|
161
|
+
export default new OTAClient();
|
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🚀 PushKoncept React Native SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Seamless, instant Over-The-Air (OTA) updates for React Native apps. Push JavaScript bundle updates and assets directly to users' devices without waiting for App Store or Google Play approval.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📦 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Using npm
|
|
11
|
+
npm install pushkoncept-react-native react-native-fs @react-native-async-storage/async-storage
|
|
12
|
+
|
|
13
|
+
# Or using yarn
|
|
14
|
+
yarn add pushkoncept-react-native react-native-fs @react-native-async-storage/async-storage
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **iOS Note**: Run `cd ios && pod install` after installation.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ⚡ Quick Start (1 Line Integration)
|
|
22
|
+
|
|
23
|
+
Wrap your root `App` component with `withPushKoncept`:
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { View, Text } from 'react-native';
|
|
28
|
+
import { withPushKoncept } from 'pushkoncept-react-native';
|
|
29
|
+
|
|
30
|
+
const App = () => {
|
|
31
|
+
return (
|
|
32
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
33
|
+
<Text>Welcome to My App!</Text>
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Wrap your root component with PushKoncept
|
|
39
|
+
export default withPushKoncept(App, {
|
|
40
|
+
serverUrl: 'http://localhost:3001', // Your PushKoncept Server URL
|
|
41
|
+
appId: 'YOUR_APP_ID', // Found in PushKoncept Portal
|
|
42
|
+
appToken: 'YOUR_APP_TOKEN', // Found in App Settings
|
|
43
|
+
channel: 'production' // 'production' | 'staging' | 'beta'
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That's it! Your app will now:
|
|
48
|
+
1. Automatically check for OTA updates on app launch and resume.
|
|
49
|
+
2. Silently download and apply updates in the background.
|
|
50
|
+
3. Automatically prompt and show a progress modal for mandatory / force updates.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🛠️ Manual / Custom Control (CodePush Style)
|
|
55
|
+
|
|
56
|
+
If you want fine-grained control over when updates are checked and applied:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import PushKoncept, { InstallMode } from 'pushkoncept-react-native';
|
|
60
|
+
|
|
61
|
+
// 1. Configure during app startup (e.g. index.js or App.js)
|
|
62
|
+
PushKoncept.configure({
|
|
63
|
+
serverUrl: 'http://localhost:3001',
|
|
64
|
+
appId: 'YOUR_APP_ID',
|
|
65
|
+
appToken: 'YOUR_APP_TOKEN',
|
|
66
|
+
channel: 'production'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 2. Check and Sync
|
|
70
|
+
async function handleCheckUpdate() {
|
|
71
|
+
const update = await PushKoncept.checkForUpdate('1.0.0');
|
|
72
|
+
if (update) {
|
|
73
|
+
console.log('Update found! Version:', update.version);
|
|
74
|
+
|
|
75
|
+
await PushKoncept.downloadAndApplyUpdate(update, {
|
|
76
|
+
installMode: InstallMode.IMMEDIATE, // or InstallMode.ON_NEXT_RESUME
|
|
77
|
+
onProgress: (pct) => {
|
|
78
|
+
console.log(`Downloading: ${pct}%`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 💻 Deploying Updates with PushKoncept CLI
|
|
88
|
+
|
|
89
|
+
Deploy updates directly from your terminal in 1 command without creating zip bundles manually:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# In your React Native project root:
|
|
93
|
+
pushkoncept release-react --app "My App" --version 1.0.1
|
|
94
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export declare const InstallMode: {
|
|
4
|
+
readonly IMMEDIATE: 'IMMEDIATE';
|
|
5
|
+
readonly ON_NEXT_RESUME: 'ON_NEXT_RESUME';
|
|
6
|
+
readonly ON_NEXT_RESTART: 'ON_NEXT_RESTART';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface PushKonceptConfig {
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
appId: string;
|
|
12
|
+
appToken?: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
channel?: 'production' | 'staging' | 'beta' | string;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
appVersion?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UpdateInfo {
|
|
20
|
+
update_available: boolean;
|
|
21
|
+
version: string;
|
|
22
|
+
build_number: number;
|
|
23
|
+
bundle_url: string;
|
|
24
|
+
bundle_hash: string;
|
|
25
|
+
bundle_size: number;
|
|
26
|
+
release_notes?: string;
|
|
27
|
+
rollout_percentage: number;
|
|
28
|
+
is_mandatory: boolean;
|
|
29
|
+
release_id: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SyncOptions {
|
|
33
|
+
appVersion?: string;
|
|
34
|
+
installMode?: keyof typeof InstallMode;
|
|
35
|
+
onProgress?: (percentage: number) => void;
|
|
36
|
+
onMandatoryUpdate?: (update: UpdateInfo, proceed: () => void) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare class PushKonceptClient {
|
|
40
|
+
configure(options: PushKonceptConfig): void;
|
|
41
|
+
getCurrentVersion(baseVersion?: string): Promise<string>;
|
|
42
|
+
checkForUpdate(currentAppVersion?: string): Promise<UpdateInfo | null>;
|
|
43
|
+
downloadAndApplyUpdate(update: UpdateInfo, options?: SyncOptions): Promise<boolean>;
|
|
44
|
+
sync(options?: SyncOptions): Promise<void>;
|
|
45
|
+
reloadApp(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare const PushKoncept: PushKonceptClient;
|
|
49
|
+
|
|
50
|
+
export declare function withPushKoncept<P extends object>(
|
|
51
|
+
WrappedComponent: React.ComponentType<P>,
|
|
52
|
+
config: PushKonceptConfig
|
|
53
|
+
): React.ComponentType<P>;
|
|
54
|
+
|
|
55
|
+
export default PushKoncept;
|
package/index.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PushKoncept React Native SDK
|
|
3
|
+
* Fast, reliable Over-The-Air (OTA) updates for React Native apps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useState } from 'react';
|
|
7
|
+
import { Platform, DevSettings, Alert, AppState, View, Text, Modal, ActivityIndicator, StyleSheet } from 'react-native';
|
|
8
|
+
import RNFS from 'react-native-fs';
|
|
9
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
10
|
+
|
|
11
|
+
export const InstallMode = {
|
|
12
|
+
IMMEDIATE: 'IMMEDIATE',
|
|
13
|
+
ON_NEXT_RESUME: 'ON_NEXT_RESUME',
|
|
14
|
+
ON_NEXT_RESTART: 'ON_NEXT_RESTART'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class PushKonceptClient {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.config = {
|
|
20
|
+
serverUrl: null,
|
|
21
|
+
appId: null,
|
|
22
|
+
appToken: null,
|
|
23
|
+
channel: 'production',
|
|
24
|
+
debug: false
|
|
25
|
+
};
|
|
26
|
+
this._isSyncing = false;
|
|
27
|
+
this._hasPendingUpdate = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log(...args) {
|
|
31
|
+
if (this.config.debug) {
|
|
32
|
+
console.log('[PushKoncept]', ...args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configure the SDK with your App ID and Server URL
|
|
38
|
+
*/
|
|
39
|
+
configure(options = {}) {
|
|
40
|
+
if (!options.serverUrl || !options.appId) {
|
|
41
|
+
console.error('[PushKoncept] Error: serverUrl and appId are required.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.config = { ...this.config, ...options };
|
|
45
|
+
this.log('Configured for App ID:', this.config.appId, 'Channel:', this.config.channel);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get active OTA version from local storage or fallback to base
|
|
50
|
+
*/
|
|
51
|
+
async getCurrentVersion(baseVersion = '1.0.0') {
|
|
52
|
+
try {
|
|
53
|
+
const savedVersion = await AsyncStorage.getItem('@pushkoncept_bundle_version');
|
|
54
|
+
return savedVersion || baseVersion;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return baseVersion;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if an update is available on the PushKoncept server
|
|
62
|
+
*/
|
|
63
|
+
async checkForUpdate(currentAppVersion = '1.0.0') {
|
|
64
|
+
if (!this.config.serverUrl || !this.config.appId) {
|
|
65
|
+
throw new Error('[PushKoncept] You must call PushKoncept.configure() first.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const activeVersion = await this.getCurrentVersion(currentAppVersion);
|
|
70
|
+
const deviceId = await this._getDeviceId();
|
|
71
|
+
|
|
72
|
+
const url = new URL(`${this.config.serverUrl.replace(/\/$/, '')}/api/update/check`);
|
|
73
|
+
url.searchParams.set('app_id', this.config.appId);
|
|
74
|
+
url.searchParams.set('version', activeVersion);
|
|
75
|
+
url.searchParams.set('app_version', currentAppVersion);
|
|
76
|
+
url.searchParams.set('channel', this.config.channel || 'production');
|
|
77
|
+
url.searchParams.set('device_id', deviceId);
|
|
78
|
+
url.searchParams.set('platform', Platform.OS);
|
|
79
|
+
|
|
80
|
+
const token = this.config.appToken || this.config.apiKey || this.config.token;
|
|
81
|
+
if (token) {
|
|
82
|
+
url.searchParams.set('token', token);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const headers = { 'ngrok-skip-browser-warning': 'true' };
|
|
86
|
+
if (token) headers['x-api-key'] = token;
|
|
87
|
+
|
|
88
|
+
this.log('Checking for update at:', url.toString());
|
|
89
|
+
const response = await fetch(url.toString(), { headers });
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
|
|
92
|
+
if (data && data.update_available) {
|
|
93
|
+
this.log('Update found! Version:', data.version, 'Mandatory:', data.is_mandatory);
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.log('App is up to date.');
|
|
98
|
+
return null;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn('[PushKoncept] Update check failed:', error.message);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Download and install a bundle update
|
|
107
|
+
*/
|
|
108
|
+
async downloadAndApplyUpdate(update, options = {}) {
|
|
109
|
+
if (!update?.bundle_url) return false;
|
|
110
|
+
|
|
111
|
+
const onProgress = options.onProgress || (() => {});
|
|
112
|
+
const installMode = options.installMode || (update.is_mandatory ? InstallMode.IMMEDIATE : InstallMode.ON_NEXT_RESUME);
|
|
113
|
+
|
|
114
|
+
const destPath = `${RNFS.DocumentDirectoryPath}/pushkoncept_update.bundle`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
this.log('Starting bundle download from:', update.bundle_url);
|
|
118
|
+
const result = await RNFS.downloadFile({
|
|
119
|
+
fromUrl: update.bundle_url,
|
|
120
|
+
toFile: destPath,
|
|
121
|
+
headers: { 'ngrok-skip-browser-warning': 'true' },
|
|
122
|
+
progress: (res) => {
|
|
123
|
+
const pct = Math.round((res.bytesWritten / res.contentLength) * 100);
|
|
124
|
+
onProgress(pct);
|
|
125
|
+
}
|
|
126
|
+
}).promise;
|
|
127
|
+
|
|
128
|
+
if (result.statusCode !== 200) {
|
|
129
|
+
throw new Error(`Download failed with HTTP ${result.statusCode}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// SHA-256 Hash verification
|
|
133
|
+
if (update.bundle_hash) {
|
|
134
|
+
const fileHash = await RNFS.hash(destPath, 'sha256');
|
|
135
|
+
if (fileHash !== update.bundle_hash) {
|
|
136
|
+
await RNFS.unlink(destPath).catch(() => {});
|
|
137
|
+
throw new Error('Hash mismatch: bundle may be corrupted.');
|
|
138
|
+
}
|
|
139
|
+
this.log('SHA-256 Hash verified successfully.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Record successful download
|
|
143
|
+
await this._reportDownload(update.release_id, 'success');
|
|
144
|
+
|
|
145
|
+
// Save version and path metadata
|
|
146
|
+
await AsyncStorage.setItem('@pushkoncept_bundle_path', destPath);
|
|
147
|
+
await AsyncStorage.setItem('@pushkoncept_bundle_version', update.version);
|
|
148
|
+
this._hasPendingUpdate = true;
|
|
149
|
+
|
|
150
|
+
this.log('Update installed successfully! InstallMode:', installMode);
|
|
151
|
+
|
|
152
|
+
if (installMode === InstallMode.IMMEDIATE) {
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
this.reloadApp();
|
|
155
|
+
}, 300);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('[PushKoncept] Download failed:', error.message);
|
|
161
|
+
await this._reportDownload(update.release_id, 'failed');
|
|
162
|
+
await RNFS.unlink(destPath).catch(() => {});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Reload app to activate new JavaScript bundle
|
|
169
|
+
*/
|
|
170
|
+
reloadApp() {
|
|
171
|
+
try {
|
|
172
|
+
if (DevSettings && DevSettings.reload) {
|
|
173
|
+
DevSettings.reload();
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.warn('[PushKoncept] Unable to auto-reload. Update will apply on next restart.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One-line CodePush style sync method
|
|
182
|
+
*/
|
|
183
|
+
async sync(options = {}) {
|
|
184
|
+
if (this._isSyncing) return;
|
|
185
|
+
this._isSyncing = true;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const update = await this.checkForUpdate(options.appVersion || '1.0.0');
|
|
189
|
+
if (!update) {
|
|
190
|
+
this._isSyncing = false;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (update.is_mandatory && options.onMandatoryUpdate) {
|
|
195
|
+
options.onMandatoryUpdate(update, async () => {
|
|
196
|
+
await this.downloadAndApplyUpdate(update, options);
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
await this.downloadAndApplyUpdate(update, options);
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
this._isSyncing = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async _reportDownload(releaseId, status) {
|
|
207
|
+
try {
|
|
208
|
+
await fetch(`${this.config.serverUrl.replace(/\/$/, '')}/api/update/download-${status}`, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: {
|
|
211
|
+
'Content-Type': 'application/json',
|
|
212
|
+
'ngrok-skip-browser-warning': 'true'
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({ release_id: releaseId, platform: Platform.OS })
|
|
215
|
+
});
|
|
216
|
+
} catch (e) {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async _getDeviceId() {
|
|
220
|
+
try {
|
|
221
|
+
let deviceId = await AsyncStorage.getItem('@pushkoncept_device_id');
|
|
222
|
+
if (!deviceId) {
|
|
223
|
+
deviceId = 'pk_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
224
|
+
await AsyncStorage.setItem('@pushkoncept_device_id', deviceId);
|
|
225
|
+
}
|
|
226
|
+
return deviceId;
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return 'device_' + Platform.OS;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const PushKoncept = new PushKonceptClient();
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Higher-Order Component (HOC) to wrap your root App component
|
|
237
|
+
* Automatically checks for updates on launch and app resume!
|
|
238
|
+
*/
|
|
239
|
+
export function withPushKoncept(WrappedComponent, config = {}) {
|
|
240
|
+
return function PushKonceptRoot(props) {
|
|
241
|
+
const [downloading, setDownloading] = useState(false);
|
|
242
|
+
const [progress, setProgress] = useState(0);
|
|
243
|
+
const [mandatory, setMandatory] = useState(false);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
PushKoncept.configure(config);
|
|
247
|
+
|
|
248
|
+
const performSync = () => {
|
|
249
|
+
PushKoncept.sync({
|
|
250
|
+
appVersion: config.appVersion || '1.0.0',
|
|
251
|
+
onProgress: (pct) => {
|
|
252
|
+
setProgress(pct);
|
|
253
|
+
},
|
|
254
|
+
onMandatoryUpdate: (update, proceed) => {
|
|
255
|
+
setMandatory(true);
|
|
256
|
+
setDownloading(true);
|
|
257
|
+
proceed();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
performSync();
|
|
263
|
+
|
|
264
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
265
|
+
if (nextState === 'active') {
|
|
266
|
+
performSync();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return () => {
|
|
271
|
+
if (subscription && subscription.remove) {
|
|
272
|
+
subscription.remove();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<View style={{ flex: 1 }}>
|
|
279
|
+
<WrappedComponent {...props} />
|
|
280
|
+
{downloading && (
|
|
281
|
+
<Modal transparent visible={downloading} animationType="fade">
|
|
282
|
+
<View style={styles.overlay}>
|
|
283
|
+
<View style={styles.dialog}>
|
|
284
|
+
<ActivityIndicator size="large" color="#00E5FF" />
|
|
285
|
+
<Text style={styles.title}>
|
|
286
|
+
{mandatory ? 'Critical Update Required' : 'Installing Update'}
|
|
287
|
+
</Text>
|
|
288
|
+
<Text style={styles.subtitle}>
|
|
289
|
+
Downloading latest improvements ({progress}%)...
|
|
290
|
+
</Text>
|
|
291
|
+
<View style={styles.progressBarBg}>
|
|
292
|
+
<View style={[styles.progressBarFill, { width: `${progress}%` }]} />
|
|
293
|
+
</View>
|
|
294
|
+
</View>
|
|
295
|
+
</View>
|
|
296
|
+
</Modal>
|
|
297
|
+
)}
|
|
298
|
+
</View>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const styles = StyleSheet.create({
|
|
304
|
+
overlay: {
|
|
305
|
+
flex: 1,
|
|
306
|
+
backgroundColor: 'rgba(10, 14, 26, 0.85)',
|
|
307
|
+
justifyContent: 'center',
|
|
308
|
+
alignItems: 'center',
|
|
309
|
+
padding: 24
|
|
310
|
+
},
|
|
311
|
+
dialog: {
|
|
312
|
+
width: '100%',
|
|
313
|
+
maxWidth: 340,
|
|
314
|
+
backgroundColor: '#121829',
|
|
315
|
+
borderRadius: 16,
|
|
316
|
+
padding: 24,
|
|
317
|
+
alignItems: 'center',
|
|
318
|
+
borderWidth: 1,
|
|
319
|
+
borderColor: 'rgba(0, 229, 255, 0.3)'
|
|
320
|
+
},
|
|
321
|
+
title: {
|
|
322
|
+
color: '#EAF0FF',
|
|
323
|
+
fontSize: 18,
|
|
324
|
+
fontWeight: 'bold',
|
|
325
|
+
marginTop: 16,
|
|
326
|
+
marginBottom: 6,
|
|
327
|
+
textAlign: 'center'
|
|
328
|
+
},
|
|
329
|
+
subtitle: {
|
|
330
|
+
color: '#8A94AD',
|
|
331
|
+
fontSize: 13,
|
|
332
|
+
marginBottom: 16,
|
|
333
|
+
textAlign: 'center'
|
|
334
|
+
},
|
|
335
|
+
progressBarBg: {
|
|
336
|
+
width: '100%',
|
|
337
|
+
height: 6,
|
|
338
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
339
|
+
borderRadius: 3,
|
|
340
|
+
overflow: 'hidden'
|
|
341
|
+
},
|
|
342
|
+
progressBarFill: {
|
|
343
|
+
height: '100%',
|
|
344
|
+
backgroundColor: '#00E5FF'
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
export default PushKoncept;
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pushkoncept-react-native",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official PushKoncept React Native SDK for seamless Over-The-Air (OTA) updates and hotfixes",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pushkoncept",
|
|
12
|
+
"ota",
|
|
13
|
+
"codepush",
|
|
14
|
+
"react-native",
|
|
15
|
+
"hotfix",
|
|
16
|
+
"live-update",
|
|
17
|
+
"over-the-air"
|
|
18
|
+
],
|
|
19
|
+
"author": "PushKoncept",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "*",
|
|
23
|
+
"react-native": "*",
|
|
24
|
+
"react-native-fs": ">=2.18.0",
|
|
25
|
+
"@react-native-async-storage/async-storage": ">=1.15.0"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/pushkoncept/Push-Koncept.git",
|
|
30
|
+
"directory": "sdk/react-native"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|