@capawesome/capacitor-live-update 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,16 +65,17 @@ We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bun
65
65
  <docgen-config>
66
66
  <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
67
67
 
68
- | Prop | Type | Description | Default | Since |
69
- | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
70
- | **`appId`** | <code>string</code> | The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud). | | 5.0.0 |
71
- | **`autoDeleteBundles`** | <code>boolean</code> | Whether or not to automatically delete unused bundles. When enabled, the plugin will automatically delete unused bundles after calling `ready()`. | <code>false</code> | 5.0.0 |
72
- | **`defaultChannel`** | <code>string</code> | The default channel of the app. | | 6.3.0 |
73
- | **`enabled`** | <code>boolean</code> | Whether or not the plugin is enabled. | <code>true</code> | 5.0.0 |
74
- | **`location`** | <code>'us' \| 'eu'</code> | The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud). | <code>'us'</code> | 6.2.0 |
75
- | **`publicKey`** | <code>string</code> | The public key to verify the integrity of the bundle. The public key must be a PEM-encoded RSA public key. | | 6.1.0 |
76
- | **`readyTimeout`** | <code>number</code> | The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle. | <code>10000</code> | 5.0.0 |
77
- | **`resetOnUpdate`** | <code>boolean</code> | Whether or not the app should be reset to the default bundle during an update. | <code>true</code> | 5.0.0 |
68
+ | Prop | Type | Description | Default | Since |
69
+ | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | ----- |
70
+ | **`appId`** | <code>string</code> | The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud). This is **NOT** the same as the app identifier (e.g. `com.example.app`). This is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`). | | 5.0.0 |
71
+ | **`autoDeleteBundles`** | <code>boolean</code> | Whether or not to automatically delete unused bundles. When enabled, the plugin will automatically delete unused bundles after calling `ready()`. | <code>false</code> | 5.0.0 |
72
+ | **`defaultChannel`** | <code>string</code> | The default channel of the app. | | 6.3.0 |
73
+ | **`enabled`** | <code>boolean</code> | Whether or not the plugin is enabled. | <code>true</code> | 5.0.0 |
74
+ | **`httpTimeout`** | <code>number</code> | The timeout in milliseconds for HTTP requests. | <code>60000</code> | 6.4.0 |
75
+ | **`location`** | <code>'us' \| 'eu'</code> | The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud). | <code>'us'</code> | 6.2.0 |
76
+ | **`publicKey`** | <code>string</code> | The public key to verify the integrity of the bundle. The public key must be a PEM-encoded RSA public key. | | 6.1.0 |
77
+ | **`readyTimeout`** | <code>number</code> | The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle. | <code>10000</code> | 5.0.0 |
78
+ | **`resetOnUpdate`** | <code>boolean</code> | Whether or not the app should be reset to the default bundle during an update. | <code>true</code> | 5.0.0 |
78
79
 
79
80
  ### Examples
80
81
 
@@ -84,10 +85,11 @@ In `capacitor.config.json`:
84
85
  {
85
86
  "plugins": {
86
87
  "LiveUpdate": {
87
- "appId": '4100e356-e851-47fe-9d3c-b411eb325a99',
88
+ "appId": '6e351b4f-69a7-415e-a057-4567df7ffe94',
88
89
  "autoDeleteBundles": undefined,
89
90
  "defaultChannel": 'production',
90
91
  "enabled": undefined,
92
+ "httpTimeout": undefined,
91
93
  "location": 'eu',
92
94
  "publicKey": '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
93
95
  "readyTimeout": undefined,
@@ -107,10 +109,11 @@ import { CapacitorConfig } from '@capacitor/cli';
107
109
  const config: CapacitorConfig = {
108
110
  plugins: {
109
111
  LiveUpdate: {
110
- appId: '4100e356-e851-47fe-9d3c-b411eb325a99',
112
+ appId: '6e351b4f-69a7-415e-a057-4567df7ffe94',
111
113
  autoDeleteBundles: undefined,
112
114
  defaultChannel: 'production',
113
115
  enabled: undefined,
116
+ httpTimeout: undefined,
114
117
  location: 'eu',
115
118
  publicKey: '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----',
116
119
  readyTimeout: undefined,
@@ -644,6 +647,20 @@ Live updates are only supported for [binary-compatible changes](https://capaweso
644
647
  If you change native code, such as adding a new plugin, you need to resubmit your app to the app stores.
645
648
  For this reason, you must be careful to [restrict live updates to compatible native versions](https://capawesome.io/blog/how-to-restrict-capacitor-live-updates-to-native-versions/) of your app.
646
649
 
650
+ ## FAQ
651
+
652
+ ### Why can't I see my changes during development?
653
+
654
+ As soon as you have installed a live update, the app will use the live update bundle and no longer the default bundle.
655
+ So if you make local changes to your app and execute `npx cap run`, for example, these changes will apply to the default bundle, which is not currently in use.
656
+ You then have three options to get back to the default bundle:
657
+
658
+ 1. **Reset**: Call the [`reset()`](#reset) method to reset the app to the default bundle.
659
+ 2. **Reinstall**: Reinstall the app to use the default bundle.
660
+ 3. **Update**: Increase the `versionCode`/`CFBundleVersion` so that the plugin automatically performs a reset (assuming the [`resetOnUpdate`](#configuration) configuration option is active).
661
+
662
+ However, this is only a problem during development. It is not a problem in production as long as you have the [`resetOnUpdate`](#configuration) configuration option enabled, as the `versionCode`/`CFBundleVersion` is always incremented during a native update and thus always resets to the default bundle.
663
+
647
664
  ## Changelog
648
665
 
649
666
  See [CHANGELOG.md](https://github.com/capawesome-team/capacitor-plugins/blob/main/packages/live-update/CHANGELOG.md).
@@ -37,6 +37,7 @@ import java.security.Signature;
37
37
  import java.security.spec.X509EncodedKeySpec;
38
38
  import java.util.Arrays;
39
39
  import java.util.UUID;
40
+ import java.util.concurrent.TimeUnit;
40
41
  import net.lingala.zip4j.ZipFile;
41
42
  import okhttp3.Call;
42
43
  import okhttp3.Callback;
@@ -213,6 +214,11 @@ public class LiveUpdate {
213
214
  new NonEmptyCallback<GetLatestBundleResponse>() {
214
215
  @Override
215
216
  public void error(Exception exception) {
217
+ // Throw error if the exception is a timeout
218
+ if (exception instanceof java.net.SocketTimeoutException) {
219
+ callback.error(new Exception(LiveUpdatePlugin.ERROR_HTTP_TIMEOUT));
220
+ return;
221
+ }
216
222
  // No update available
217
223
  Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
218
224
  SyncResult syncResult = new SyncResult(null);
@@ -381,6 +387,11 @@ public class LiveUpdate {
381
387
  new NonEmptyCallback<File>() {
382
388
  @Override
383
389
  public void error(Exception exception) {
390
+ // Throw error if the exception is a timeout
391
+ if (exception instanceof java.net.SocketTimeoutException) {
392
+ callback.error(new Exception(LiveUpdatePlugin.ERROR_HTTP_TIMEOUT));
393
+ return;
394
+ }
384
395
  callback.error(exception);
385
396
  }
386
397
 
@@ -425,7 +436,12 @@ public class LiveUpdate {
425
436
  }
426
437
 
427
438
  private void downloadFile(@NonNull String url, @NonNull NonEmptyCallback<File> callback) {
428
- OkHttpClient okHttpClient = new OkHttpClient();
439
+ int httpTimeout = config.getHttpTimeout();
440
+ OkHttpClient okHttpClient = new OkHttpClient.Builder()
441
+ .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
442
+ .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
443
+ .writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
444
+ .build();
429
445
  Request request = new Request.Builder().url(url).build();
430
446
  okHttpClient
431
447
  .newCall(request)
@@ -495,7 +511,13 @@ public class LiveUpdate {
495
511
  .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
496
512
  .build();
497
513
  Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle from " + url);
498
- OkHttpClient client = new OkHttpClient();
514
+ int httpTimeout = config.getHttpTimeout();
515
+ OkHttpClient client = new OkHttpClient()
516
+ .newBuilder()
517
+ .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
518
+ .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
519
+ .writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
520
+ .build();
499
521
  okhttp3.Request request = new okhttp3.Request.Builder().url(url).build();
500
522
  client
501
523
  .newCall(request)
@@ -14,6 +14,7 @@ public class LiveUpdateConfig {
14
14
  private String defaultChannel = null;
15
15
 
16
16
  private boolean enabled = true;
17
+ private int httpTimeout = 60000;
17
18
 
18
19
  @Nullable
19
20
  private String location = null;
@@ -42,6 +43,10 @@ public class LiveUpdateConfig {
42
43
  return enabled;
43
44
  }
44
45
 
46
+ public int getHttpTimeout() {
47
+ return httpTimeout;
48
+ }
49
+
45
50
  @Nullable
46
51
  public String getLocation() {
47
52
  return location;
@@ -76,6 +81,10 @@ public class LiveUpdateConfig {
76
81
  this.enabled = enabled;
77
82
  }
78
83
 
84
+ public void setHttpTimeout(int httpTimeout) {
85
+ this.httpTimeout = httpTimeout;
86
+ }
87
+
79
88
  public void setLocation(@Nullable String location) {
80
89
  this.location = location;
81
90
  }
@@ -19,7 +19,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
19
19
  public class LiveUpdatePlugin extends Plugin {
20
20
 
21
21
  public static final String TAG = "LiveUpdate";
22
- public static final String VERSION = "6.3.0";
22
+ public static final String VERSION = "6.4.0";
23
23
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
24
24
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
25
25
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
@@ -30,10 +30,14 @@ public class LiveUpdatePlugin extends Plugin {
30
30
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
31
31
  public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
32
32
  public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
33
+ public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
33
34
  public static final String ERROR_URL_MISSING = "url must be provided.";
34
35
  public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
35
36
  public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
36
37
  public static final String ERROR_SIGNATURE_MISSING = "Bundle does not contain a signature.";
38
+ public static final String ERROR_SYNC_IN_PROGRESS = "Sync is already in progress.";
39
+
40
+ private boolean syncInProgress = false;
37
41
 
38
42
  @Nullable
39
43
  private LiveUpdateConfig config;
@@ -404,14 +408,23 @@ public class LiveUpdatePlugin extends Plugin {
404
408
  call.reject(ERROR_APP_ID_MISSING);
405
409
  return;
406
410
  }
411
+
412
+ if (syncInProgress) {
413
+ call.reject(ERROR_SYNC_IN_PROGRESS);
414
+ return;
415
+ }
416
+ syncInProgress = true;
417
+
407
418
  NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
408
419
  @Override
409
420
  public void success(Result result) {
421
+ syncInProgress = false;
410
422
  call.resolve(result.toJSObject());
411
423
  }
412
424
 
413
425
  @Override
414
426
  public void error(Exception exception) {
427
+ syncInProgress = false;
415
428
  Logger.error(TAG, exception.getMessage(), exception);
416
429
  call.reject(exception.getMessage());
417
430
  }
@@ -419,6 +432,7 @@ public class LiveUpdatePlugin extends Plugin {
419
432
 
420
433
  implementation.sync(callback);
421
434
  } catch (Exception exception) {
435
+ syncInProgress = false;
422
436
  Logger.error(TAG, exception.getMessage(), exception);
423
437
  call.reject(exception.getMessage());
424
438
  }
@@ -435,6 +449,8 @@ public class LiveUpdatePlugin extends Plugin {
435
449
  config.setDefaultChannel(defaultChannel);
436
450
  boolean enabled = getConfig().getBoolean("enabled", config.getEnabled());
437
451
  config.setEnabled(enabled);
452
+ int httpTimeout = getConfig().getInt("httpTimeout", config.getHttpTimeout());
453
+ config.setHttpTimeout(httpTimeout);
438
454
  String location = getConfig().getString("location", config.getLocation());
439
455
  config.setLocation(location);
440
456
  String publicKey = getConfig().getString("publicKey", config.getPublicKey());
package/dist/docs.json CHANGED
@@ -722,11 +722,11 @@
722
722
  "name": "since"
723
723
  },
724
724
  {
725
- "text": "'4100e356-e851-47fe-9d3c-b411eb325a99'",
725
+ "text": "'6e351b4f-69a7-415e-a057-4567df7ffe94'",
726
726
  "name": "example"
727
727
  }
728
728
  ],
729
- "docs": "The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).",
729
+ "docs": "The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).\n\nThis is **NOT** the same as the app identifier (e.g. `com.example.app`).\nThis is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`).",
730
730
  "complexTypes": [],
