@capawesome/capacitor-live-update 7.3.0 → 8.0.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.
Files changed (33) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +3 -3
  2. package/Package.swift +4 -4
  3. package/README.md +235 -3
  4. package/android/build.gradle +10 -10
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +519 -246
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +33 -5
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +134 -3
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +25 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +2 -2
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
  15. package/dist/docs.json +319 -0
  16. package/dist/esm/definitions.d.ts +167 -2
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/web.d.ts +8 -1
  19. package/dist/esm/web.js +21 -0
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +21 -0
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +21 -0
  24. package/dist/plugin.js.map +1 -1
  25. package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
  26. package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
  27. package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
  28. package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
  29. package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
  30. package/ios/Plugin/LiveUpdate.swift +77 -4
  31. package/ios/Plugin/LiveUpdatePlugin.swift +78 -1
  32. package/ios/Plugin/LiveUpdatePreferences.swift +44 -2
  33. package/package.json +10 -10
@@ -4,9 +4,13 @@ import androidx.annotation.NonNull;
4
4
  import androidx.annotation.Nullable;
5
5
  import com.getcapacitor.Logger;
6
6
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
7
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
8
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
7
9
  import java.io.File;
8
10
  import java.io.IOException;
9
11
  import java.util.concurrent.TimeUnit;
12
+ import okhttp3.Call;
13
+ import okhttp3.Callback;
10
14
  import okhttp3.OkHttpClient;
11
15
  import okhttp3.Request;
12
16
  import okhttp3.Response;
