react-native-codepush-sdk 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/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/hooks/useFrameworkReady.js +11 -0
- package/dist/index.js +36 -0
- package/dist/services/codepushService.js +164 -0
- package/dist/src/components/UpdateChecker.js +230 -0
- package/dist/src/sdk/CodePushProvider.js +181 -0
- package/dist/src/sdk/CustomCodePush.js +405 -0
- package/dist/src/utils/BundleManager.js +124 -0
- package/dist/types/codepush.js +2 -0
- package/hooks/useFrameworkReady.ts +17 -0
- package/index.ts +10 -0
- package/package.json +72 -0
- package/services/codepushService.ts +181 -0
- package/src/components/UpdateChecker.tsx +303 -0
- package/src/sdk/CodePushProvider.tsx +184 -0
- package/src/sdk/CustomCodePush.ts +526 -0
- package/src/utils/BundleManager.ts +140 -0
- package/types/codepush.ts +44 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { CodePushUpdate, CodePushDeployment, CodePushSyncStatus, UpdateHistory } from '../types/codepush';
|
|
2
|
+
|
|
3
|
+
// Mock service for demonstration - replace with actual CodePush SDK calls
|
|
4
|
+
class CodePushService {
|
|
5
|
+
private mockDeployments: CodePushDeployment[] = [
|
|
6
|
+
{
|
|
7
|
+
name: 'Production',
|
|
8
|
+
key: 'prod-key-12345678',
|
|
9
|
+
package: {
|
|
10
|
+
id: '1',
|
|
11
|
+
label: 'v1.2.3',
|
|
12
|
+
description: 'Bug fixes and performance improvements',
|
|
13
|
+
packageHash: 'abc123def456',
|
|
14
|
+
blobUrl: 'https://codepush.blob.core.windows.net/package1',
|
|
15
|
+
downloadUrl: 'https://codepush.blob.core.windows.net/package1',
|
|
16
|
+
packageSize: 2048576,
|
|
17
|
+
deploymentKey: 'prod-key-12345678',
|
|
18
|
+
isFirstRun: false,
|
|
19
|
+
failedInstall: false,
|
|
20
|
+
isMandatory: false,
|
|
21
|
+
timestamp: Date.now() - 86400000,
|
|
22
|
+
version: '1.2.3'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Staging',
|
|
27
|
+
key: 'staging-key-87654321',
|
|
28
|
+
package: {
|
|
29
|
+
id: '2',
|
|
30
|
+
label: 'v1.2.4-beta',
|
|
31
|
+
description: 'New feature preview and experimental changes',
|
|
32
|
+
packageHash: 'def456ghi789',
|
|
33
|
+
blobUrl: 'https://codepush.blob.core.windows.net/package2',
|
|
34
|
+
downloadUrl: 'https://codepush.blob.core.windows.net/package2',
|
|
35
|
+
packageSize: 3145728,
|
|
36
|
+
deploymentKey: 'staging-key-87654321',
|
|
37
|
+
isFirstRun: false,
|
|
38
|
+
failedInstall: false,
|
|
39
|
+
isMandatory: true,
|
|
40
|
+
timestamp: Date.now() - 3600000,
|
|
41
|
+
version: '1.2.4'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Development',
|
|
46
|
+
key: 'dev-key-11223344',
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
private mockHistory: UpdateHistory[] = [
|
|
51
|
+
{
|
|
52
|
+
id: '1',
|
|
53
|
+
version: '1.2.3',
|
|
54
|
+
timestamp: Date.now() - 86400000,
|
|
55
|
+
status: 'SUCCESS',
|
|
56
|
+
description: 'Bug fixes and performance improvements',
|
|
57
|
+
downloadSize: 2048576,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: '2',
|
|
61
|
+
version: '1.2.2',
|
|
62
|
+
timestamp: Date.now() - 172800000,
|
|
63
|
+
status: 'SUCCESS',
|
|
64
|
+
description: 'UI improvements and new dashboard features',
|
|
65
|
+
downloadSize: 1536000,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: '3',
|
|
69
|
+
version: '1.2.1',
|
|
70
|
+
timestamp: Date.now() - 259200000,
|
|
71
|
+
status: 'ROLLBACK',
|
|
72
|
+
description: 'Critical bug fix rollback',
|
|
73
|
+
downloadSize: 1024000,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: '4',
|
|
77
|
+
version: '1.2.0',
|
|
78
|
+
timestamp: Date.now() - 345600000,
|
|
79
|
+
status: 'FAILED',
|
|
80
|
+
description: 'Major feature update with new authentication',
|
|
81
|
+
downloadSize: 4096000,
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
async checkForUpdate(): Promise<CodePushUpdate | null> {
|
|
86
|
+
// Simulate API call delay
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
88
|
+
|
|
89
|
+
// 70% chance of no update, 30% chance of update available
|
|
90
|
+
if (Math.random() > 0.3) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: 'update-' + Date.now(),
|
|
96
|
+
label: 'v1.2.5',
|
|
97
|
+
description: 'Security updates and bug fixes',
|
|
98
|
+
packageHash: 'new-hash-123',
|
|
99
|
+
blobUrl: 'https://codepush.blob.core.windows.net/new-package',
|
|
100
|
+
downloadUrl: 'https://codepush.blob.core.windows.net/new-package',
|
|
101
|
+
packageSize: 1843200,
|
|
102
|
+
deploymentKey: 'prod-key-12345678',
|
|
103
|
+
isFirstRun: false,
|
|
104
|
+
failedInstall: false,
|
|
105
|
+
isMandatory: false,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
version: '1.2.5'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async downloadUpdate(
|
|
112
|
+
update: CodePushUpdate,
|
|
113
|
+
onProgress: (progress: number) => void
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
// Simulate download progress
|
|
116
|
+
for (let i = 0; i <= 100; i += 5) {
|
|
117
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
118
|
+
onProgress(i);
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async installUpdate(): Promise<boolean> {
|
|
124
|
+
// Simulate installation
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async sync(onStatusChange: (status: CodePushSyncStatus) => void): Promise<boolean> {
|
|
130
|
+
onStatusChange({ status: 'CHECKING_FOR_UPDATE' });
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
132
|
+
|
|
133
|
+
const update = await this.checkForUpdate();
|
|
134
|
+
|
|
135
|
+
if (!update) {
|
|
136
|
+
onStatusChange({ status: 'UP_TO_DATE' });
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onStatusChange({ status: 'DOWNLOADING_PACKAGE', progress: 0 });
|
|
141
|
+
|
|
142
|
+
const downloadSuccess = await this.downloadUpdate(update, (progress) => {
|
|
143
|
+
onStatusChange({ status: 'DOWNLOADING_PACKAGE', progress });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!downloadSuccess) {
|
|
147
|
+
onStatusChange({ status: 'UNKNOWN_ERROR' });
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onStatusChange({ status: 'INSTALLING_UPDATE' });
|
|
152
|
+
const installSuccess = await this.installUpdate();
|
|
153
|
+
|
|
154
|
+
if (installSuccess) {
|
|
155
|
+
onStatusChange({ status: 'UPDATE_INSTALLED' });
|
|
156
|
+
return true;
|
|
157
|
+
} else {
|
|
158
|
+
onStatusChange({ status: 'UNKNOWN_ERROR' });
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getDeployments(): Promise<CodePushDeployment[]> {
|
|
164
|
+
// Simulate API call
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
166
|
+
return this.mockDeployments;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getUpdateHistory(): Promise<UpdateHistory[]> {
|
|
170
|
+
// Simulate API call
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
172
|
+
return this.mockHistory;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getCurrentPackageInfo(): Promise<CodePushUpdate | null> {
|
|
176
|
+
// Return current package info
|
|
177
|
+
return this.mockDeployments[0].package || null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const codePushService = new CodePushService();
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
Alert,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useCodePush } from '../sdk/CodePushProvider';
|
|
11
|
+
|
|
12
|
+
const UpdateChecker: React.FC = () => {
|
|
13
|
+
const {
|
|
14
|
+
currentUpdate,
|
|
15
|
+
availableUpdate,
|
|
16
|
+
syncStatus,
|
|
17
|
+
isChecking,
|
|
18
|
+
isDownloading,
|
|
19
|
+
isInstalling,
|
|
20
|
+
checkForUpdate,
|
|
21
|
+
syncUpdate,
|
|
22
|
+
rollback,
|
|
23
|
+
clearUpdates,
|
|
24
|
+
} = useCodePush();
|
|
25
|
+
|
|
26
|
+
const getStatusMessage = () => {
|
|
27
|
+
if (isChecking) return 'Checking for updates...';
|
|
28
|
+
if (isDownloading) return 'Downloading update...';
|
|
29
|
+
if (isInstalling) return 'Installing update...';
|
|
30
|
+
if (availableUpdate) return 'Update available!';
|
|
31
|
+
return 'App is up to date';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getStatusColor = () => {
|
|
35
|
+
if (isChecking || isDownloading || isInstalling) return '#007bff';
|
|
36
|
+
if (availableUpdate) return '#ffc107';
|
|
37
|
+
return '#28a745';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleRollback = () => {
|
|
41
|
+
Alert.alert(
|
|
42
|
+
'Rollback Update',
|
|
43
|
+
'Are you sure you want to rollback to the previous version? This will restart the app.',
|
|
44
|
+
[
|
|
45
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
46
|
+
{ text: 'Rollback', style: 'destructive', onPress: rollback },
|
|
47
|
+
]
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleClearUpdates = () => {
|
|
52
|
+
Alert.alert(
|
|
53
|
+
'Clear Updates',
|
|
54
|
+
'This will remove all downloaded updates and reset to the original app version.',
|
|
55
|
+
[
|
|
56
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
57
|
+
{ text: 'Clear', style: 'destructive', onPress: clearUpdates },
|
|
58
|
+
]
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const formatBytes = (bytes: number) => {
|
|
63
|
+
if (bytes === 0) return '0 Bytes';
|
|
64
|
+
const k = 1024;
|
|
65
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
66
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
67
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={styles.container}>
|
|
72
|
+
<View style={styles.header}>
|
|
73
|
+
<Text style={styles.title}>Custom CodePush</Text>
|
|
74
|
+
<Text style={styles.subtitle}>Over-the-Air Updates</Text>
|
|
75
|
+
</View>
|
|
76
|
+
|
|
77
|
+
{/* Status Card */}
|
|
78
|
+
<View style={[styles.statusCard, { borderLeftColor: getStatusColor() }]}>
|
|
79
|
+
<View style={styles.statusHeader}>
|
|
80
|
+
<Text style={[styles.statusText, { color: getStatusColor() }]}>
|
|
81
|
+
{getStatusMessage()}
|
|
82
|
+
</Text>
|
|
83
|
+
{(isChecking || isDownloading || isInstalling) && (
|
|
84
|
+
<ActivityIndicator size="small" color={getStatusColor()} />
|
|
85
|
+
)}
|
|
86
|
+
</View>
|
|
87
|
+
|
|
88
|
+
{syncStatus?.progress !== undefined && (
|
|
89
|
+
<View style={styles.progressContainer}>
|
|
90
|
+
<View style={styles.progressBar}>
|
|
91
|
+
<View
|
|
92
|
+
style={[
|
|
93
|
+
styles.progressFill,
|
|
94
|
+
{
|
|
95
|
+
width: `${syncStatus.progress}%`,
|
|
96
|
+
backgroundColor: getStatusColor(),
|
|
97
|
+
},
|
|
98
|
+
]}
|
|
99
|
+
/>
|
|
100
|
+
</View>
|
|
101
|
+
<Text style={styles.progressText}>{Math.round(syncStatus.progress)}%</Text>
|
|
102
|
+
</View>
|
|
103
|
+
)}
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{/* Current Version Info */}
|
|
107
|
+
{currentUpdate && (
|
|
108
|
+
<View style={styles.infoCard}>
|
|
109
|
+
<Text style={styles.cardTitle}>Current Version</Text>
|
|
110
|
+
<Text style={styles.infoText}>Label: {currentUpdate.label}</Text>
|
|
111
|
+
<Text style={styles.infoText}>Version: {currentUpdate.appVersion}</Text>
|
|
112
|
+
<Text style={styles.infoText}>Hash: {currentUpdate.packageHash.substring(0, 8)}...</Text>
|
|
113
|
+
<Text style={styles.infoText}>Size: {formatBytes(currentUpdate.packageSize)}</Text>
|
|
114
|
+
{currentUpdate.description && (
|
|
115
|
+
<Text style={styles.infoDescription}>{currentUpdate.description}</Text>
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Available Update Info */}
|
|
121
|
+
{availableUpdate && (
|
|
122
|
+
<View style={styles.infoCard}>
|
|
123
|
+
<Text style={styles.cardTitle}>Available Update</Text>
|
|
124
|
+
<Text style={styles.infoText}>Label: {availableUpdate.label}</Text>
|
|
125
|
+
<Text style={styles.infoText}>Version: {availableUpdate.appVersion}</Text>
|
|
126
|
+
<Text style={styles.infoText}>Size: {formatBytes(availableUpdate.packageSize)}</Text>
|
|
127
|
+
<Text style={[
|
|
128
|
+
styles.infoText,
|
|
129
|
+
{ color: availableUpdate.isMandatory ? '#dc3545' : '#28a745' }
|
|
130
|
+
]}>
|
|
131
|
+
{availableUpdate.isMandatory ? 'Mandatory Update' : 'Optional Update'}
|
|
132
|
+
</Text>
|
|
133
|
+
{availableUpdate.description && (
|
|
134
|
+
<Text style={styles.infoDescription}>{availableUpdate.description}</Text>
|
|
135
|
+
)}
|
|
136
|
+
</View>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Action Buttons */}
|
|
140
|
+
<View style={styles.buttonContainer}>
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
style={[styles.button, styles.primaryButton]}
|
|
143
|
+
onPress={checkForUpdate}
|
|
144
|
+
disabled={isChecking}
|
|
145
|
+
>
|
|
146
|
+
<Text style={styles.buttonText}>Check for Updates</Text>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
|
|
149
|
+
{availableUpdate && (
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
style={[styles.button, styles.successButton]}
|
|
152
|
+
onPress={syncUpdate}
|
|
153
|
+
disabled={isDownloading || isInstalling}
|
|
154
|
+
>
|
|
155
|
+
<Text style={styles.buttonText}>
|
|
156
|
+
{availableUpdate.isMandatory ? 'Install Now (Required)' : 'Install Update'}
|
|
157
|
+
</Text>
|
|
158
|
+
</TouchableOpacity>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{currentUpdate && (
|
|
162
|
+
<TouchableOpacity
|
|
163
|
+
style={[styles.button, styles.warningButton]}
|
|
164
|
+
onPress={handleRollback}
|
|
165
|
+
disabled={isDownloading || isInstalling}
|
|
166
|
+
>
|
|
167
|
+
<Text style={styles.buttonText}>Rollback</Text>
|
|
168
|
+
</TouchableOpacity>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<TouchableOpacity
|
|
172
|
+
style={[styles.button, styles.dangerButton]}
|
|
173
|
+
onPress={handleClearUpdates}
|
|
174
|
+
disabled={isDownloading || isInstalling}
|
|
175
|
+
>
|
|
176
|
+
<Text style={styles.buttonText}>Clear All Updates</Text>
|
|
177
|
+
</TouchableOpacity>
|
|
178
|
+
</View>
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const styles = StyleSheet.create({
|
|
184
|
+
container: {
|
|
185
|
+
flex: 1,
|
|
186
|
+
padding: 20,
|
|
187
|
+
backgroundColor: '#f8f9fa',
|
|
188
|
+
},
|
|
189
|
+
header: {
|
|
190
|
+
alignItems: 'center',
|
|
191
|
+
marginBottom: 30,
|
|
192
|
+
},
|
|
193
|
+
title: {
|
|
194
|
+
fontSize: 28,
|
|
195
|
+
fontWeight: 'bold',
|
|
196
|
+
color: '#212529',
|
|
197
|
+
marginBottom: 8,
|
|
198
|
+
},
|
|
199
|
+
subtitle: {
|
|
200
|
+
fontSize: 16,
|
|
201
|
+
color: '#6c757d',
|
|
202
|
+
},
|
|
203
|
+
statusCard: {
|
|
204
|
+
backgroundColor: '#ffffff',
|
|
205
|
+
borderRadius: 8,
|
|
206
|
+
padding: 20,
|
|
207
|
+
marginBottom: 20,
|
|
208
|
+
borderLeftWidth: 4,
|
|
209
|
+
shadowColor: '#000',
|
|
210
|
+
shadowOffset: { width: 0, height: 2 },
|
|
211
|
+
shadowOpacity: 0.1,
|
|
212
|
+
shadowRadius: 4,
|
|
213
|
+
elevation: 3,
|
|
214
|
+
},
|
|
215
|
+
statusHeader: {
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
justifyContent: 'space-between',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
},
|
|
220
|
+
statusText: {
|
|
221
|
+
fontSize: 18,
|
|
222
|
+
fontWeight: '600',
|
|
223
|
+
},
|
|
224
|
+
progressContainer: {
|
|
225
|
+
marginTop: 15,
|
|
226
|
+
flexDirection: 'row',
|
|
227
|
+
alignItems: 'center',
|
|
228
|
+
},
|
|
229
|
+
progressBar: {
|
|
230
|
+
flex: 1,
|
|
231
|
+
height: 8,
|
|
232
|
+
backgroundColor: '#e9ecef',
|
|
233
|
+
borderRadius: 4,
|
|
234
|
+
marginRight: 10,
|
|
235
|
+
},
|
|
236
|
+
progressFill: {
|
|
237
|
+
height: '100%',
|
|
238
|
+
borderRadius: 4,
|
|
239
|
+
},
|
|
240
|
+
progressText: {
|
|
241
|
+
fontSize: 14,
|
|
242
|
+
fontWeight: '500',
|
|
243
|
+
color: '#495057',
|
|
244
|
+
minWidth: 40,
|
|
245
|
+
},
|
|
246
|
+
infoCard: {
|
|
247
|
+
backgroundColor: '#ffffff',
|
|
248
|
+
borderRadius: 8,
|
|
249
|
+
padding: 16,
|
|
250
|
+
marginBottom: 16,
|
|
251
|
+
shadowColor: '#000',
|
|
252
|
+
shadowOffset: { width: 0, height: 1 },
|
|
253
|
+
shadowOpacity: 0.1,
|
|
254
|
+
shadowRadius: 2,
|
|
255
|
+
elevation: 2,
|
|
256
|
+
},
|
|
257
|
+
cardTitle: {
|
|
258
|
+
fontSize: 18,
|
|
259
|
+
fontWeight: '600',
|
|
260
|
+
color: '#212529',
|
|
261
|
+
marginBottom: 12,
|
|
262
|
+
},
|
|
263
|
+
infoText: {
|
|
264
|
+
fontSize: 14,
|
|
265
|
+
color: '#495057',
|
|
266
|
+
marginBottom: 4,
|
|
267
|
+
},
|
|
268
|
+
infoDescription: {
|
|
269
|
+
fontSize: 14,
|
|
270
|
+
color: '#6c757d',
|
|
271
|
+
marginTop: 8,
|
|
272
|
+
fontStyle: 'italic',
|
|
273
|
+
},
|
|
274
|
+
buttonContainer: {
|
|
275
|
+
marginTop: 20,
|
|
276
|
+
},
|
|
277
|
+
button: {
|
|
278
|
+
paddingVertical: 12,
|
|
279
|
+
paddingHorizontal: 20,
|
|
280
|
+
borderRadius: 8,
|
|
281
|
+
marginBottom: 12,
|
|
282
|
+
alignItems: 'center',
|
|
283
|
+
},
|
|
284
|
+
primaryButton: {
|
|
285
|
+
backgroundColor: '#007bff',
|
|
286
|
+
},
|
|
287
|
+
successButton: {
|
|
288
|
+
backgroundColor: '#28a745',
|
|
289
|
+
},
|
|
290
|
+
warningButton: {
|
|
291
|
+
backgroundColor: '#ffc107',
|
|
292
|
+
},
|
|
293
|
+
dangerButton: {
|
|
294
|
+
backgroundColor: '#dc3545',
|
|
295
|
+
},
|
|
296
|
+
buttonText: {
|
|
297
|
+
color: '#ffffff',
|
|
298
|
+
fontSize: 16,
|
|
299
|
+
fontWeight: '600',
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
export default UpdateChecker;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
2
|
+
import { AppState, AppStateStatus } from 'react-native';
|
|
3
|
+
import CustomCodePush, { CodePushConfiguration, UpdatePackage, SyncStatus } from './CustomCodePush';
|
|
4
|
+
|
|
5
|
+
interface CodePushContextType {
|
|
6
|
+
codePush: CustomCodePush | null;
|
|
7
|
+
currentUpdate: UpdatePackage | null;
|
|
8
|
+
availableUpdate: UpdatePackage | null;
|
|
9
|
+
syncStatus: SyncStatus | null;
|
|
10
|
+
isChecking: boolean;
|
|
11
|
+
isDownloading: boolean;
|
|
12
|
+
isInstalling: boolean;
|
|
13
|
+
checkForUpdate: () => Promise<void>;
|
|
14
|
+
syncUpdate: () => Promise<void>;
|
|
15
|
+
rollback: () => Promise<void>;
|
|
16
|
+
clearUpdates: () => Promise<void>;
|
|
17
|
+
getBundleUrl: () => string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CodePushContext = createContext<CodePushContextType | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
interface CodePushProviderProps {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
config: CodePushConfiguration;
|
|
25
|
+
autoCheck?: boolean;
|
|
26
|
+
checkOnResume?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CodePushProvider: React.FC<CodePushProviderProps> = ({
|
|
30
|
+
children,
|
|
31
|
+
config,
|
|
32
|
+
autoCheck = true,
|
|
33
|
+
checkOnResume = true,
|
|
34
|
+
}) => {
|
|
35
|
+
const [codePush] = useState(() => new CustomCodePush(config));
|
|
36
|
+
const [currentUpdate, setCurrentUpdate] = useState<UpdatePackage | null>(null);
|
|
37
|
+
const [availableUpdate, setAvailableUpdate] = useState<UpdatePackage | null>(null);
|
|
38
|
+
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
|
39
|
+
const [isChecking, setIsChecking] = useState(false);
|
|
40
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
41
|
+
const [isInstalling, setIsInstalling] = useState(false);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
// Wait for SDK to initialize (load stored package) before updating state
|
|
45
|
+
codePush.initialize()
|
|
46
|
+
.then(async () => {
|
|
47
|
+
await loadCurrentUpdate();
|
|
48
|
+
if (autoCheck) {
|
|
49
|
+
await checkForUpdate();
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch(error => {
|
|
53
|
+
console.error('Error initializing CodePush SDK:', error);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Set up app state listener for resume checks
|
|
57
|
+
if (checkOnResume) {
|
|
58
|
+
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
59
|
+
if (nextAppState === 'active') {
|
|
60
|
+
checkForUpdate();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
65
|
+
return () => subscription?.remove();
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const loadCurrentUpdate = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const current = await codePush.getCurrentPackage();
|
|
72
|
+
setCurrentUpdate(current);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to load current update:', error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const checkForUpdate = async () => {
|
|
79
|
+
if (isChecking) return;
|
|
80
|
+
|
|
81
|
+
setIsChecking(true);
|
|
82
|
+
try {
|
|
83
|
+
const update = await codePush.checkForUpdate();
|
|
84
|
+
setAvailableUpdate(update);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to check for update:', error);
|
|
87
|
+
} finally {
|
|
88
|
+
setIsChecking(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const syncUpdate = async () => {
|
|
93
|
+
if (!availableUpdate || isDownloading || isInstalling) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const success = await codePush.sync(
|
|
97
|
+
{
|
|
98
|
+
installMode: 'ON_NEXT_RESTART',
|
|
99
|
+
mandatoryInstallMode: 'ON_NEXT_RESTART',
|
|
100
|
+
},
|
|
101
|
+
(status) => {
|
|
102
|
+
setSyncStatus(status);
|
|
103
|
+
|
|
104
|
+
switch (status.status) {
|
|
105
|
+
case 'DOWNLOADING_PACKAGE':
|
|
106
|
+
setIsDownloading(true);
|
|
107
|
+
setIsInstalling(false);
|
|
108
|
+
break;
|
|
109
|
+
case 'INSTALLING_UPDATE':
|
|
110
|
+
setIsDownloading(false);
|
|
111
|
+
setIsInstalling(true);
|
|
112
|
+
break;
|
|
113
|
+
case 'UPDATE_INSTALLED':
|
|
114
|
+
case 'UP_TO_DATE':
|
|
115
|
+
case 'UNKNOWN_ERROR':
|
|
116
|
+
setIsDownloading(false);
|
|
117
|
+
setIsInstalling(false);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
(progress) => {
|
|
122
|
+
// Handle download progress if needed
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (success) {
|
|
127
|
+
setAvailableUpdate(null);
|
|
128
|
+
await loadCurrentUpdate();
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Failed to sync update:', error);
|
|
132
|
+
setIsDownloading(false);
|
|
133
|
+
setIsInstalling(false);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const rollback = async () => {
|
|
138
|
+
try {
|
|
139
|
+
await codePush.rollback();
|
|
140
|
+
setCurrentUpdate(null);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Failed to rollback:', error);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const clearUpdates = async () => {
|
|
147
|
+
try {
|
|
148
|
+
await codePush.clearUpdates();
|
|
149
|
+
setCurrentUpdate(null);
|
|
150
|
+
setAvailableUpdate(null);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Failed to clear updates:', error);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const contextValue: CodePushContextType = {
|
|
157
|
+
codePush,
|
|
158
|
+
currentUpdate,
|
|
159
|
+
availableUpdate,
|
|
160
|
+
syncStatus,
|
|
161
|
+
isChecking,
|
|
162
|
+
isDownloading,
|
|
163
|
+
isInstalling,
|
|
164
|
+
checkForUpdate,
|
|
165
|
+
syncUpdate,
|
|
166
|
+
rollback,
|
|
167
|
+
clearUpdates,
|
|
168
|
+
getBundleUrl: () => codePush.getBundleUrl(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<CodePushContext.Provider value={contextValue}>
|
|
173
|
+
{children}
|
|
174
|
+
</CodePushContext.Provider>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const useCodePush = (): CodePushContextType => {
|
|
179
|
+
const context = useContext(CodePushContext);
|
|
180
|
+
if (context === undefined) {
|
|
181
|
+
throw new Error('useCodePush must be used within a CodePushProvider');
|
|
182
|
+
}
|
|
183
|
+
return context;
|
|
184
|
+
};
|