@capgo/capacitor-updater 7.23.1 → 7.23.13

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 CHANGED
@@ -16,8 +16,8 @@
16
16
  [![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2FCapgo%2Fbounties%3Fstatus%3Dcompleted)](https://console.algora.io/org/Capgo/bounties?status=completed)
17
17
 
18
18
  <div align="center">
19
- <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
20
- <h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
19
+ <h2><a href="https://capgo.app/?ref=plugin_updater_v7"> ➡️ Get Instant updates for your App with Capgo</a></h2>
20
+ <h2><a href="https://capgo.app/consulting/?ref=plugin_updater_v7"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
21
21
  </div>
22
22
 
23
23
  Capacitor plugin to update your app remotely in real-time.
@@ -71,7 +71,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
71
71
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
72
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
73
73
 
74
- private final String PLUGIN_VERSION = "7.23.1";
74
+ private final String PLUGIN_VERSION = "7.23.13";
75
75
  private static final String DELAY_CONDITION_PREFERENCES = "";
76
76
 
77
77
  private SharedPreferences.Editor editor;
@@ -81,7 +81,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
81
81
  private Boolean persistModifyUrl = false;
82
82
 
83
83
  private Integer appReadyTimeout = 10000;
84
- private Integer counterActivityCreate = 0;
85
84
  private Integer periodCheckDelay = 0;
86
85
  private Boolean autoDeleteFailed = true;
87
86
  private Boolean autoDeletePrevious = true;
@@ -190,7 +189,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
190
189
  @Override
191
190
  public void load() {
192
191
  super.load();
193
- this.counterActivityCreate++;
194
192
  this.prefs = this.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
195
193
  this.editor = this.prefs.edit();
196
194
 
@@ -276,9 +274,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
276
274
  }
277
275
  logger.info("appId: " + implementation.appId);
278
276
 
279
- // Update User-Agent for shared OkHttpClient
280
- DownloadService.updateUserAgent(this.implementation.appId, this.PLUGIN_VERSION);
281
-
282
277
  this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
283
278
  this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
284
279
  this.implementation.publicKey = this.getConfig().getString("publicKey", "");
@@ -316,6 +311,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
316
311
  this.implementation.deviceID = this.prefs.getString("appUUID", UUID.randomUUID().toString()).toLowerCase();
317
312
  this.editor.putString("appUUID", this.implementation.deviceID);
318
313
  this.editor.apply();
314
+
315
+ // Update User-Agent for shared OkHttpClient with OS version
316
+ DownloadService.updateUserAgent(this.implementation.appId, this.PLUGIN_VERSION, this.implementation.versionOs);
317
+
319
318
  if (Boolean.TRUE.equals(this.persistCustomId)) {
320
319
  final String storedCustomId = this.prefs.getString(CUSTOM_ID_PREF_KEY, "");
321
320
  if (storedCustomId != null && !storedCustomId.isEmpty()) {
@@ -357,6 +356,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
357
356
  if (resetWhenUpdate) {
358
357
  this.cleanupObsoleteVersions();
359
358
  }
359
+
360
+ // Check for 'kill' delay condition on app launch
361
+ // This handles cases where the app was killed by the system (onDestroy is not reliable)
362
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
363
+
360
364
  this.checkForUpdateAfterDelay();
361
365
  }
362
366
 
@@ -1910,6 +1914,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
1910
1914
  ) {
1911
1915
  this.backgroundDownloadTask = this.backgroundDownload();
1912
1916
  } else {
1917
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
1918
+ String serverUrl = config.getServerUrl();
1919
+ if (serverUrl != null && !serverUrl.isEmpty()) {
1920
+ CapacitorUpdaterPlugin.this.implementation.sendStats("blocked_by_server_url", current.getVersionName());
1921
+ }
1913
1922
  logger.info("Auto update is disabled");
1914
1923
  this.sendReadyToJs(current, "disabled");
1915
1924
  }
@@ -1980,11 +1989,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
1980
1989
  }
1981
1990
  }
1982
1991
 