@@ -21,6 +25,9 @@ public class LiveUpdateHttpClient {
21
25
  @NonNull
22
26
  private final LiveUpdateConfig config;
23
27
 
28
+ @NonNull
29
+ private final OkHttpClient okHttpClient;
30
+
24
31
  @Nullable
25
32
  public static String getChecksumFromResponse(Response response) {
26
33
  String checksum = response.header("X-Checksum");
@@ -41,17 +48,38 @@ public class LiveUpdateHttpClient {
41
48
 
42
49
  public LiveUpdateHttpClient(@NonNull LiveUpdateConfig config) {
43
50
  this.config = config;
44
- }
45
-
46
- public Response execute(String url) throws IOException {
47
51
  int httpTimeout = config.getHttpTimeout();
48
- OkHttpClient okHttpClient = new OkHttpClient.Builder()
52
+
53
+ // Increase max requests per host to allow multiple parallel downloads from the same host
54
+ okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher();
55
+ dispatcher.setMaxRequestsPerHost(30);
56
+
57
+ this.okHttpClient = new OkHttpClient.Builder()
58
+ .dispatcher(dispatcher)
49
59
  .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
50
60
  .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
51
61
  .writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
52
62
  .build();
63
+ }
64
+
65
+ public Call enqueue(String url, NonEmptyCallback<Response> callback) {
53
66
  Request request = new Request.Builder().url(url).build();
54
- return okHttpClient.newCall(request).execute();
67
+
68
+ Call call = okHttpClient.newCall(request);
69
+ call.enqueue(
70
+ new Callback() {
71
+ @Override
72
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
73
+ callback.success(response);
74
+ }
75
+
76
+ @Override
77
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
78
+ callback.error(new Exception(e));
79
+ }
80
+ }
81
+ );
82
+ return call;
55
83
  }
56
84
 
57
85
  public static void writeResponseBodyToFile(ResponseBody body, File file, @Nullable DownloadProgressCallback callback)
@@ -16,11 +16,17 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundle
16
16
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
17
17
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
18
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
19
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
19
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
20
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
21
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
23
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
24
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
25
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
22
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
27
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
23
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
29
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
24
30
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
25
31
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
26
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
@@ -29,7 +35,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
29
35
  public class LiveUpdatePlugin extends Plugin {
30
36
 
31
37
  public static final String TAG = "LiveUpdate";
32
- public static final String VERSION = "7.3.0";
38
+ public static final String VERSION = "7.4.0";
33
39
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
34
40
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
35
41
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
@@ -49,6 +55,7 @@ public class LiveUpdatePlugin extends Plugin {
49
55
  public static final String ERROR_UNKNOWN_ERROR = "An unknown error has occurred.";
50
56
  public static final String EVENT_DOWNLOAD_BUNDLE_PROGRESS = "downloadBundleProgress";
51
57
  public static final String EVENT_NEXT_BUNDLE_SET = "nextBundleSet";
58
+ public static final String EVENT_RELOADED = "reloaded";
52
59
 
53
60
  @Nullable
54
61
  private LiveUpdateConfig config;
@@ -97,6 +104,16 @@ public class LiveUpdatePlugin extends Plugin {
97
104
  }
98
105
  }
99
106
 
107
+ @PluginMethod
108
+ public void clearBlockedBundles(PluginCall call) {
109
+ try {
110
+ implementation.clearBlockedBundles();
111
+ resolveCall(call);
112
+ } catch (Exception exception) {
113
+ rejectCall(call, exception);
114
+ }
115
+ }
116
+
100
117
  @PluginMethod
101
118
  public void deleteBundle(PluginCall call) {
102
119
  try {
@@ -171,9 +188,9 @@ public class LiveUpdatePlugin extends Plugin {
171
188
  }
172
189
 
173
190
  FetchLatestBundleOptions options = new FetchLatestBundleOptions(call);
174
- NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
191
+ NonEmptyCallback<FetchLatestBundleResult> callback = new NonEmptyCallback<>() {
175
192
  @Override
176
- public void success(Result result) {
193
+ public void success(FetchLatestBundleResult result) {
177
194
  resolveCall(call, result.toJSObject());
178
195
  }
179
196
 
@@ -189,6 +206,28 @@ public class LiveUpdatePlugin extends Plugin {
189
206
  }
190
207
  }
191
208
 
209
+ @PluginMethod
210
+ public void getBlockedBundles(PluginCall call) {
211
+ try {
212
+ NonEmptyCallback<GetBlockedBundlesResult> callback = new NonEmptyCallback<>() {
213
+ @Override
214
+ public void success(GetBlockedBundlesResult result) {
215
+ resolveCall(call, result.toJSObject());
216
+ }
217
+
218
+ @Override
219
+ public void error(Exception exception) {
220
+ rejectCall(call, exception);
221
+ }
222
+ };
223
+
224
+ assert implementation != null;
225
+ implementation.getBlockedBundles(callback);
226
+ } catch (Exception exception) {
227
+ rejectCall(call, exception);
228
+ }
229
+ }
230
+
192
231
  @PluginMethod
193
232
  public void getBundles(PluginCall call) {
194
233
  try {
@@ -210,6 +249,28 @@ public class LiveUpdatePlugin extends Plugin {
210
249
  }
211
250
  }
212
251
 
252
+ @PluginMethod
253
+ public void getDownloadedBundles(PluginCall call) {
254
+ try {
255
+ NonEmptyCallback<GetDownloadedBundlesResult> callback = new NonEmptyCallback<>() {
256
+ @Override
257
+ public void success(GetDownloadedBundlesResult result) {
258
+ resolveCall(call, result.toJSObject());
259
+ }
260
+
261
+ @Override
262
+ public void error(Exception exception) {
263
+ rejectCall(call, exception);
264
+ }
265
+ };
266
+
267
+ assert implementation != null;
268
+ implementation.getDownloadedBundles(callback);
269
+ } catch (Exception exception) {
270
+ rejectCall(call, exception);
271
+ }
272
+ }
273
+
213
274
  @PluginMethod
214
275
  public void getChannel(PluginCall call) {
215
276
  try {
@@ -231,6 +292,28 @@ public class LiveUpdatePlugin extends Plugin {
231
292
  }
232
293
  }
233
294
 
295
+ @PluginMethod
296
+ public void getConfig(PluginCall call) {
297
+ try {
298
+ NonEmptyCallback<GetConfigResult> callback = new NonEmptyCallback<>() {
299
+ @Override
300
+ public void success(GetConfigResult result) {
301
+ resolveCall(call, result.toJSObject());
302
+ }
303
+
304
+ @Override
305
+ public void error(Exception exception) {
306
+ rejectCall(call, exception);
307
+ }
308
+ };
309
+
310
+ assert implementation != null;
311
+ implementation.getConfig(callback);
312
+ } catch (Exception exception) {
313
+ rejectCall(call, exception);
314
+ }
315
+ }
316
+
234
317
  @PluginMethod
235
318
  public void getCurrentBundle(PluginCall call) {
236
319
  try {
@@ -359,6 +442,28 @@ public class LiveUpdatePlugin extends Plugin {
359
442
  }
360
443
  }
361
444
 
445
+ @PluginMethod
446
+ public void isSyncing(PluginCall call) {
447
+ try {
448
+ NonEmptyCallback<IsSyncingResult> callback = new NonEmptyCallback<>() {
449
+ @Override
450
+ public void success(IsSyncingResult result) {
451
+ resolveCall(call, result.toJSObject());
452
+ }
453
+
454
+ @Override
455
+ public void error(Exception exception) {
456
+ rejectCall(call, exception);
457
+ }
458
+ };
459
+
460
+ assert implementation != null;
461
+ implementation.isSyncing(callback);
462
+ } catch (Exception exception) {
463
+ rejectCall(call, exception);
464
+ }
465
+ }
466
+
362
467
  @PluginMethod
363
468
  public void ready(PluginCall call) {
364
469
  try {
@@ -400,6 +505,16 @@ public class LiveUpdatePlugin extends Plugin {
400
505
  }
401
506
  }
402
507
 
508
+ @PluginMethod
509
+ public void resetConfig(PluginCall call) {
510
+ try {
511
+ implementation.resetConfig();
512
+ resolveCall(call);
513
+ } catch (Exception exception) {
514
+ rejectCall(call, exception);
515
+ }
516
+ }
517
+
403
518
  @PluginMethod
404
519
  public void setChannel(PluginCall call) {
405
520
  try {
@@ -424,6 +539,17 @@ public class LiveUpdatePlugin extends Plugin {
424
539
  }
425
540
  }
426
541
 
542
+ @PluginMethod
543
+ public void setConfig(PluginCall call) {
544
+ try {
545
+ SetConfigOptions options = new SetConfigOptions(call);
546
+ implementation.setConfig(options);
547
+ resolveCall(call);
548
+ } catch (Exception exception) {
549
+ rejectCall(call, exception);
550
+ }
551
+ }
552
+
427
553
  @PluginMethod
428
554
  public void setCustomId(PluginCall call) {
429
555
  try {
@@ -510,6 +636,11 @@ public class LiveUpdatePlugin extends Plugin {
510
636
  notifyListeners(EVENT_NEXT_BUNDLE_SET, event.toJSObject(), false);
511
637
  }
512
638
 
639
+ public void notifyReloadedListeners() {
640
+ JSObject event = new JSObject();
641
+ notifyListeners(EVENT_RELOADED, event, true);
642
+ }
643
+
513
644
  private LiveUpdateConfig getLiveUpdateConfig() {
514
645
  LiveUpdateConfig config = new LiveUpdateConfig();
515
646
 
@@ -15,10 +15,12 @@ public class LiveUpdatePreferences {
15
15
  @NonNull
16
16
  private final SharedPreferences.Editor settingsEditor;
17
17
 
18
+ private final String appIdKey = "appId"; // DO NOT CHANGE
18
19
  private final String blockedBundleIdsKey = "blockedBundleIds"; // DO NOT CHANGE
19
20
  private final String channelKey = "channel"; // DO NOT CHANGE
20
21
  private final String deviceIdKey = "deviceId"; // DO NOT CHANGE
21
22
  private final String customIdKey = "customId"; // DO NOT CHANGE
23
+ private final String lastVersionCodeKey = "lastVersionCode"; // DO NOT CHANGE
22
24
  private final String previousBundleIdKey = "previousBundleId"; // DO NOT CHANGE
23
25
 
24
26
  public LiveUpdatePreferences(@NonNull Context context) {
@@ -26,6 +28,11 @@ public class LiveUpdatePreferences {
26
28
  this.settingsEditor = context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).edit();
27
29
  }
28
30
 
31
+ @Nullable
32
+ public String getAppId() {
33
+ return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(appIdKey, null);
34
+ }
35
+
29
36
  @Nullable
30
37
  public String getChannel() {
31
38
  return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(channelKey, null);
@@ -49,6 +56,10 @@ public class LiveUpdatePreferences {
49
56
  }
50
57
  }
51
58
 
59
+ public int getLastVersionCode() {
60
+ return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getInt(lastVersionCodeKey, -1);
61
+ }
62
+
52
63
  @Nullable
53
64
  public String getPreviousBundleId() {
54
65
  return context
@@ -63,6 +74,15 @@ public class LiveUpdatePreferences {
63
74
  .getString(blockedBundleIdsKey, null);
64
75
  }
65
76
 
77
+ public void setAppId(@Nullable String appId) {
78
+ if (appId == null) {
79
+ settingsEditor.remove(appIdKey);
80
+ } else {
81
+ settingsEditor.putString(appIdKey, appId);
82
+ }
83
+ settingsEditor.apply();
84
+ }
85
+
66
86
  public void setBlockedBundleIds(@Nullable String blockedBundleIds) {
67
87
  if (blockedBundleIds == null) {
68
88
  settingsEditor.remove(blockedBundleIdsKey);
@@ -99,6 +119,11 @@ public class LiveUpdatePreferences {
99
119
  settingsEditor.apply();
100
120
  }
101
121
 
122
+ public void setLastVersionCode(int versionCode) {
123
+ settingsEditor.putInt(lastVersionCodeKey, versionCode);
124
+ settingsEditor.apply();
125
+ }
126
+
102
127
  public void setPreviousBundleId(@Nullable String bundleId) {
103
128
  if (bundleId == null) {
104
129
  settingsEditor.remove(previousBundleIdKey);
@@ -36,9 +36,9 @@ public class GetLatestBundleResponse {
36
36
  this.artifactType = ArtifactType.ZIP;
37
37
  }
38
38
  this.bundleId = responseJson.optString("bundleId");
39
- this.checksum = responseJson.optString("checksum");
39
+ this.checksum = responseJson.isNull("checksum") ? null : responseJson.optString("checksum");
40
40
  this.customProperties = responseJson.optJSONObject("customProperties");
41
- this.signature = responseJson.optString("signature");
41
+ this.signature = responseJson.isNull("signature") ? null : responseJson.optString("signature");
42
42
  this.url = responseJson.optString("url");
43
43
  }
44
44
 
@@ -0,0 +1,19 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes.options;
2
+
3
+ import androidx.annotation.Nullable;
4
+ import com.getcapacitor.PluginCall;
5
+
6
+ public class SetConfigOptions {
7
+
8
+ @Nullable
9
+ private final String appId;
10
+
11
+ public SetConfigOptions(PluginCall call) {
12
+ this.appId = call.getString("appId");
13
+ }
14
+
15
+ @Nullable
16
+ public String getAppId() {
17
+ return appId;
18
+ }
19
+ }
@@ -0,0 +1,27 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSArray;
5
+ import com.getcapacitor.JSObject;
6
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
7
+
8
+ public class GetBlockedBundlesResult implements Result {
9
+
10
+ @NonNull
11
+ private String[] bundleIds;
12
+
13
+ public GetBlockedBundlesResult(@NonNull String[] bundleIds) {
14
+ this.bundleIds = bundleIds;
15
+ }
16
+
17
+ public JSObject toJSObject() {
18
+ JSArray bundleIdsResult = new JSArray();
19
+ for (String bundleId : bundleIds) {
20
+ bundleIdsResult.put(bundleId);
21
+ }
22
+
23
+ JSObject result = new JSObject();
24
+ result.put("bundleIds", bundleIdsResult);
25
+ return result;
26
+ }
27
+ }
@@ -0,0 +1,40 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+ import com.getcapacitor.JSObject;
6
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
7
+ import org.json.JSONObject;
8
+
9
+ public class GetConfigResult implements Result {
10
+
11
+ @Nullable
12
+ private final String appId;
13
+
14
+ @NonNull
15
+ private final String autoUpdateStrategy;
16
+
17
+ public GetConfigResult(@Nullable String appId, @NonNull String autoUpdateStrategy) {
18
+ this.appId = appId;
19
+ this.autoUpdateStrategy = autoUpdateStrategy;
20
+ }
21
+
22
+ @Nullable
23
+ public String getAppId() {
24
+ return appId;
25
+ }
26
+
27
+ @NonNull
28
+ public String getAutoUpdateStrategy() {
29
+ return autoUpdateStrategy;
30
+ }
31
+
32
+ @Override
33
+ @NonNull
34
+ public JSObject toJSObject() {
35
+ JSObject result = new JSObject();
36
+ result.put("appId", appId == null ? JSONObject.NULL : appId);
37
+ result.put("autoUpdateStrategy", autoUpdateStrategy);
38
+ return result;
39
+ }
40
+ }
@@ -0,0 +1,27 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSArray;
5
+ import com.getcapacitor.JSObject;
6
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
7
+
8
+ public class GetDownloadedBundlesResult implements Result {
9
+
10
+ @NonNull
11
+ private String[] bundleIds;
12
+
13
+ public GetDownloadedBundlesResult(@NonNull String[] bundleIds) {
14
+ this.bundleIds = bundleIds;
15
+ }
16
+
17
+ public JSObject toJSObject() {
18
+ JSArray bundleIdsResult = new JSArray();
19
+ for (String bundleId : bundleIds) {
20
+ bundleIdsResult.put(bundleId);
21
+ }
22
+
23
+ JSObject result = new JSObject();
24
+ result.put("bundleIds", bundleIdsResult);
25
+ return result;
26
+ }
27
+ }
@@ -0,0 +1,26 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
6
+
7
+ public class IsSyncingResult implements Result {
8
+
9
+ private final boolean syncing;
10
+
11
+ public IsSyncingResult(boolean syncing) {
12
+ this.syncing = syncing;
13
+ }
14
+
15
+ public boolean getSyncing() {
16
+ return syncing;
17
+ }
18
+
19
+ @Override
20
+ @NonNull
21
+ public JSObject toJSObject() {
22
+ JSObject result = new JSObject();
23
+ result.put("syncing", syncing);
24
+ return result;
25
+ }
26
+ }