@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 +30 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +110 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +44 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/dist/docs.json +3 -3
- package/dist/esm/definitions.d.ts +15 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +87 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +32 -6
- package/package.json +1 -1
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)
|
|
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) => {
|
|
|
1272
1291
|
});
|
|
1273
1292
|
```
|
|
1274
1293
|
|
|
1275
|
-
This sends a request to the Capgo backend
|
|
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
|
|
1314
|
+
Remove the plugin-managed local channel assignment and return to the default channel.
|
|
1295
1315
|
|
|
1296
|
-
This
|
|
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
|
|
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:
|
|
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.
|
|
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<
|
|
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<
|
|
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 (
|
|
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
|
|
29
|
+
* Uses Android Keystore-backed storage and backup-restorable SharedPreferences.
|
|
30
30
|
*
|
|
31
|
-
* The device ID is a random UUID stored in
|
|
32
|
-
*
|
|
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
|
|
49
|
-
* 2. Falls back to legacy SharedPreferences
|
|
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
|
|
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
|
-
|
|
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)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (encryptedDeviceId == null || ivString == null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Get the encryption key from Keystore
|
|
107
|
+
SecretKey key = getOrCreateKey();
|
|
108
|
+
if (key == null) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
982
|
+
* Remove the plugin-managed local channel assignment and return to the default channel.
|
|
977
983
|
*
|
|
978
|
-
* This
|
|
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
|
|
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:
|
|
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)
|