1983
- private void appKilled() {
1984
- logger.debug("onActivityDestroyed: all activity destroyed");
1985
- this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
1986
- }
1987
-
1988
1992
  @Override
1989
1993
  public void handleOnStart() {
1990
1994
  try {
@@ -2048,10 +2052,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2048
2052
  try {
2049
2053
  logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2050
2054
  this.implementation.activity = getActivity();
2051
- counterActivityCreate--;
2052
- if (counterActivityCreate == 0) {
2053
- this.appKilled();
2054
- }
2055
2055
 
2056
2056
  // Clean up shake menu
2057
2057
  if (shakeMenu != null) {
@@ -81,6 +81,12 @@ public class CapgoUpdater {
81
81
  public String deviceID = "";
82
82
  public int timeout = 20000;
83
83
 
84
+ // Flag to track if we received a 429 response - stops requests until app restart
85
+ private static volatile boolean rateLimitExceeded = false;
86
+
87
+ // Flag to track if we've already sent the rate limit statistic - prevents infinite loop
88
+ private static volatile boolean rateLimitStatisticSent = false;
89
+
84
90
  private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
85
91
  private final ExecutorService io = Executors.newSingleThreadExecutor();
86
92
 
@@ -776,6 +782,59 @@ public class CapgoUpdater {
776
782
  return json;
777
783
  }
778
784
 
785
+ /**
786
+ * Check if a 429 (Too Many Requests) response was received and set the flag
787
+ */
788
+ private boolean checkAndHandleRateLimitResponse(Response response) {
789
+ if (response.code() == 429) {
790
+ // Send a statistic about the rate limit BEFORE setting the flag
791
+ // Only send once to prevent infinite loop if the stat request itself gets rate limited
792
+ if (!rateLimitExceeded && !rateLimitStatisticSent) {
793
+ rateLimitStatisticSent = true;
794
+ sendRateLimitStatistic();
795
+ }
796
+ rateLimitExceeded = true;
797
+ logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.");
798
+ return true;
799
+ }
800
+ return false;
801
+ }
802
+
803
+ /**
804
+ * Send a synchronous statistic about rate limiting
805
+ */
806
+ private void sendRateLimitStatistic() {
807
+ String statsUrl = this.statsUrl;
808
+ if (statsUrl == null || statsUrl.isEmpty()) {
809
+ return;
810
+ }
811
+
812
+ try {
813
+ BundleInfo current = this.getCurrentBundle();
814
+ JSONObject json = this.createInfoObject();
815
+ json.put("version_name", current.getVersionName());
816
+ json.put("old_version_name", "");
817
+ json.put("action", "rate_limit_reached");
818
+
819
+ Request request = new Request.Builder()
820
+ .url(statsUrl)
821
+ .post(RequestBody.create(json.toString(), MediaType.get("application/json")))
822
+ .build();
823
+
824
+ // Send synchronously to ensure it goes out before the flag is set
825
+ // User-Agent header is automatically added by DownloadService.sharedClient interceptor
826
+ try (Response response = DownloadService.sharedClient.newCall(request).execute()) {
827
+ if (response.isSuccessful()) {
828
+ logger.info("Rate limit statistic sent");
829
+ } else {
830
+ logger.error("Error sending rate limit statistic: " + response.code());
831
+ }
832
+ }
833
+ } catch (final Exception e) {
834
+ logger.error("Failed to send rate limit statistic: " + e.getMessage());
835
+ }
836
+ }
837
+
779
838
  private void makeJsonRequest(String url, JSONObject jsonBody, Callback callback) {
780
839
  MediaType JSON = MediaType.get("application/json; charset=utf-8");
781
840
  RequestBody body = RequestBody.create(jsonBody.toString(), JSON);
@@ -797,6 +856,15 @@ public class CapgoUpdater {
797
856
  @Override
798
857
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
799
858
  try (ResponseBody responseBody = response.body()) {
859
+ // Check for 429 rate limit
860
+ if (checkAndHandleRateLimitResponse(response)) {
861
+ Map<String, Object> retError = new HashMap<>();
862
+ retError.put("message", "Rate limit exceeded");
863
+ retError.put("error", "rate_limit_exceeded");
864
+ callback.callback(retError);
865
+ return;
866
+ }
867
+
800
868
  if (!response.isSuccessful()) {
801
869
  Map<String, Object> retError = new HashMap<>();
802
870
  retError.put("message", "Server error: " + response.code());
@@ -865,6 +933,16 @@ public class CapgoUpdater {
865
933
  }
866
934
 
867
935
  public void unsetChannel(final Callback callback) {
936
+ // Check if rate limit was exceeded
937
+ if (rateLimitExceeded) {
938
+ logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.");
939
+ final Map<String, Object> retError = new HashMap<>();
940
+ retError.put("message", "Rate limit exceeded");
941
+ retError.put("error", "rate_limit_exceeded");
942
+ callback.callback(retError);
943
+ return;
944
+ }
945
+
868
946
  String channelUrl = this.channelUrl;
869
947
  if (channelUrl == null || channelUrl.isEmpty()) {
870
948
  logger.error("Channel URL is not set");
@@ -906,6 +984,15 @@ public class CapgoUpdater {
906
984
  @Override
907
985
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
908
986
  try (ResponseBody responseBody = response.body()) {
987
+ // Check for 429 rate limit
988
+ if (checkAndHandleRateLimitResponse(response)) {
989
+ Map<String, Object> retError = new HashMap<>();
990
+ retError.put("message", "Rate limit exceeded");
991
+ retError.put("error", "rate_limit_exceeded");
992
+ callback.callback(retError);
993
+ return;
994
+ }
995
+
909
996
  if (!response.isSuccessful()) {
910
997
  Map<String, Object> retError = new HashMap<>();
911
998
  retError.put("message", "Server error: " + response.code());
@@ -950,6 +1037,16 @@ public class CapgoUpdater {
950
1037
  }
951
1038
 
952
1039
  public void setChannel(final String channel, final Callback callback) {
1040
+ // Check if rate limit was exceeded
1041
+ if (rateLimitExceeded) {
1042
+ logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.");
1043
+ final Map<String, Object> retError = new HashMap<>();
1044
+ retError.put("message", "Rate limit exceeded");
1045
+ retError.put("error", "rate_limit_exceeded");
1046
+ callback.callback(retError);
1047
+ return;
1048
+ }
1049
+
953
1050
  String channelUrl = this.channelUrl;
954
1051
  if (channelUrl == null || channelUrl.isEmpty()) {
955
1052
  logger.error("Channel URL is not set");
@@ -976,6 +1073,16 @@ public class CapgoUpdater {
976
1073
  }
977
1074
 
978
1075
  public void getChannel(final Callback callback) {
1076
+ // Check if rate limit was exceeded
1077
+ if (rateLimitExceeded) {
1078
+ logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.");
1079
+ final Map<String, Object> retError = new HashMap<>();
1080
+ retError.put("message", "Rate limit exceeded");
1081
+ retError.put("error", "rate_limit_exceeded");
1082
+ callback.callback(retError);
1083
+ return;
1084
+ }
1085
+
979
1086
  String channelUrl = this.channelUrl;
980
1087
  if (channelUrl == null || channelUrl.isEmpty()) {
981
1088
  logger.error("Channel URL is not set");
@@ -1017,6 +1124,15 @@ public class CapgoUpdater {
1017
1124
  @Override
1018
1125
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1019
1126
  try (ResponseBody responseBody = response.body()) {
1127
+ // Check for 429 rate limit
1128
+ if (checkAndHandleRateLimitResponse(response)) {
1129
+ Map<String, Object> retError = new HashMap<>();
1130
+ retError.put("message", "Rate limit exceeded");
1131
+ retError.put("error", "rate_limit_exceeded");
1132
+ callback.callback(retError);
1133
+ return;
1134
+ }
1135
+
1020
1136
  if (response.code() == 400) {
1021
1137
  assert responseBody != null;
1022
1138
  String data = responseBody.string();
@@ -1074,6 +1190,16 @@ public class CapgoUpdater {
1074
1190
  }
1075
1191
 
1076
1192
  public void listChannels(final Callback callback) {
1193
+ // Check if rate limit was exceeded
1194
+ if (rateLimitExceeded) {
1195
+ logger.debug("Skipping listChannels due to rate limit (429). Requests will resume after app restart.");
1196
+ final Map<String, Object> retError = new HashMap<>();
1197
+ retError.put("message", "Rate limit exceeded");
1198
+ retError.put("error", "rate_limit_exceeded");
1199
+ callback.callback(retError);
1200
+ return;
1201
+ }
1202
+
1077
1203
  String channelUrl = this.channelUrl;
1078
1204
  if (channelUrl == null || channelUrl.isEmpty()) {
1079
1205
  logger.error("Channel URL is not set");
@@ -1114,6 +1240,15 @@ public class CapgoUpdater {
1114
1240
  @Override
1115
1241
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1116
1242
  try (ResponseBody responseBody = response.body()) {
1243
+ // Check for 429 rate limit
1244
+ if (checkAndHandleRateLimitResponse(response)) {
1245
+ Map<String, Object> retError = new HashMap<>();
1246
+ retError.put("message", "Rate limit exceeded");
1247
+ retError.put("error", "rate_limit_exceeded");
1248
+ callback.callback(retError);
1249
+ return;
1250
+ }
1251
+
1117
1252
  if (!response.isSuccessful()) {
1118
1253
  Map<String, Object> retError = new HashMap<>();
1119
1254
  retError.put("message", "Server error: " + response.code());
@@ -1185,6 +1320,12 @@ public class CapgoUpdater {
1185
1320
  }
1186
1321
 
1187
1322
  public void sendStats(final String action, final String versionName, final String oldVersionName) {
1323
+ // Check if rate limit was exceeded
1324
+ if (rateLimitExceeded) {
1325
+ logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.");
1326
+ return;
1327
+ }
1328
+
1188
1329
  String statsUrl = this.statsUrl;
1189
1330
  if (statsUrl == null || statsUrl.isEmpty()) {
1190
1331
  return;
@@ -1217,6 +1358,11 @@ public class CapgoUpdater {
1217
1358
  @Override
1218
1359
  public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1219
1360
  try (ResponseBody responseBody = response.body()) {
1361
+ // Check for 429 rate limit
1362
+ if (checkAndHandleRateLimitResponse(response)) {
1363
+ return;
1364
+ }
1365
+
1220
1366
  if (response.isSuccessful()) {
1221
1367
  logger.info("Stats send for \"" + action + "\", version " + versionName);
1222
1368
  } else {
@@ -70,6 +70,7 @@ public class DownloadService extends Worker {
70
70
  protected static OkHttpClient sharedClient;
71
71
  private static String currentAppId = "unknown";
72
72
  private static String currentPluginVersion = "unknown";
73
+ private static String currentVersionOs = "unknown";
73
74
 
74
75
  // Initialize shared client with User-Agent interceptor
75
76
  static {
@@ -82,7 +83,8 @@ public class DownloadService extends Worker {
82
83
  (currentPluginVersion != null ? currentPluginVersion : "unknown") +
83
84
  " (" +
84
85
  (currentAppId != null ? currentAppId : "unknown") +
85
- ")";
86
+ ") android/" +
87
+ (currentVersionOs != null ? currentVersionOs : "unknown");
86
88
  Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
87
89
  return chain.proceed(requestWithUserAgent);
88
90
  })
@@ -90,10 +92,13 @@ public class DownloadService extends Worker {
90
92
  }
91
93
 
92
94
  // Method to update User-Agent values
93
- public static void updateUserAgent(String appId, String pluginVersion) {
95
+ public static void updateUserAgent(String appId, String pluginVersion, String versionOs) {
94
96
  currentAppId = appId != null ? appId : "unknown";
95
97
  currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
96
- logger.debug("Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ")");
98
+ currentVersionOs = versionOs != null ? versionOs : "unknown";
99
+ logger.debug(
100
+ "Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ") android/" + currentVersionOs
101
+ );
97
102
  }
98
103
 
99
104
  public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
@@ -54,7 +54,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
54
54
  CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
55
55
  ]
56
56
  public var implementation = CapgoUpdater()
57
- private let PLUGIN_VERSION: String = "7.23.1"
57
+ private let PLUGIN_VERSION: String = "7.23.13"
58
58
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
59
59
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
60
60
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -236,7 +236,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
236
236
  let nc = NotificationCenter.default
237
237
  nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
238
238
  nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
239
- nc.addObserver(self, selector: #selector(appKilled), name: UIApplication.willTerminateNotification, object: nil)
239
+
240
+ // Check for 'kill' delay condition on app launch
241
+ // This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
242
+ self.delayUpdateUtils.checkCancelDelay(source: .killed)
243
+
240
244
  self.appMovedToForeground()
241
245
  self.checkForUpdateAfterDelay()
242
246
  }
@@ -1377,20 +1381,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1377
1381
  }
1378
1382
  }
1379
1383
 
1380
- @objc func appKilled() {
1381
- logger.info("onActivityDestroyed: all activity destroyed")
1382
- self.delayUpdateUtils.checkCancelDelay(source: .killed)
1383
-
1384
- // Clean up resources
1385
- periodicUpdateTimer?.invalidate()
1386
- periodicUpdateTimer = nil
1387
- backgroundWork?.cancel()
1388
- backgroundWork = nil
1389
-
1390
- // Signal any waiting semaphores to prevent deadlocks
1391
- semaphoreReady.signal()
1392
- }
1393
-
1394
1384
  private func installNext() {
1395
1385
  let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
1396
1386
  let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
@@ -1446,6 +1436,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1446
1436
  if self._isAutoUpdateEnabled() {
1447
1437
  self.backgroundDownload()
1448
1438
  } else {
1439
+ let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
1440
+ if instanceDescriptor?.serverURL != nil {
1441
+ self.implementation.sendStats(action: "blocked_by_server_url", versionName: current.getVersionName())
1442
+ }
1449
1443
  logger.info("Auto update is disabled")
1450
1444
  self.sendReadyToJs(current: current, msg: "disabled")
1451
1445
  }
@@ -42,10 +42,16 @@ import UIKit
42
42
  public var deviceID = ""
43
43
  public var publicKey: String = ""
44
44
 
45
+ // Flag to track if we received a 429 response - stops requests until app restart
46
+ private static var rateLimitExceeded = false
47
+
48
+ // Flag to track if we've already sent the rate limit statistic - prevents infinite loop
49
+ private static var rateLimitStatisticSent = false
50
+
45
51
  private var userAgent: String {
46
52
  let safePluginVersion = PLUGIN_VERSION.isEmpty ? "unknown" : PLUGIN_VERSION
47
53
  let safeAppId = appId.isEmpty ? "unknown" : appId
48
- return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId))"
54
+ return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(versionOs)"
49
55
  }
50
56
 
51
57
  private lazy var alamofireSession: Session = {
@@ -85,6 +91,58 @@ import UIKit
85
91
  return !self.isDevEnvironment && !self.isAppStoreReceiptSandbox() && !self.hasEmbeddedMobileProvision()
86
92
  }
87
93
 
94
+ /**
95
+ * Check if a 429 (Too Many Requests) response was received and set the flag
96
+ */
97
+ private func checkAndHandleRateLimitResponse(statusCode: Int?) -> Bool {
98
+ if statusCode == 429 {
99
+ // Send a statistic about the rate limit BEFORE setting the flag
100
+ // Only send once to prevent infinite loop if the stat request itself gets rate limited
101
+ if !CapgoUpdater.rateLimitExceeded && !CapgoUpdater.rateLimitStatisticSent {
102
+ CapgoUpdater.rateLimitStatisticSent = true
103
+ self.sendRateLimitStatistic()
104
+ }
105
+ CapgoUpdater.rateLimitExceeded = true
106
+ logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.")
107
+ return true
108
+ }
109
+ return false
110
+ }
111
+
112
+ /**
113
+ * Send a synchronous statistic about rate limiting
114
+ */
115
+ private func sendRateLimitStatistic() {
116
+ guard !statsUrl.isEmpty else {
117
+ return
118
+ }
119
+
120
+ let current = getCurrentBundle()
121
+ var parameters = createInfoObject()
122
+ parameters.action = "rate_limit_reached"
123
+ parameters.version_name = current.getVersionName()
124
+ parameters.old_version_name = ""
125
+
126
+ // Send synchronously to ensure it goes out before the flag is set
127
+ let semaphore = DispatchSemaphore(value: 0)
128
+ self.alamofireSession.request(
129
+ self.statsUrl,
130
+ method: .post,
131
+ parameters: parameters,
132
+ encoder: JSONParameterEncoder.default,
133
+ requestModifier: { $0.timeoutInterval = self.timeout }
134
+ ).responseData { response in
135
+ switch response.result {
136
+ case .success:
137
+ self.logger.info("Rate limit statistic sent")
138
+ case let .failure(error):
139
+ self.logger.error("Error sending rate limit statistic: \(error.localizedDescription)")
140
+ }
141
+ semaphore.signal()
142
+ }
143
+ semaphore.wait()
144
+ }
145
+
88
146
  // MARK: Private
89
147
  private func hasEmbeddedMobileProvision() -> Bool {
90
148
  guard Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") == nil else {
@@ -1011,6 +1069,15 @@ import UIKit
1011
1069
 
1012
1070
  func unsetChannel() -> SetChannel {
1013
1071
  let setChannel: SetChannel = SetChannel()
1072
+
1073
+ // Check if rate limit was exceeded
1074
+ if CapgoUpdater.rateLimitExceeded {
1075
+ logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.")
1076
+ setChannel.message = "Rate limit exceeded"
1077
+ setChannel.error = "rate_limit_exceeded"
1078
+ return setChannel
1079
+ }
1080
+
1014
1081
  if (self.channelUrl ).isEmpty {
1015
1082
  logger.error("Channel URL is not set")
1016
1083
  setChannel.message = "Channel URL is not set"
@@ -1023,6 +1090,14 @@ import UIKit
1023
1090
  let request = alamofireSession.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1024
1091
 
1025
1092
  request.validate().responseDecodable(of: SetChannelDec.self) { response in
1093
+ // Check for 429 rate limit
1094
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1095
+ setChannel.message = "Rate limit exceeded"
1096
+ setChannel.error = "rate_limit_exceeded"
1097
+ semaphore.signal()
1098
+ return
1099
+ }
1100
+
1026
1101
  switch response.result {
1027
1102
  case .success:
1028
1103
  if let responseValue = response.value {
@@ -1045,6 +1120,15 @@ import UIKit
1045
1120
 
1046
1121
  func setChannel(channel: String) -> SetChannel {
1047
1122
  let setChannel: SetChannel = SetChannel()
1123
+
1124
+ // Check if rate limit was exceeded
1125
+ if CapgoUpdater.rateLimitExceeded {
1126
+ logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.")
1127
+ setChannel.message = "Rate limit exceeded"
1128
+ setChannel.error = "rate_limit_exceeded"
1129
+ return setChannel
1130
+ }
1131
+
1048
1132
  if (self.channelUrl ).isEmpty {
1049
1133
  logger.error("Channel URL is not set")
1050
1134
  setChannel.message = "Channel URL is not set"
@@ -1058,6 +1142,14 @@ import UIKit
1058
1142
  let request = alamofireSession.request(self.channelUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1059
1143
 
1060
1144
  request.validate().responseDecodable(of: SetChannelDec.self) { response in
1145
+ // Check for 429 rate limit
1146
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1147
+ setChannel.message = "Rate limit exceeded"
1148
+ setChannel.error = "rate_limit_exceeded"
1149
+ semaphore.signal()
1150
+ return
1151
+ }
1152
+
1061
1153
  switch response.result {
1062
1154
  case .success:
1063
1155
  if let responseValue = response.value {
@@ -1080,6 +1172,15 @@ import UIKit
1080
1172
 
1081
1173
  func getChannel() -> GetChannel {
1082
1174
  let getChannel: GetChannel = GetChannel()
1175
+
1176
+ // Check if rate limit was exceeded
1177
+ if CapgoUpdater.rateLimitExceeded {
1178
+ logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.")
1179
+ getChannel.message = "Rate limit exceeded"
1180
+ getChannel.error = "rate_limit_exceeded"
1181
+ return getChannel
1182
+ }
1183
+
1083
1184
  if (self.channelUrl ).isEmpty {
1084
1185
  logger.error("Channel URL is not set")
1085
1186
  getChannel.message = "Channel URL is not set"
@@ -1094,6 +1195,14 @@ import UIKit
1094
1195
  defer {
1095
1196
  semaphore.signal()
1096
1197
  }
1198
+
1199
+ // Check for 429 rate limit
1200
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1201
+ getChannel.message = "Rate limit exceeded"
1202
+ getChannel.error = "rate_limit_exceeded"
1203
+ return
1204
+ }
1205
+
1097
1206
  switch response.result {
1098
1207
  case .success:
1099
1208
  if let responseValue = response.value {
@@ -1125,6 +1234,14 @@ import UIKit
1125
1234
 
1126
1235
  func listChannels() -> ListChannels {
1127
1236
  let listChannels: ListChannels = ListChannels()
1237
+
1238
+ // Check if rate limit was exceeded
1239
+ if CapgoUpdater.rateLimitExceeded {
1240
+ logger.debug("Skipping listChannels due to rate limit (429). Requests will resume after app restart.")
1241
+ listChannels.error = "rate_limit_exceeded"
1242
+ return listChannels
1243
+ }
1244
+
1128
1245
  if (self.channelUrl).isEmpty {
1129
1246
  logger.error("Channel URL is not set")
1130
1247
  listChannels.error = "Channel URL is not set"
@@ -1160,6 +1277,13 @@ import UIKit
1160
1277
  defer {
1161
1278
  semaphore.signal()
1162
1279
  }
1280
+
1281
+ // Check for 429 rate limit
1282
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1283
+ listChannels.error = "rate_limit_exceeded"
1284
+ return
1285
+ }
1286
+
1163
1287
  switch response.result {
1164
1288
  case .success:
1165
1289
  if let responseValue = response.value {
@@ -1193,6 +1317,12 @@ import UIKit
1193
1317
  private let operationQueue = OperationQueue()
1194
1318
 
1195
1319
  func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
1320
+ // Check if rate limit was exceeded
1321
+ if CapgoUpdater.rateLimitExceeded {
1322
+ logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.")
1323
+ return
1324
+ }
1325
+
1196
1326
  guard !statsUrl.isEmpty else {
1197
1327
  return
1198
1328
  }
@@ -1214,6 +1344,12 @@ import UIKit
1214
1344
  encoder: JSONParameterEncoder.default,
1215
1345
  requestModifier: { $0.timeoutInterval = self.timeout }
1216
1346
  ).responseData { response in
1347
+ // Check for 429 rate limit
1348
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1349
+ semaphore.signal()
1350
+ return
1351
+ }
1352
+
1217
1353
  switch response.result {
1218
1354
  case .success:
1219
1355
  self.logger.info("Stats sent for \(action), version \(versionName)")
@@ -1222,7 +1358,7 @@ import UIKit
1222
1358
  }
1223
1359
  semaphore.signal()
1224
1360
  }
1225
- semaphore.signal()
1361
+ semaphore.wait()
1226
1362
  }
1227
1363
  operationQueue.addOperation(operation)
1228
1364
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.23.1",
3
+ "version": "7.23.13",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",
@@ -48,7 +48,7 @@
48
48
  "test:ios": "./scripts/test-ios.sh",
49
49
  "test:android": "cd android && ./gradlew test && cd ..",
50
50
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
51
- "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
51
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
52
52
  "eslint": "eslint . --ext .ts",
53
53
  "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
54
54
  "swiftlint": "node-swiftlint",