@capgo/capacitor-updater 8.49.1 → 8.49.3

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
@@ -79,7 +79,7 @@ First follow the migration guide of Capacitor:
79
79
  - **Channel storage change**: `setChannel()` now stores channel assignments locally on the device instead of in the cloud. This provides better offline support and reduces backend load.
80
80
  - Channel assignments persist between app restarts
81
81
  - Use `unsetChannel()` to clear the local assignment and revert to `defaultChannel`
82
- - Old devices (< v7.34.0) will continue using cloud-based storage
82
+ - Old devices (< v7.34.0) use now a KV storage to prevent overload the primary DB of Capgo
83
83
  - **New event**: Listen to the `channelPrivate` event to handle cases where a user tries to assign themselves to a private channel (one that doesn't allow self-assignment). See example in the `setChannel()` documentation above.
84
84
 
85
85
  ## Migration to v7
@@ -133,6 +133,20 @@ We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bun
133
133
 
134
134
  ## Installation
135
135
 
136
+ You can use our AI-Assisted Setup to install the plugin. Add the Capgo skills to your AI tool using the following command:
137
+
138
+ ```bash
139
+ npx skills add https://github.com/cap-go/capacitor-skills --skill capacitor-plugins
140
+ ```
141
+
142
+ Then use the following prompt:
143
+
144
+ ```text
145
+ Use the `capacitor-plugins` skill from `cap-go/capacitor-skills` to install the `@capgo/capacitor-updater` plugin in my project.
146
+ ```
147
+
148
+ If you prefer Manual Setup, install the plugin by running the following commands and follow the platform-specific instructions below:
149
+
136
150
  Step by step here: [Getting started](https://capgo.app/docs/getting-started/add-an-app/)
137
151
 
138
152
  Or
@@ -1246,6 +1260,11 @@ Assign this device to a specific update channel at runtime.
1246
1260
  Channels allow you to distribute different bundle versions to different groups of users
1247
1261
  (e.g., "production", "beta", "staging"). This method switches the device to a new channel.
1248
1262
 
1263
+ **Device Override UI:** `setChannel()` validates the channel with the backend, then stores the
1264
+ selected channel locally on the device. It does not create or update a backend Device Override,
1265
+ so the device will not appear as overridden in the Capgo dashboard. Only assignments created
1266
+ from the dashboard or the Public API are shown in the Device Override UI.
1267
+
1249
1268
  **Requirements:**
1250
1269
  - The target channel must allow self-assignment (configured in your Capgo dashboard or backend)
1251
1270
  - The backend may accept or reject the request based on channel settings
@@ -1272,7 +1291,8 @@ CapacitorUpdater.addListener('channelPrivate', (data) =&gt; {
1272
1291
  });
1273
1292
  ```
1274
1293
 
1275
- This sends a request to the Capgo backend linking your device ID to the specified channel.
1294
+ This sends a request to the Capgo backend to validate the specified channel, then stores the
1295
+ channel locally on the device.
1276
1296
 
1277
1297
  | Param | Type | Description |
1278
1298
  | ------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -1291,11 +1311,12 @@ This sends a request to the Capgo backend linking your device ID to the specifie
1291
1311
  unsetChannel(options: UnsetChannelOptions) => Promise<void>
1292
1312
  ```
1293
1313
 
1294
- Remove the device's channel assignment and return to the default channel.
1314
+ Remove the plugin-managed local channel assignment and return to the default channel.
1295
1315
 
1296
- This unlinks the device from any specifically assigned channel, causing it to fall back to:
1316
+ This clears only the channel stored locally by {@link setChannel}; it does not delete Dashboard or Public API Device Override records. After the local assignment is cleared, normal channel precedence applies:
1317
+ - An existing Dashboard or Public API Device Override, if one exists
1297
1318
  - The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or
1298
- - Your backend's default channel for this app
1319
+ - Your backend default channel for this app
1299
1320
 
1300
1321
  Use this when:
1301
1322
  - Users opt out of beta/testing programs
@@ -1444,7 +1465,7 @@ It's automatically generated and stored securely by the plugin.
1444
1465
  **Privacy & Security characteristics:**
1445
1466
  - Generated as a UUID (not based on hardware identifiers)
1446
1467
  - Stored securely in platform-specific secure storage
1447
- - Android: Android Keystore (persists across app reinstalls on API 23+)
1468
+ - Android: mirrored into backup-restorable app preferences for reinstall restore
1448
1469
  - iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
1449
1470
  - Not synced to cloud (iOS)
1450
1471
  - Follows Apple and Google privacy best practices
@@ -1452,7 +1473,9 @@ It's automatically generated and stored securely by the plugin.
1452
1473
 
1453
1474
  **Persistence:**
1454
1475
  The device ID persists across app reinstalls to maintain consistent device identity
1455
- for update tracking and analytics.
1476
+ for update tracking and analytics when platform storage is preserved. On Android,
1477
+ apps with custom backup rules must keep the plugin app preferences eligible for
1478
+ backup/restore; disabling Android backup or clearing app data creates a new ID.
1456
1479
 
1457
1480
  Use this to:
1458
1481
  - Debug update delivery issues (check what ID the server sees)
@@ -119,6 +119,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
119
119
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
120
120
  private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
121
121
  private static final String LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY = "CapacitorUpdater.lastWebViewRenderProcessGone";
122
+ private static final String LAST_VERSION_OS_PREF_KEY = "CapacitorUpdater.lastVersionOs";
123
+ private static final String LAST_VERSION_BUILD_PREF_KEY = "CapacitorUpdater.lastVersionBuild";
124
+ private static final String LAST_VERSION_CODE_PREF_KEY = "CapacitorUpdater.lastVersionCode";
125
+ private static final String OS_VERSION_CHANGED_ACTION = "os_version_changed";
126
+ private static final String NATIVE_APP_VERSION_CHANGED_ACTION = "native_app_version_changed";
122
127
  private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
123
128
  private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
124
129
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
@@ -137,7 +142,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
137
142
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
138
143
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
139
144
 
140
- private final String pluginVersion = "8.49.1";
145
+ private final String pluginVersion = "8.49.3";
141
146
  private static final String DELAY_CONDITION_PREFERENCES = "";
142
147
 
143
148
  private SharedPreferences.Editor editor;
@@ -857,6 +862,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
857
862
  this.clearPreviewSessionForNativeBuildChange();
858
863
  }
859
864
  this.leavePreviewSessionForLaunchIntentIfNeeded();
865
+ this.reportNativeVersionStatsIfChanged();
860
866
  this.reportPreviousAppExitReasons();
861
867
  this.reportPreviousWebViewRenderProcessGone();
862
868
  this.installWebViewStatsReporter();
@@ -1299,6 +1305,109 @@ public class CapacitorUpdaterPlugin extends Plugin {
1299
1305
  return !lastKnownVersion.isEmpty() && !lastKnownVersion.equals(this.currentBuildVersion);
1300
1306
  }
1301
1307
 
1308
+ void reportNativeVersionStatsIfChanged() {
1309
+ if (this.implementation == null || this.prefs == null || this.editor == null) {
1310
+ return;
1311
+ }
1312
+
1313
+ this.reportNativeVersionStatsIfChanged(
1314
+ this.implementation.versionBuild,
1315
+ this.implementation.versionCode,
1316
+ this.implementation.versionOs
1317
+ );
1318
+ }
1319
+
1320
+ void reportNativeVersionStatsIfChanged(
1321
+ final String currentVersionBuild,
1322
+ final String currentVersionCode,
1323
+ final String currentVersionOs
1324
+ ) {
1325
+ if (this.implementation == null || this.prefs == null || this.editor == null) {
1326
+ return;
1327
+ }
1328
+
1329
+ final String normalizedVersionBuild = this.normalizedStatsValue(currentVersionBuild);
1330
+ final String normalizedVersionCode = this.normalizedStatsValue(currentVersionCode);
1331
+ final String normalizedVersionOs = this.normalizedStatsValue(currentVersionOs);
1332
+ final String previousVersionOs = this.prefs.getString(LAST_VERSION_OS_PREF_KEY, "");
1333
+ final String previousVersionBuild = this.prefs.getString(LAST_VERSION_BUILD_PREF_KEY, "");
1334
+ final String previousVersionCode = this.prefs.getString(LAST_VERSION_CODE_PREF_KEY, "");
1335
+ final boolean osVersionChanged =
1336
+ !normalizedVersionOs.isEmpty() &&
1337
+ previousVersionOs != null &&
1338
+ !previousVersionOs.isEmpty() &&
1339
+ !previousVersionOs.equals(normalizedVersionOs);
1340
+
1341
+ if (osVersionChanged) {
1342
+ final Map<String, String> metadata = new HashMap<>();
1343
+ metadata.put("previous_version_os", previousVersionOs);
1344
+ metadata.put("current_version_os", normalizedVersionOs);
1345
+ this.implementation.sendStats(
1346
+ OS_VERSION_CHANGED_ACTION,
1347
+ this.implementation.getCurrentBundle().getVersionName(),
1348
+ "",
1349
+ metadata,
1350
+ () -> this.persistLastVersionOs(normalizedVersionOs)
1351
+ );
1352
+ }
1353
+
1354
+ final boolean hasPreviousNativeVersion =
1355
+ (previousVersionBuild != null && !previousVersionBuild.isEmpty()) ||
1356
+ (previousVersionCode != null && !previousVersionCode.isEmpty());
1357
+ final boolean nativeVersionChanged =
1358
+ hasPreviousNativeVersion &&
1359
+ (!Objects.equals(previousVersionBuild, normalizedVersionBuild) || !Objects.equals(previousVersionCode, normalizedVersionCode));
1360
+
1361
+ if (nativeVersionChanged) {
1362
+ final Map<String, String> metadata = new HashMap<>();
1363
+ metadata.put("previous_version_build", previousVersionBuild == null ? "" : previousVersionBuild);
1364
+ metadata.put("current_version_build", normalizedVersionBuild);
1365
+ metadata.put("previous_version_code", previousVersionCode == null ? "" : previousVersionCode);
1366
+ metadata.put("current_version_code", normalizedVersionCode);
1367
+ this.implementation.sendStats(
1368
+ NATIVE_APP_VERSION_CHANGED_ACTION,
1369
+ this.implementation.getCurrentBundle().getVersionName(),
1370
+ "",
1371
+ metadata,
1372
+ () -> this.persistLastNativeAppVersion(normalizedVersionBuild, normalizedVersionCode)
1373
+ );
1374
+ }
1375
+
1376
+ if (!osVersionChanged || !nativeVersionChanged) {
1377
+ if (!osVersionChanged) {
1378
+ this.editor.putString(LAST_VERSION_OS_PREF_KEY, normalizedVersionOs);
1379
+ }
1380
+ if (!nativeVersionChanged) {
1381
+ this.editor.putString(LAST_VERSION_BUILD_PREF_KEY, normalizedVersionBuild);
1382
+ this.editor.putString(LAST_VERSION_CODE_PREF_KEY, normalizedVersionCode);
1383
+ }
1384
+ this.editor.apply();
1385
+ }
1386
+ }
1387
+
1388
+ private void persistLastVersionOs(final String versionOs) {
1389
+ if (this.editor == null) {
1390
+ return;
1391
+ }
1392
+
1393
+ this.editor.putString(LAST_VERSION_OS_PREF_KEY, versionOs);
1394
+ this.editor.apply();
1395
+ }
1396
+
1397
+ private void persistLastNativeAppVersion(final String versionBuild, final String versionCode) {
1398
+ if (this.editor == null) {
1399
+ return;
1400
+ }
1401
+
1402
+ this.editor.putString(LAST_VERSION_BUILD_PREF_KEY, versionBuild);
1403
+ this.editor.putString(LAST_VERSION_CODE_PREF_KEY, versionCode);
1404
+ this.editor.apply();
1405
+ }
1406
+
1407
+ private String normalizedStatsValue(final String value) {
1408
+ return value == null ? "" : value;
1409
+ }
1410
+
1302
1411
  private void reportPreviousAppExitReasons() {
1303
1412
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || this.implementation == null || this.implementation.statsUrl.isEmpty()) {
1304
1413
  return;
@@ -102,11 +102,22 @@ public class CapgoUpdater {
102
102
  private static volatile boolean rateLimitStatisticSent = false;
103
103
 
104
104
  // Stats batching - queue events and send max once per second
105
- private final List<JSONObject> statsQueue = new CopyOnWriteArrayList<>();
105
+ private final List<QueuedStatsEvent> statsQueue = new CopyOnWriteArrayList<>();
106
106
  private final ScheduledExecutorService statsScheduler = Executors.newSingleThreadScheduledExecutor();
107
107
  private ScheduledFuture<?> statsFlushTask = null;
108
108
  private static final long STATS_FLUSH_INTERVAL_MS = 1000;
109
109
 
110
+ private static final class QueuedStatsEvent {
111
+
112
+ private final JSONObject event;
113
+ private final Runnable onSent;
114
+
115
+ private QueuedStatsEvent(final JSONObject event, final Runnable onSent) {
116
+ this.event = event;
117
+ this.onSent = onSent;
118
+ }
119
+ }
120
+
110
121
  private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
111
122
  private final ExecutorService io = Executors.newSingleThreadExecutor();
112
123
 
@@ -2086,6 +2097,16 @@ public class CapgoUpdater {
2086
2097
  }
2087
2098
 
2088
2099
  public void sendStats(final String action, final String versionName, final String oldVersionName, final Map<String, String> metadata) {
2100
+ this.sendStats(action, versionName, oldVersionName, metadata, null);
2101
+ }
2102
+
2103
+ public void sendStats(
2104
+ final String action,
2105
+ final String versionName,
2106
+ final String oldVersionName,
2107
+ final Map<String, String> metadata,
2108
+ final Runnable onSent
2109
+ ) {
2089
2110
  if (this.previewSession) {
2090
2111
  if (logger != null) {
2091
2112
  logger.debug("Skipping sendStats during preview session.");
@@ -2120,7 +2141,7 @@ public class CapgoUpdater {
2120
2141
  return;
2121
2142
  }
2122
2143
 
2123
- statsQueue.add(json);
2144
+ statsQueue.add(new QueuedStatsEvent(json, onSent));
2124
2145
  ensureStatsTimerStarted();
2125
2146
  }
2126
2147
 
@@ -2147,7 +2168,7 @@ public class CapgoUpdater {
2147
2168
  }
2148
2169
 
2149
2170
  // Copy and clear the queue atomically using synchronized block
2150
- List<JSONObject> eventsToSend;
2171
+ List<QueuedStatsEvent> eventsToSend;
2151
2172
  synchronized (statsQueue) {
2152
2173
  if (statsQueue.isEmpty()) {
2153
2174
  return;
@@ -2157,8 +2178,8 @@ public class CapgoUpdater {
2157
2178
  }
2158
2179
 
2159
2180
  JSONArray jsonArray = new JSONArray();
2160
- for (JSONObject event : eventsToSend) {
2161
- jsonArray.put(event);
2181
+ for (QueuedStatsEvent queuedEvent : eventsToSend) {
2182
+ jsonArray.put(queuedEvent.event);
2162
2183
  }
2163
2184
 
2164
2185
  Request request = new Request.Builder()
@@ -2186,6 +2207,7 @@ public class CapgoUpdater {
2186
2207
  if (response.isSuccessful()) {
2187
2208
  logger.info("Stats batch sent successfully");
2188
2209
  logger.debug("Sent " + eventCount + " events");
2210
+ runStatsCallbacks(eventsToSend);
2189
2211
  } else {
2190
2212
  logger.error("Error sending stats batch");
2191
2213
  logger.debug("Response code: " + response.code());
@@ -2196,6 +2218,23 @@ public class CapgoUpdater {
2196
2218
  );
2197
2219
  }
2198
2220
 
2221
+ private void runStatsCallbacks(final List<QueuedStatsEvent> sentEvents) {
2222
+ for (final QueuedStatsEvent sentEvent : sentEvents) {
2223
+ if (sentEvent.onSent == null) {
2224
+ continue;
2225
+ }
2226
+
2227
+ try {
2228
+ sentEvent.onSent.run();
2229
+ } catch (Exception e) {
2230
+ if (logger != null) {
2231
+ logger.error("Error running stats sent callback");
2232
+ logger.debug("Error: " + e.getMessage());
2233
+ }
2234
+ }
2235
+ }
2236
+ }
2237
+
2199
2238
  public BundleInfo getBundleInfo(final String id) {
2200
2239
  String trueId = BundleInfo.VERSION_UNKNOWN;
2201
2240
  if (id != null) {
@@ -26,10 +26,10 @@ import javax.crypto.spec.GCMParameterSpec;
26
26
 
27
27
  /**
28
28
  * Helper class to manage device ID persistence across app installations.
29
- * Uses Android Keystore to persist the device ID across reinstalls.
29
+ * Uses Android Keystore-backed storage and backup-restorable SharedPreferences.
30
30
  *
31
- * The device ID is a random UUID stored in the Android Keystore, which persists
32
- * even after app uninstall/reinstall on Android 6.0+ (API 23+).
31
+ * The device ID is a random UUID stored in encrypted preferences and mirrored
32
+ * to appUUID so Android backup/restore can keep it across reinstalls.
33
33
  */
34
34
  public class DeviceIdHelper {
35
35
 
@@ -45,10 +45,10 @@ public class DeviceIdHelper {
45
45
  * Gets or creates a device ID that persists across reinstalls.
46
46
  *
47
47
  * This method:
48
- * 1. First checks for an existing ID in Keystore-encrypted storage (persists across reinstalls)
49
- * 2. Falls back to legacy SharedPreferences (for migration)
48
+ * 1. First checks for an existing ID in Keystore-encrypted storage
49
+ * 2. Falls back to legacy SharedPreferences restored by Android backup
50
50
  * 3. Generates a new UUID if neither exists
51
- * 4. Stores the ID in Keystore-encrypted storage for future use
51
+ * 4. Stores the ID synchronously in both stores for future use
52
52
  *
53
53
  * @param context Application context
54
54
  * @param legacyPrefs Legacy SharedPreferences (for migration)
@@ -60,7 +60,9 @@ public class DeviceIdHelper {
60
60
  String deviceId = getDeviceIdFromKeystore(context);
61
61
 
62
62
  if (deviceId != null && !deviceId.isEmpty()) {
63
- return deviceId.toLowerCase();
63
+ deviceId = deviceId.toLowerCase();
64
+ saveLegacyDeviceId(legacyPrefs, deviceId);
65
+ return deviceId;
64
66
  }
65
67
 
66
68
  // Migration: Check legacy SharedPreferences for existing device ID
@@ -74,7 +76,8 @@ public class DeviceIdHelper {
74
76
  // Ensure lowercase for consistency
75
77
  deviceId = deviceId.toLowerCase();
76
78
 
77
- // Save to Keystore storage
79
+ // Save to backup-restorable preferences and Keystore storage
80
+ saveLegacyDeviceId(legacyPrefs, deviceId);
78
81
  saveDeviceIdToKeystore(context, deviceId);
79
82
 
80
83
  return deviceId;
@@ -90,31 +93,35 @@ public class DeviceIdHelper {
90
93
  * @param context Application context
91
94
  * @return Device ID string or null if not found
92
95
  */
93
- private static String getDeviceIdFromKeystore(Context context) throws Exception {
94
- SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
95
- String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
96
- String ivString = prefs.getString(IV_KEY, null);
96
+ private static String getDeviceIdFromKeystore(Context context) {
97
+ try {
98
+ SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
99
+ String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
100
+ String ivString = prefs.getString(IV_KEY, null);
97
101
 
98
- if (encryptedDeviceId == null || ivString == null) {
99
- return null;
100
- }
102
+ if (encryptedDeviceId == null || ivString == null) {
103
+ return null;
104
+ }
101
105
 
102
- // Get the encryption key from Keystore
103
- SecretKey key = getOrCreateKey();
104
- if (key == null) {
105
- return null;
106
- }
106
+ // Get the encryption key from Keystore
107
+ SecretKey key = getOrCreateKey();
108
+ if (key == null) {
109
+ return null;
110
+ }
107
111
 
108
- // Decrypt the device ID
109
- byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
110
- byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
112
+ // Decrypt the device ID
113
+ byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
114
+ byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
111
115
 
112
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
113
- GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
114
- cipher.init(Cipher.DECRYPT_MODE, key, spec);
116
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
117
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
118
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
115
119
 
116
- byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
117
- return new String(decryptedBytes, StandardCharsets.UTF_8);
120
+ byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
121
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
122
+ } catch (Exception e) {
123
+ return null;
124
+ }
118
125
  }
119
126
 
120
127
  /**
@@ -146,7 +153,7 @@ public class DeviceIdHelper {
146
153
  .edit()
147
154
  .putString(DEVICE_ID_KEY, android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT))
148
155
  .putString(IV_KEY, android.util.Base64.encodeToString(iv, android.util.Base64.DEFAULT))
149
- .apply();
156
+ .commit();
150
157
  }
151
158
 
152
159
  /**
@@ -207,9 +214,17 @@ public class DeviceIdHelper {
207
214
 
208
215
  if (deviceId == null || deviceId.isEmpty()) {
209
216
  deviceId = UUID.randomUUID().toString();
210
- legacyPrefs.edit().putString(LEGACY_PREFS_KEY, deviceId).apply();
217
+ saveLegacyDeviceId(legacyPrefs, deviceId);
211
218
  }
212
219
 
213
220
  return deviceId.toLowerCase();
214
221
  }
222
+
223
+ private static void saveLegacyDeviceId(SharedPreferences legacyPrefs, String deviceId) {
224
+ if (legacyPrefs == null || deviceId == null || deviceId.isEmpty()) {
225
+ return;
226
+ }
227
+
228
+ legacyPrefs.edit().putString(LEGACY_PREFS_KEY, deviceId).commit();
229
+ }
215
230
  }
package/dist/docs.json CHANGED
@@ -846,7 +846,7 @@
846
846
  "text": "4.7.0"
847
847
  }
848
848
  ],
849
- "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend linking your device ID to the specified channel.",
849
+ "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Device Override UI:** `setChannel()` validates the channel with the backend, then stores the\nselected channel locally on the device. It does not create or update a backend Device Override,\nso the device will not appear as overridden in the Capgo dashboard. Only assignments created\nfrom the dashboard or the Public API are shown in the Device Override UI.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend to validate the specified channel, then stores the\nchannel locally on the device.",
850
850
  "complexTypes": [
851
851
  "ChannelRes",
852
852
  "SetChannelOptions"
@@ -886,7 +886,7 @@
886
886
  "text": "4.7.0"
887
887
  }
888
888
  ],
889
- "docs": "Remove the device's channel assignment and return to the default channel.\n\nThis unlinks the device from any specifically assigned channel, causing it to fall back to:\n- The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or\n- Your backend's default channel for this app\n\nUse this when:\n- Users opt out of beta/testing programs\n- You want to reset a device to standard update distribution\n- Testing channel switching behavior",
889
+ "docs": "Remove the plugin-managed local channel assignment and return to the default channel.\n\nThis clears only the channel stored locally by {@link setChannel}; it does not delete Dashboard or Public API Device Override records. After the local assignment is cleared, normal channel precedence applies:\n- An existing Dashboard or Public API Device Override, if one exists\n- The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or\n- Your backend default channel for this app\n\nUse this when:\n- Users opt out of beta/testing programs\n- You want to reset a device to standard update distribution\n- Testing channel switching behavior",
890
890
  "complexTypes": [
891
891
  "UnsetChannelOptions"
892
892
  ],
@@ -1013,7 +1013,7 @@
1013
1013
  "text": "{Error} If the operation fails."
1014
1014
  }
1015
1015
  ],
1016
- "docs": "Get the unique, privacy-friendly identifier for this device.\n\nThis ID is used to identify the device when communicating with update servers.\nIt's automatically generated and stored securely by the plugin.\n\n**Privacy & Security characteristics:**\n- Generated as a UUID (not based on hardware identifiers)\n- Stored securely in platform-specific secure storage\n- Android: Android Keystore (persists across app reinstalls on API 23+)\n- iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`\n- Not synced to cloud (iOS)\n- Follows Apple and Google privacy best practices\n- Users can clear it via system settings (Android) or keychain access (iOS)\n\n**Persistence:**\nThe device ID persists across app reinstalls to maintain consistent device identity\nfor update tracking and analytics.\n\nUse this to:\n- Debug update delivery issues (check what ID the server sees)\n- Implement device-specific features\n- Correlate server logs with specific devices",
1016
+ "docs": "Get the unique, privacy-friendly identifier for this device.\n\nThis ID is used to identify the device when communicating with update servers.\nIt's automatically generated and stored securely by the plugin.\n\n**Privacy & Security characteristics:**\n- Generated as a UUID (not based on hardware identifiers)\n- Stored securely in platform-specific secure storage\n- Android: mirrored into backup-restorable app preferences for reinstall restore\n- iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`\n- Not synced to cloud (iOS)\n- Follows Apple and Google privacy best practices\n- Users can clear it via system settings (Android) or keychain access (iOS)\n\n**Persistence:**\nThe device ID persists across app reinstalls to maintain consistent device identity\nfor update tracking and analytics when platform storage is preserved. On Android,\napps with custom backup rules must keep the plugin app preferences eligible for\nbackup/restore; disabling Android backup or clearing app data creates a new ID.\n\nUse this to:\n- Debug update delivery issues (check what ID the server sees)\n- Implement device-specific features\n- Correlate server logs with specific devices",
1017
1017
  "complexTypes": [
1018
1018
  "DeviceId"
1019
1019
  ],
@@ -938,6 +938,11 @@ export interface CapacitorUpdaterPlugin {
938
938
  * Channels allow you to distribute different bundle versions to different groups of users
939
939
  * (e.g., "production", "beta", "staging"). This method switches the device to a new channel.
940
940
  *
941
+ * **Device Override UI:** `setChannel()` validates the channel with the backend, then stores the
942
+ * selected channel locally on the device. It does not create or update a backend Device Override,
943
+ * so the device will not appear as overridden in the Capgo dashboard. Only assignments created
944
+ * from the dashboard or the Public API are shown in the Device Override UI.
945
+ *
941
946
  * **Requirements:**
942
947
  * - The target channel must allow self-assignment (configured in your Capgo dashboard or backend)
943
948
  * - The backend may accept or reject the request based on channel settings
@@ -964,7 +969,8 @@ export interface CapacitorUpdaterPlugin {
964
969
  * });
965
970
  * ```
966
971
  *
967
- * This sends a request to the Capgo backend linking your device ID to the specified channel.
972
+ * This sends a request to the Capgo backend to validate the specified channel, then stores the
973
+ * channel locally on the device.
968
974
  *
969
975
  * @param options The {@link SetChannelOptions} containing the channel name and optional auto-update trigger.
970
976
  * @returns {Promise<ChannelRes>} Channel operation result with status and optional error/message.
@@ -973,11 +979,12 @@ export interface CapacitorUpdaterPlugin {
973
979
  */
974
980
  setChannel(options: SetChannelOptions): Promise<ChannelRes>;
975
981
  /**
976
- * Remove the device's channel assignment and return to the default channel.
982
+ * Remove the plugin-managed local channel assignment and return to the default channel.
977
983
  *
978
- * This unlinks the device from any specifically assigned channel, causing it to fall back to:
984
+ * This clears only the channel stored locally by {@link setChannel}; it does not delete Dashboard or Public API Device Override records. After the local assignment is cleared, normal channel precedence applies:
985
+ * - An existing Dashboard or Public API Device Override, if one exists
979
986
  * - The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or
980
- * - Your backend's default channel for this app
987
+ * - Your backend default channel for this app
981
988
  *
982
989
  * Use this when:
983
990
  * - Users opt out of beta/testing programs
@@ -1088,7 +1095,7 @@ export interface CapacitorUpdaterPlugin {
1088
1095
  * **Privacy & Security characteristics:**
1089
1096
  * - Generated as a UUID (not based on hardware identifiers)
1090
1097
  * - Stored securely in platform-specific secure storage
1091
- * - Android: Android Keystore (persists across app reinstalls on API 23+)
1098
+ * - Android: mirrored into backup-restorable app preferences for reinstall restore
1092
1099
  * - iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
1093
1100
  * - Not synced to cloud (iOS)
1094
1101
  * - Follows Apple and Google privacy best practices
@@ -1096,7 +1103,9 @@ export interface CapacitorUpdaterPlugin {
1096
1103
  *
1097
1104
  * **Persistence:**
1098
1105
  * The device ID persists across app reinstalls to maintain consistent device identity
1099
- * for update tracking and analytics.
1106
+ * for update tracking and analytics when platform storage is preserved. On Android,
1107
+ * apps with custom backup rules must keep the plugin app preferences eligible for
1108
+ * backup/restore; disabling Android backup or clearing app data creates a new ID.
1100
1109
  *
1101
1110
  * Use this to:
1102
1111
  * - Debug update delivery issues (check what ID the server sees)