731
731
  "type": "string | undefined"
732
732
  },
@@ -778,6 +778,22 @@
778
778
  "complexTypes": [],
779
779
  "type": "boolean | undefined"
780
780
  },
781
+ {
782
+ "name": "httpTimeout",
783
+ "tags": [
784
+ {
785
+ "text": "6.4.0",
786
+ "name": "since"
787
+ },
788
+ {
789
+ "text": "60000",
790
+ "name": "default"
791
+ }
792
+ ],
793
+ "docs": "The timeout in milliseconds for HTTP requests.",
794
+ "complexTypes": [],
795
+ "type": "number | undefined"
796
+ },
781
797
  {
782
798
  "name": "location",
783
799
  "tags": [
@@ -4,8 +4,11 @@ declare module '@capacitor/cli' {
4
4
  /**
5
5
  * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).
6
6
  *
7
+ * This is **NOT** the same as the app identifier (e.g. `com.example.app`).
8
+ * This is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`).
9
+ *
7
10
  * @since 5.0.0
8
- * @example '4100e356-e851-47fe-9d3c-b411eb325a99'
11
+ * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'
9
12
  */
10
13
  appId?: string;
11
14
  /**
@@ -31,6 +34,13 @@ declare module '@capacitor/cli' {
31
34
  * @default true
32
35
  */
33
36
  enabled?: boolean;
37
+ /**
38
+ * The timeout in milliseconds for HTTP requests.
39
+ *
40
+ * @since 6.4.0
41
+ * @default 60000
42
+ */
43
+ httpTimeout?: number;
34
44
  /**
35
45
  * The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud).
36
46
  *
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * @since 5.0.0\n * @example '4100e356-e851-47fe-9d3c-b411eb325a99'\n */\n appId?: string;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * The default channel of the app.\n *\n * @since 6.3.0\n * @example 'production'\n */\n defaultChannel?: string;\n /**\n * Whether or not the plugin is enabled.\n *\n * @since 5.0.0\n * @default true\n */\n enabled?: boolean;\n /**\n * The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * @since 6.2.0\n * @default 'us'\n * @example 'eu'\n * @see https://capawesome.io/cloud/live-updates/advanced/privacy/\n */\n location?: 'us' | 'eu';\n /**\n * The public key to verify the integrity of the bundle.\n *\n * The public key must be a PEM-encoded RSA public key.\n *\n * @since 6.1.0\n * @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'\n */\n publicKey?: string;\n /**\n * The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle.\n *\n * @since 5.0.0\n * @default 10000\n */\n readyTimeout?: number;\n /**\n * Whether or not the app should be reset to the default bundle during an update.\n *\n * @since 5.0.0\n * @default true\n */\n resetOnUpdate?: boolean;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Get the active bundle identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundle(): Promise<GetBundleResult>;\n /**\n * Get all available bundle identifiers.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the device identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Get the version code of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * **Attention**: This method should be called as soon as the app is ready to use\n * to prevent the app from being reset to the default bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<void>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setBundle(options: SetBundleOptions): Promise<void>;\n /**\n * Set the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(): Promise<SyncResult>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The unique identifier of the bundle.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n /**\n * The checksum of the bundle to verify the integrity of the ZIP file.\n *\n * Must be a SHA-256 hash in hexadecimal format.\n *\n * @since 6.1.0\n * @example 'aab8e0a312e807c8eaeb923e2bdccd7731c5508869c62ad8e6e43ee881414c84'\n */\n checksum?: string;\n /**\n * The URL of the bundle to download.\n *\n * The bundle must be a ZIP file containing at least a `index.html` file.\n *\n * @since 5.0.0\n * @example 'https://example.com/bundle.zip'\n */\n url: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundleResult {\n /**\n * The unique identifier of the active bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundlesResult {\n /**\n * An array of unique identifiers of all available bundles.\n *\n * @since 5.0.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetChannelResult {\n /**\n * The channel of the app.\n *\n * If `null`, the app is using the default channel.\n *\n * @since 5.0.0\n * @example 'production'\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetDeviceIdResult {\n /**\n * The unique identifier of the device.\n *\n * On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used.\n * The value of this property is the same for apps that come from the same vendor running on the same device.\n *\n * @since 5.0.0\n *\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n deviceId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionCodeResult {\n /**\n * The version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1\"\n */\n versionCode: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionNameResult {\n /**\n * The version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1.0.0\"\n */\n versionName: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetCustomIdResult {\n /**\n * The custom identifier of the app.\n *\n * If `null`, no custom identifier is set.\n *\n * @since 5.0.0\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetChannelOptions {\n /**\n * The channel of the app.\n *\n * Set `null` to remove the channel.\n *\n * @since 5.0.0\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetCustomIdOptions {\n /**\n * The custom identifier of the app.\n *\n * Set `null` to remove the custom identifier.\n *\n * @since 5.0.0\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SyncResult {\n /**\n * The identifier of the next bundle to use.\n *\n * If `null`, the app is up-to-date and no new bundle is available.\n *\n * @since 5.0.0\n */\n nextBundleId: string | null;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * This is **NOT** the same as the app identifier (e.g. `com.example.app`).\n * This is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`).\n *\n * @since 5.0.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId?: string;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * The default channel of the app.\n *\n * @since 6.3.0\n * @example 'production'\n */\n defaultChannel?: string;\n /**\n * Whether or not the plugin is enabled.\n *\n * @since 5.0.0\n * @default true\n */\n enabled?: boolean;\n /**\n * The timeout in milliseconds for HTTP requests.\n *\n * @since 6.4.0\n * @default 60000\n */\n httpTimeout?: number;\n /**\n * The location of the server to use when using [Capawesome Cloud](https://capawesome.io/cloud).\n *\n * @since 6.2.0\n * @default 'us'\n * @example 'eu'\n * @see https://capawesome.io/cloud/live-updates/advanced/privacy/\n */\n location?: 'us' | 'eu';\n /**\n * The public key to verify the integrity of the bundle.\n *\n * The public key must be a PEM-encoded RSA public key.\n *\n * @since 6.1.0\n * @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'\n */\n publicKey?: string;\n /**\n * The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle.\n *\n * @since 5.0.0\n * @default 10000\n */\n readyTimeout?: number;\n /**\n * Whether or not the app should be reset to the default bundle during an update.\n *\n * @since 5.0.0\n * @default true\n */\n resetOnUpdate?: boolean;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Get the active bundle identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundle(): Promise<GetBundleResult>;\n /**\n * Get all available bundle identifiers.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the device identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Get the version code of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * **Attention**: This method should be called as soon as the app is ready to use\n * to prevent the app from being reset to the default bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<void>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setBundle(options: SetBundleOptions): Promise<void>;\n /**\n * Set the channel of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the custom identifier of the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(): Promise<SyncResult>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The unique identifier of the bundle.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n /**\n * The checksum of the bundle to verify the integrity of the ZIP file.\n *\n * Must be a SHA-256 hash in hexadecimal format.\n *\n * @since 6.1.0\n * @example 'aab8e0a312e807c8eaeb923e2bdccd7731c5508869c62ad8e6e43ee881414c84'\n */\n checksum?: string;\n /**\n * The URL of the bundle to download.\n *\n * The bundle must be a ZIP file containing at least a `index.html` file.\n *\n * @since 5.0.0\n * @example 'https://example.com/bundle.zip'\n */\n url: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundleResult {\n /**\n * The unique identifier of the active bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundlesResult {\n /**\n * An array of unique identifiers of all available bundles.\n *\n * @since 5.0.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetChannelResult {\n /**\n * The channel of the app.\n *\n * If `null`, the app is using the default channel.\n *\n * @since 5.0.0\n * @example 'production'\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetDeviceIdResult {\n /**\n * The unique identifier of the device.\n *\n * On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used.\n * The value of this property is the same for apps that come from the same vendor running on the same device.\n *\n * @since 5.0.0\n *\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n deviceId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionCodeResult {\n /**\n * The version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1\"\n */\n versionCode: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionNameResult {\n /**\n * The version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1.0.0\"\n */\n versionName: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetCustomIdResult {\n /**\n * The custom identifier of the app.\n *\n * If `null`, no custom identifier is set.\n *\n * @since 5.0.0\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetChannelOptions {\n /**\n * The channel of the app.\n *\n * Set `null` to remove the channel.\n *\n * @since 5.0.0\n */\n channel: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetCustomIdOptions {\n /**\n * The custom identifier of the app.\n *\n * Set `null` to remove the custom identifier.\n *\n * @since 5.0.0\n */\n customId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SyncResult {\n /**\n * The identifier of the next bundle to use.\n *\n * If `null`, the app is up-to-date and no new bundle is available.\n *\n * @since 5.0.0\n */\n nextBundleId: string | null;\n}\n"]}
@@ -11,8 +11,10 @@ public enum CustomError: Error {
11
11
  case checksumMismatch
12
12
  case customIdMissing
13
13
  case downloadFailed
14
+ case httpTimeout
14
15
  case signatureMissing
15
16
  case signatureVerificationFailed
17
+ case syncInProgress
16
18
  case urlMissing
17
19
  }
18
20
 
@@ -39,10 +41,14 @@ extension CustomError: LocalizedError {
39
41
  return NSLocalizedString("customId must be provided.", comment: "customIdMissing")
40
42
  case .downloadFailed:
41
43
  return NSLocalizedString("Bundle could not be downloaded.", comment: "downloadFailed")
44
+ case .httpTimeout:
45
+ return NSLocalizedString("Request timed out.", comment: "httpTimeout")
42
46
  case .signatureMissing:
43
47
  return NSLocalizedString("Bundle does not contain a signature.", comment: "signatureMissing")
44
48
  case .signatureVerificationFailed:
45
49
  return NSLocalizedString("Signature verification failed.", comment: "signatureVerificationFailed")
50
+ case .syncInProgress:
51
+ return NSLocalizedString("Sync is already in progress.", comment: "syncInProgress")
46
52
  case .urlMissing:
47
53
  return NSLocalizedString("url must be provided.", comment: "urlMissing")
48
54
  }
@@ -356,11 +356,22 @@ import CommonCrypto
356
356
  let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
357
357
  return (temporaryZipFileUrl, [.createIntermediateDirectories])
358
358
  }
359
-
360
- AF.download(url, to: destination).validate().response { response in
359
+ var request = URLRequest(url: URL(string: url)!)
360
+ request.httpMethod = HTTPMethod.get.rawValue
361
+ request.timeoutInterval = Double(config.httpTimeout) / 1000.0
362
+ AF.download(request, to: destination).validate().response { response in
361
363
  if let error = response.error {
362
364
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
363
- completion(nil, CustomError.downloadFailed)
365
+ if let urlError = error.underlyingError as? URLError {
366
+ switch urlError.code {
367
+ case .timedOut:
368
+ completion(nil, CustomError.httpTimeout)
369
+ default:
370
+ completion(nil, CustomError.downloadFailed)
371
+ }
372
+ } else {
373
+ completion(nil, CustomError.downloadFailed)
374
+ }
364
375
  return
365
376
  }
366
377
  completion(response.fileURL, nil)
@@ -384,15 +395,28 @@ import CommonCrypto
384
395
  host = "api.cloud.capawesome.eu"
385
396
  }
386
397
  }
387
- let url = URL(string: "https://\(host)/v1/apps/\(config.appId ?? "")/bundles/latest")!
388
- AF.request(url, method: .get, parameters: parameters).validate().responseDecodable(of: GetLatestBundleResponse.self) { response in
398
+ var urlComponents = URLComponents(string: "https://\(host)/v1/apps/\(config.appId ?? "")/bundles/latest")!
399
+ urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
400
+ var request = URLRequest(url: urlComponents.url!)
401
+ request.httpMethod = HTTPMethod.get.rawValue
402
+ request.timeoutInterval = Double(config.httpTimeout) / 1000.0
403
+ AF.request(request).validate().responseDecodable(of: GetLatestBundleResponse.self) { response in
389
404
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle from ", response.request?.url?.absoluteString ?? "")
390
405
  if let error = response.error {
391
406
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
392
407
  if let data = response.data {
393
408
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", String(decoding: data, as: UTF8.self))
394
409
  }
395
- completion(nil, error)
410
+ if let urlError = error.underlyingError as? URLError {
411
+ switch urlError.code {
412
+ case .timedOut:
413
+ completion(nil, CustomError.httpTimeout)
414
+ default:
415
+ completion(nil, error)
416
+ }
417
+ } else {
418
+ completion(nil, error)
419
+ }
396
420
  return
397
421
  }
398
422
  completion(response.value, nil)
@@ -3,6 +3,7 @@ public struct LiveUpdateConfig {
3
3
  var autoDeleteBundles = false
4
4
  var defaultChannel: String?
5
5
  var enabled = true
6
+ var httpTimeout = 60000
6
7
  var location: String?
7
8
  var publicKey: String?
8
9
  var readyTimeout = 10000
@@ -8,11 +8,12 @@ import Capacitor
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin {
10
10
  public static let tag = "LiveUpdate"
11
- public static let version = "6.3.0"
11
+ public static let version = "6.4.0"
12
12
  public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
13
13
 
14
14
  private var config: LiveUpdateConfig?
15
15
  private var implementation: LiveUpdate?
16
+ private var syncInProgress = false
16
17
 
17
18
  override public func load() {
18
19
  self.config = liveUpdateConfig()
@@ -222,7 +223,15 @@ public class LiveUpdatePlugin: CAPPlugin {
222
223
  call.reject(CustomError.appIdMissing.localizedDescription)
223
224
  return
224
225
  }
226
+
227
+ guard !syncInProgress else {
228
+ call.reject(CustomError.syncInProgress.localizedDescription)
229
+ return
230
+ }
231
+ syncInProgress = true
232
+
225
233
  implementation?.sync(completion: { result, error in
234
+ self.syncInProgress = false
226
235
  if let error = error {
227
236
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
228
237
  call.reject(error.localizedDescription)
@@ -241,6 +250,7 @@ public class LiveUpdatePlugin: CAPPlugin {
241
250
  config.autoDeleteBundles = getConfig().getBoolean("autoDeleteBundles", config.autoDeleteBundles)
242
251
  config.defaultChannel = getConfig().getString("defaultChannel", config.defaultChannel)
243
252
  config.enabled = getConfig().getBoolean("enabled", config.enabled)
253
+ config.httpTimeout = getConfig().getInt("httpTimeout", config.httpTimeout)
244
254
  config.location = getConfig().getString("location", config.location)
245
255
  config.publicKey = getConfig().getString("publicKey", config.publicKey)
246
256
  config.readyTimeout = getConfig().getInt("readyTimeout", config.readyTimeout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",