@capgo/capacitor-updater 7.0.18 → 7.0.25
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/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +96 -23
- package/ios/Plugin/CapacitorUpdater.swift +49 -12
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +1 -1
- package/package.json +1 -1
|
@@ -235,6 +235,7 @@ public class CapacitorUpdater {
|
|
|
235
235
|
|
|
236
236
|
boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
|
|
237
237
|
if (!success) {
|
|
238
|
+
Log.e(TAG, "Finish download failed: " + version);
|
|
238
239
|
saveBundleInfo(
|
|
239
240
|
id,
|
|
240
241
|
new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
|
|
@@ -249,6 +250,7 @@ public class CapacitorUpdater {
|
|
|
249
250
|
case FAILED:
|
|
250
251
|
Data failedData = workInfo.getOutputData();
|
|
251
252
|
String error = failedData.getString(DownloadService.ERROR);
|
|
253
|
+
Log.e(TAG, "Download failed: " + error + " " + workInfo.getState());
|
|
252
254
|
String failedVersion = failedData.getString(DownloadService.VERSION);
|
|
253
255
|
saveBundleInfo(
|
|
254
256
|
id,
|
|
@@ -57,7 +57,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
57
57
|
private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
|
|
58
58
|
private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
|
|
59
59
|
|
|
60
|
-
private final String PLUGIN_VERSION = "7.0.
|
|
60
|
+
private final String PLUGIN_VERSION = "7.0.25";
|
|
61
61
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
62
62
|
|
|
63
63
|
private SharedPreferences.Editor editor;
|
|
@@ -16,6 +16,7 @@ import java.io.FileInputStream;
|
|
|
16
16
|
import java.net.HttpURLConnection;
|
|
17
17
|
import java.net.URL;
|
|
18
18
|
import java.nio.channels.FileChannel;
|
|
19
|
+
import java.nio.file.Files;
|
|
19
20
|
import java.security.MessageDigest;
|
|
20
21
|
import java.util.ArrayList;
|
|
21
22
|
import java.util.Arrays;
|
|
@@ -109,7 +110,6 @@ public class DownloadService extends Worker {
|
|
|
109
110
|
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
110
111
|
}
|
|
111
112
|
} catch (Exception e) {
|
|
112
|
-
Log.e(TAG, "Error in doWork", e);
|
|
113
113
|
return createFailureResult(e.getMessage());
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -163,25 +163,38 @@ public class DownloadService extends Worker {
|
|
|
163
163
|
String fileHash = entry.getString("file_hash");
|
|
164
164
|
String downloadUrl = entry.getString("download_url");
|
|
165
165
|
|
|
166
|
+
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
167
|
+
try {
|
|
168
|
+
fileHash = CryptoCipherV2.decryptChecksum(fileHash, publicKey);
|
|
169
|
+
} catch (Exception e) {
|
|
170
|
+
Log.e(TAG, "Error decrypting checksum for " + fileName, e);
|
|
171
|
+
hasError.set(true);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
final String finalFileHash = fileHash;
|
|
166
177
|
File targetFile = new File(destFolder, fileName);
|
|
167
|
-
File cacheFile = new File(cacheFolder,
|
|
178
|
+
File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
|
|
168
179
|
File builtinFile = new File(builtinFolder, fileName);
|
|
169
180
|
|
|
170
181
|
// Ensure parent directories of the target file exist
|
|
171
182
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
172
|
-
|
|
183
|
+
Log.e(TAG, "Failed to create parent directory for: " + targetFile.getAbsolutePath());
|
|
184
|
+
hasError.set(true);
|
|
185
|
+
continue;
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
Future<?> future = executor.submit(() -> {
|
|
176
189
|
try {
|
|
177
|
-
if (builtinFile.exists() && verifyChecksum(builtinFile,
|
|
190
|
+
if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
|
|
178
191
|
copyFile(builtinFile, targetFile);
|
|
179
192
|
Log.d(TAG, "using builtin file " + fileName);
|
|
180
|
-
} else if (cacheFile.exists() && verifyChecksum(cacheFile,
|
|
193
|
+
} else if (cacheFile.exists() && verifyChecksum(cacheFile, finalFileHash)) {
|
|
181
194
|
copyFile(cacheFile, targetFile);
|
|
182
195
|
Log.d(TAG, "already cached " + fileName);
|
|
183
196
|
} else {
|
|
184
|
-
downloadAndVerify(downloadUrl, targetFile, cacheFile,
|
|
197
|
+
downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey);
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
long completed = completedFiles.incrementAndGet();
|
|
@@ -216,10 +229,12 @@ public class DownloadService extends Worker {
|
|
|
216
229
|
}
|
|
217
230
|
|
|
218
231
|
if (hasError.get()) {
|
|
232
|
+
Log.e(TAG, "One or more files failed to download");
|
|
219
233
|
throw new IOException("One or more files failed to download");
|
|
220
234
|
}
|
|
221
235
|
} catch (Exception e) {
|
|
222
236
|
Log.e(TAG, "Error in handleManifestDownload", e);
|
|
237
|
+
throw new RuntimeException(e.getLocalizedMessage());
|
|
223
238
|
}
|
|
224
239
|
}
|
|
225
240
|
|
|
@@ -317,10 +332,8 @@ public class DownloadService extends Worker {
|
|
|
317
332
|
}
|
|
318
333
|
}
|
|
319
334
|
} catch (OutOfMemoryError e) {
|
|
320
|
-
e.printStackTrace();
|
|
321
335
|
throw new RuntimeException("low_mem_fail");
|
|
322
336
|
} catch (Exception e) {
|
|
323
|
-
e.printStackTrace();
|
|
324
337
|
throw new RuntimeException(e.getLocalizedMessage());
|
|
325
338
|
}
|
|
326
339
|
}
|
|
@@ -334,7 +347,8 @@ public class DownloadService extends Worker {
|
|
|
334
347
|
infoFile.createNewFile();
|
|
335
348
|
tempFile.createNewFile();
|
|
336
349
|
} catch (IOException e) {
|
|
337
|
-
e
|
|
350
|
+
Log.e(TAG, "Error in clearDownloadData", e);
|
|
351
|
+
// not a fatal error, so we don't throw an exception
|
|
338
352
|
}
|
|
339
353
|
}
|
|
340
354
|
|
|
@@ -394,18 +408,10 @@ public class DownloadService extends Worker {
|
|
|
394
408
|
decryptedExpectedHash = CryptoCipherV2.decryptChecksum(decryptedExpectedHash, publicKey);
|
|
395
409
|
}
|
|
396
410
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
FileOutputStream fos = new FileOutputStream(targetFile)
|
|
402
|
-
) {
|
|
403
|
-
byte[] buffer = new byte[8192];
|
|
404
|
-
int len;
|
|
405
|
-
while ((len = brotliInputStream.read(buffer)) != -1) {
|
|
406
|
-
fos.write(buffer, 0, len);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
411
|
+
// Use new decompression method
|
|
412
|
+
byte[] compressedData = Files.readAllBytes(compressedFile.toPath());
|
|
413
|
+
byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
414
|
+
Files.write(targetFile.toPath(), decompressedData);
|
|
409
415
|
|
|
410
416
|
// Delete the compressed file
|
|
411
417
|
compressedFile.delete();
|
|
@@ -417,10 +423,18 @@ public class DownloadService extends Worker {
|
|
|
417
423
|
copyFile(targetFile, cacheFile);
|
|
418
424
|
} else {
|
|
419
425
|
targetFile.delete();
|
|
420
|
-
throw new IOException(
|
|
426
|
+
throw new IOException(
|
|
427
|
+
"Checksum verification failed for: " +
|
|
428
|
+
downloadUrl +
|
|
429
|
+
" " +
|
|
430
|
+
targetFile.getName() +
|
|
431
|
+
" expected: " +
|
|
432
|
+
decryptedExpectedHash +
|
|
433
|
+
" calculated: " +
|
|
434
|
+
calculatedHash
|
|
435
|
+
);
|
|
421
436
|
}
|
|
422
437
|
} catch (Exception e) {
|
|
423
|
-
Log.e(TAG, "Error in downloadAndVerify", e);
|
|
424
438
|
throw new IOException("Error in downloadAndVerify: " + e.getMessage());
|
|
425
439
|
}
|
|
426
440
|
}
|
|
@@ -453,4 +467,63 @@ public class DownloadService extends Worker {
|
|
|
453
467
|
}
|
|
454
468
|
return sb.toString();
|
|
455
469
|
}
|
|
470
|
+
|
|
471
|
+
private byte[] decompressBrotli(byte[] data, String fileName) throws IOException {
|
|
472
|
+
// Validate input
|
|
473
|
+
if (data == null) {
|
|
474
|
+
Log.e(TAG, "Error: Null data received for " + fileName);
|
|
475
|
+
throw new IOException("Null data received");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Handle empty files
|
|
479
|
+
if (data.length == 0) {
|
|
480
|
+
return new byte[0];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Handle the special EMPTY_BROTLI_STREAM case
|
|
484
|
+
if (data.length == 3 && data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06) {
|
|
485
|
+
return new byte[0];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// For small files, check if it's a minimal Brotli wrapper
|
|
489
|
+
if (data.length > 3) {
|
|
490
|
+
try {
|
|
491
|
+
// Handle our minimal wrapper pattern
|
|
492
|
+
if (data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data[data.length - 1] == 0x03) {
|
|
493
|
+
return Arrays.copyOfRange(data, 3, data.length - 1);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Handle brotli.compress minimal wrapper (quality 0)
|
|
497
|
+
if (data[0] == 0x0b && data[1] == 0x02 && data[2] == (byte) 0x80 && data[data.length - 1] == 0x03) {
|
|
498
|
+
return Arrays.copyOfRange(data, 3, data.length - 1);
|
|
499
|
+
}
|
|
500
|
+
} catch (ArrayIndexOutOfBoundsException e) {
|
|
501
|
+
Log.e(TAG, "Error: Malformed data for " + fileName);
|
|
502
|
+
throw new IOException("Malformed data structure");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// For all other cases, try standard decompression
|
|
507
|
+
try (
|
|
508
|
+
ByteArrayInputStream bis = new ByteArrayInputStream(data);
|
|
509
|
+
BrotliInputStream brotliInputStream = new BrotliInputStream(bis);
|
|
510
|
+
ByteArrayOutputStream bos = new ByteArrayOutputStream()
|
|
511
|
+
) {
|
|
512
|
+
byte[] buffer = new byte[8192];
|
|
513
|
+
int len;
|
|
514
|
+
while ((len = brotliInputStream.read(buffer)) != -1) {
|
|
515
|
+
bos.write(buffer, 0, len);
|
|
516
|
+
}
|
|
517
|
+
return bos.toByteArray();
|
|
518
|
+
} catch (IOException e) {
|
|
519
|
+
Log.e(TAG, "Error: Brotli process failed for " + fileName + ". Status: " + e.getMessage());
|
|
520
|
+
// Add hex dump for debugging
|
|
521
|
+
StringBuilder hexDump = new StringBuilder();
|
|
522
|
+
for (int i = 0; i < Math.min(32, data.length); i++) {
|
|
523
|
+
hexDump.append(String.format("%02x ", data[i]));
|
|
524
|
+
}
|
|
525
|
+
Log.e(TAG, "Error: Raw data (" + fileName + "): " + hexDump.toString());
|
|
526
|
+
throw e;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
456
529
|
}
|
|
@@ -347,7 +347,7 @@ import UIKit
|
|
|
347
347
|
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("update.dat")
|
|
348
348
|
}
|
|
349
349
|
private var tempData = Data()
|
|
350
|
-
|
|
350
|
+
|
|
351
351
|
private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
|
|
352
352
|
let actualHash = CryptoCipherV2.calcChecksum(filePath: file)
|
|
353
353
|
return actualHash == expectedHash
|
|
@@ -446,9 +446,9 @@ import UIKit
|
|
|
446
446
|
let statusCode = response.response?.statusCode ?? 200
|
|
447
447
|
if statusCode < 200 || statusCode >= 300 {
|
|
448
448
|
if let stringData = String(data: data, encoding: .utf8) {
|
|
449
|
-
throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData)"])
|
|
449
|
+
throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData) for file \(fileName) at url \(downloadUrl)"])
|
|
450
450
|
} else {
|
|
451
|
-
throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid"])
|
|
451
|
+
throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid for file \(fileName) at url \(downloadUrl)"])
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
@@ -470,7 +470,7 @@ import UIKit
|
|
|
470
470
|
|
|
471
471
|
// Decompress the Brotli data
|
|
472
472
|
guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
|
|
473
|
-
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
|
|
473
|
+
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
|
|
474
474
|
}
|
|
475
475
|
finalData = decompressedData
|
|
476
476
|
|
|
@@ -479,7 +479,7 @@ import UIKit
|
|
|
479
479
|
// assume that calcChecksum != null
|
|
480
480
|
let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
|
|
481
481
|
if calculatedChecksum != fileHash {
|
|
482
|
-
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash))"])
|
|
482
|
+
throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
|
|
483
483
|
}
|
|
484
484
|
}
|
|
485
485
|
|
|
@@ -491,10 +491,10 @@ import UIKit
|
|
|
491
491
|
print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
|
|
492
492
|
} catch {
|
|
493
493
|
downloadError = error
|
|
494
|
-
|
|
494
|
+
NSLog("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error.localizedDescription)")
|
|
495
495
|
}
|
|
496
496
|
case .failure(let error):
|
|
497
|
-
|
|
497
|
+
NSLog("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error.localizedDescription). Debug response: \(response.debugDescription).")
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
}
|
|
@@ -518,14 +518,43 @@ import UIKit
|
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
private func decompressBrotli(data: Data, fileName: String) -> Data? {
|
|
521
|
+
// Handle empty files
|
|
522
|
+
if data.count == 0 {
|
|
523
|
+
return data
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Handle the special EMPTY_BROTLI_STREAM case
|
|
527
|
+
if data.count == 3 && data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 {
|
|
528
|
+
return Data()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// For small files, check if it's a minimal Brotli wrapper
|
|
532
|
+
if data.count > 3 {
|
|
533
|
+
let maxBytes = min(32, data.count)
|
|
534
|
+
let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
535
|
+
// Handle our minimal wrapper pattern
|
|
536
|
+
if data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data.last == 0x03 {
|
|
537
|
+
let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
|
|
538
|
+
return data[range]
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Handle brotli.compress minimal wrapper (quality 0)
|
|
542
|
+
if data[0] == 0x0b && data[1] == 0x02 && data[2] == 0x80 && data.last == 0x03 {
|
|
543
|
+
let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
|
|
544
|
+
return data[range]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// For all other cases, try standard decompression
|
|
521
549
|
let outputBufferSize = 65536
|
|
522
550
|
var outputBuffer = [UInt8](repeating: 0, count: outputBufferSize)
|
|
523
551
|
var decompressedData = Data()
|
|
524
552
|
|
|
525
553
|
let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
|
|
526
554
|
var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
|
|
555
|
+
|
|
527
556
|
guard status != COMPRESSION_STATUS_ERROR else {
|
|
528
|
-
print("\(CapacitorUpdater.TAG)
|
|
557
|
+
print("\(CapacitorUpdater.TAG) Error: Failed to initialize Brotli stream for \(fileName). Status: \(status)")
|
|
529
558
|
return nil
|
|
530
559
|
}
|
|
531
560
|
|
|
@@ -547,7 +576,7 @@ import UIKit
|
|
|
547
576
|
if let baseAddress = rawBufferPointer.baseAddress {
|
|
548
577
|
streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
549
578
|
} else {
|
|
550
|
-
print("\(CapacitorUpdater.TAG) Error:
|
|
579
|
+
print("\(CapacitorUpdater.TAG) Error: Failed to get base address for \(fileName)")
|
|
551
580
|
status = COMPRESSION_STATUS_ERROR
|
|
552
581
|
return
|
|
553
582
|
}
|
|
@@ -555,6 +584,9 @@ import UIKit
|
|
|
555
584
|
}
|
|
556
585
|
|
|
557
586
|
if status == COMPRESSION_STATUS_ERROR {
|
|
587
|
+
let maxBytes = min(32, data.count)
|
|
588
|
+
let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
589
|
+
print("\(CapacitorUpdater.TAG) Error: Brotli decompression failed for \(fileName). First \(maxBytes) bytes: \(hexDump)")
|
|
558
590
|
break
|
|
559
591
|
}
|
|
560
592
|
|
|
@@ -568,15 +600,19 @@ import UIKit
|
|
|
568
600
|
if status == COMPRESSION_STATUS_END {
|
|
569
601
|
break
|
|
570
602
|
} else if status == COMPRESSION_STATUS_ERROR {
|
|
571
|
-
print("\(CapacitorUpdater.TAG) Error
|
|
572
|
-
// Try to decode as text if mostly ASCII
|
|
603
|
+
print("\(CapacitorUpdater.TAG) Error: Brotli process failed for \(fileName). Status: \(status)")
|
|
573
604
|
if let text = String(data: data, encoding: .utf8) {
|
|
574
605
|
let asciiCount = text.unicodeScalars.filter { $0.isASCII }.count
|
|
575
606
|
let totalCount = text.unicodeScalars.count
|
|
576
607
|
if totalCount > 0 && Double(asciiCount) / Double(totalCount) >= 0.8 {
|
|
577
|
-
print("\(CapacitorUpdater.TAG)
|
|
608
|
+
print("\(CapacitorUpdater.TAG) Error: Input appears to be plain text: \(text)")
|
|
578
609
|
}
|
|
579
610
|
}
|
|
611
|
+
|
|
612
|
+
let maxBytes = min(32, data.count)
|
|
613
|
+
let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
614
|
+
print("\(CapacitorUpdater.TAG) Error: Raw data (\(fileName)): \(hexDump)")
|
|
615
|
+
|
|
580
616
|
return nil
|
|
581
617
|
}
|
|
582
618
|
|
|
@@ -586,6 +622,7 @@ import UIKit
|
|
|
586
622
|
}
|
|
587
623
|
|
|
588
624
|
if input.count == 0 {
|
|
625
|
+
print("\(CapacitorUpdater.TAG) Error: Zero input size for \(fileName)")
|
|
589
626
|
break
|
|
590
627
|
}
|
|
591
628
|
}
|
|
@@ -45,7 +45,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
45
45
|
CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise)
|
|
46
46
|
]
|
|
47
47
|
public var implementation = CapacitorUpdater()
|
|
48
|
-
private let PLUGIN_VERSION: String = "7.0.
|
|
48
|
+
private let PLUGIN_VERSION: String = "7.0.25"
|
|
49
49
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
50
50
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
51
51
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|