cordova-plugin-reteno 2.2.0 → 2.2.2

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 (38) hide show
  1. package/README.md +2 -4
  2. package/docs/Cordova/Action Buttons/README.md +62 -0
  3. package/docs/Cordova/AppInbox/README.md +55 -0
  4. package/docs/Cordova/AppLifeCycleEvents/README.md +80 -0
  5. package/docs/Cordova/Ecommerce/README.md +144 -0
  6. package/docs/Cordova/InAppMessages/README.md +72 -0
  7. package/docs/Cordova/Push notification/README.md +129 -0
  8. package/docs/Cordova/Recommendations/README.md +62 -0
  9. package/docs/Cordova/SetupGuide/Android.md +135 -0
  10. package/docs/Cordova/SetupGuide/IOS.md +325 -0
  11. package/docs/Cordova/SetupGuide/README.md +91 -0
  12. package/docs/Cordova/Tracking user behaviour/README.md +52 -0
  13. package/docs/Cordova/Tracking user information/README.md +112 -0
  14. package/docs/Ionic/Action Buttons/README.md +66 -0
  15. package/docs/Ionic/AppInbox/README.md +61 -0
  16. package/docs/Ionic/AppLifeCycleEvents/README.md +77 -0
  17. package/docs/Ionic/Ecommerce/README.md +139 -0
  18. package/docs/Ionic/InAppMessages/README.md +65 -0
  19. package/docs/Ionic/Push notification/README.md +133 -0
  20. package/docs/Ionic/Recommendations/README.md +46 -0
  21. package/docs/Ionic/SetupGuide/Android.md +173 -0
  22. package/docs/Ionic/SetupGuide/IOS.md +354 -0
  23. package/docs/Ionic/SetupGuide/README.md +167 -0
  24. package/docs/Ionic/Tracking user behaviour/README.md +31 -0
  25. package/docs/Ionic/Tracking user information/README.md +98 -0
  26. package/package.json +1 -1
  27. package/plugin.xml +20 -2
  28. package/scripts/ios-podfile-use-modular-headers.js +109 -0
  29. package/scripts/validate-install.js +37 -9
  30. package/src/android/app/build.gradle +1 -1
  31. package/src/android/app/src/main/java/com/reteno/plugin/RetenoPlugin.java +29 -9
  32. package/src/android/reteno.gradle +19 -9
  33. package/src/ios/RetenoPlugin.swift +37 -5
  34. package/src/ios/RetenoPluginBootstrap.m +117 -0
  35. package/types/index.ts +1 -1
  36. package/docs/android.md +0 -264
  37. package/docs/api.md +0 -734
  38. package/docs/ios.md +0 -495
@@ -0,0 +1,31 @@
1
+ # Tracking user behaviour
2
+
3
+ ## Track custom events
4
+
5
+ ```ts
6
+ await this.reteno.logEvent({
7
+ eventName: 'EVENT_NAME',
8
+ date: new Date().toISOString(),
9
+ parameters: [{ name: 'Additional parameter', value: 'Additional value' }],
10
+ });
11
+ ```
12
+
13
+ ## Force push cached data
14
+
15
+ ```ts
16
+ await this.reteno.forcePushData();
17
+ ```
18
+
19
+ On iOS this uses a technical `logEvent(..., forcePush: true)` call under the hood.
20
+
21
+ ## Log screen view events
22
+
23
+ ```ts
24
+ await this.reteno.logScreenView('HomeScreen');
25
+ ```
26
+
27
+ ## Lifecycle tracking options
28
+
29
+ See configuration and examples in:
30
+
31
+ - [AppLifeCycleEvents/README.md](../AppLifeCycleEvents/README.md)
@@ -0,0 +1,98 @@
1
+ # Tracking user information
2
+
3
+ Use `AwesomeCordovaPluginReteno` service methods.
4
+
5
+ ## Set user attributes (identified user)
6
+
7
+ ```ts
8
+ await this.reteno.setUserAttributes({
9
+ externalUserId: 'USER_ID',
10
+ user: {
11
+ userAttributes: {
12
+ email: 'john.doe@example.com',
13
+ phone: '+1234567890',
14
+ firstName: 'John',
15
+ lastName: 'Doe',
16
+ languageCode: 'en',
17
+ timeZone: 'Europe/Kyiv',
18
+ marketId: 'market_1',
19
+ address: {
20
+ region: 'Kyivska',
21
+ town: 'Kyiv',
22
+ address: 'Khreshchatyk St, 1',
23
+ postcode: '01001',
24
+ },
25
+ fields: [{ key: 'plan', value: 'premium' }],
26
+ },
27
+ subscriptionKeys: ['news', 'promotions'],
28
+ groupNamesInclude: ['beta-testers'],
29
+ groupNamesExclude: ['unsubscribed'],
30
+ },
31
+ });
32
+ ```
33
+
34
+ ## Set anonymous user attributes
35
+
36
+ ```ts
37
+ await this.reteno.setAnonymousUserAttributes({
38
+ firstName: 'John',
39
+ lastName: 'Doe',
40
+ languageCode: 'en',
41
+ timeZone: 'Europe/Kyiv',
42
+ marketId: 'market_1',
43
+ address: {
44
+ region: 'Kyivska',
45
+ town: 'Kyiv',
46
+ address: 'Khreshchatyk St, 1',
47
+ postcode: '01001',
48
+ },
49
+ fields: [{ key: 'utm_source', value: 'google' }],
50
+ });
51
+ ```
52
+
53
+ Anonymous attributes cannot include `phone` or `email`.
54
+
55
+ ## Multi-account mode
56
+
57
+ ```ts
58
+ await this.reteno.setMultiAccountUserAttributes({
59
+ externalUserId: 'user-123',
60
+ user: {
61
+ userAttributes: {
62
+ email: 'john.doe@example.com',
63
+ firstName: 'John',
64
+ lastName: 'Doe',
65
+ marketId: 'market_1',
66
+ },
67
+ },
68
+ });
69
+ ```
70
+
71
+ ## Market ID
72
+
73
+ `marketId` requires `cordova-plugin-reteno@2.1.0`. The
74
+ `awesome-cordova-plugins-reteno@9.2.0` wrapper includes its TypeScript declaration. It is
75
+ supported by `setUserAttributes`, `setAnonymousUserAttributes`, and
76
+ `setMultiAccountUserAttributes`.
77
+
78
+ - The value can contain up to 64 Latin letters, digits, hyphens (`-`), and underscores (`_`).
79
+ - Pass an empty string to clear the existing value:
80
+
81
+ ```ts
82
+ await this.reteno.setUserAttributes({
83
+ externalUserId: 'USER_ID',
84
+ user: {
85
+ userAttributes: {
86
+ marketId: '',
87
+ },
88
+ },
89
+ });
90
+ ```
91
+
92
+ - Omit `marketId` to keep the existing value unchanged.
93
+ - The feature requires Reteno Android SDK 2.9.5+ or Reteno iOS SDK 2.7.1+. These versions are included in `cordova-plugin-reteno@2.1.0`.
94
+
95
+ ## Notes
96
+
97
+ - `languageCode`: RFC 5646 (example: `de-AT`)
98
+ - `timeZone`: TZ database value (example: `Europe/Kyiv`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-reteno",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "The Reteno Cordova Plugin for Mobile Customer Engagement and Analytics solutions",
5
5
  "types": "./types/index.ts",
6
6
  "files": [
package/plugin.xml CHANGED
@@ -1,16 +1,20 @@
1
1
  <?xml version='1.0' encoding='utf-8'?>
2
- <plugin id="cordova-plugin-reteno" version="2.2.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
2
+ <plugin id="cordova-plugin-reteno" version="2.2.2" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
3
3
  <name>cordova-plugin-reteno</name>
4
4
  <engines>
5
5
  <engine name="cordova-android" version=">=12.0.0" />
6
6
  <engine name="cordova-ios" version=">=8.0.0" />
7
7
  </engines>
8
+ <preference name="RETENO_ACCESS_KEY" default="MISSING" />
9
+ <!-- Deprecated compatibility alias. Use RETENO_ACCESS_KEY for new integrations. -->
8
10
  <preference name="SDK_ACCESS_KEY" default="MISSING" />
9
11
  <preference name="IOS_DEVICE_TOKEN_HANDLING_MODE" default="manual" />
10
12
  <hook type="before_plugin_install" src="scripts/validate-install.js" />
11
13
  <hook type="after_plugin_install" src="scripts/android-fcm-setup.js" />
12
14
  <hook type="after_platform_add" src="scripts/android-fcm-setup.js" />
13
15
  <hook type="after_prepare" src="scripts/android-fcm-setup.js" />
16
+ <hook type="before_plugin_install" src="scripts/ios-podfile-use-modular-headers.js" />
17
+ <hook type="before_prepare" src="scripts/ios-podfile-use-modular-headers.js" />
14
18
  <js-module name="cordova-plugin-reteno" src="www/cordova-plugin-reteno.js">
15
19
  <clobbers target="RetenoPlugin" />
16
20
  </js-module>
@@ -21,6 +25,8 @@
21
25
  </config-file>
22
26
 
23
27
  <config-file target="AndroidManifest.xml" parent="/manifest/application">
28
+ <meta-data android:name="com.reteno.RETENO_ACCESS_KEY" android:value="$RETENO_ACCESS_KEY" />
29
+ <!-- Deprecated compatibility alias. -->
24
30
  <meta-data android:name="com.reteno.SDK_ACCESS_KEY" android:value="$SDK_ACCESS_KEY" />
25
31
  <provider
26
32
  android:name="com.reteno.plugin.RetenoNotificationGroupingRuleProvider"
@@ -67,12 +73,24 @@
67
73
  </config-file>
68
74
 
69
75
  <podspec>
76
+ <!--
77
+ Empty <config/> is required even without <source> entries: cordova-ios's
78
+ Api.js#removePodSpecs does `Object.keys(obj.sources)` unconditionally (unlike
79
+ addPodSpecs, which guards it) and crashes with "Cannot convert undefined or
80
+ null to object" on `cordova plugin rm` when <config> is absent, since
81
+ PluginInfo#getPodSpecs() then returns `sources: undefined` instead of `{}`.
82
+ That crash aborts the uninstall before it finishes, which then leaves iOS
83
+ plugin bookkeeping (plugins/ios.json) stale so a later reinstall thinks the
84
+ plugin is "already installed" and skips re-copying its www/ bridge file.
85
+ -->
86
+ <config></config>
70
87
  <pods>
71
- <pod name="Reteno" spec="2.7.3" />
88
+ <pod name="Reteno" spec="2.7.6" />
72
89
  <pod name="FirebaseMessaging" />
73
90
  </pods>
74
91
  </podspec>
75
92
 
76
93
  <source-file src="src/ios/RetenoPlugin.swift" />
94
+ <source-file src="src/ios/RetenoPluginBootstrap.m" />
77
95
  </platform>
78
96
  </plugin>
@@ -0,0 +1,109 @@
1
+ /*
2
+ * Cordova hook: add `use_modular_headers!` to platforms/ios/Podfile.
3
+ *
4
+ * cordova-ios auto-generates the Podfile and does not expose a config.xml/plugin.xml
5
+ * knob for arbitrary CocoaPods directives other than `use_frameworks!` and
6
+ * `inhibit_all_warnings!` (see cordova-ios/lib/Podfile.js declarationRegexpMap).
7
+ * Cordova links pods statically by default (no `use_frameworks!`), and recent
8
+ * Firebase pods (pulled in via `pod 'FirebaseMessaging'`) depend on GoogleUtilities,
9
+ * which does not define modules -- `pod install` refuses to proceed under static
10
+ * linking unless `use_modular_headers!` is set.
11
+ *
12
+ * The generated Podfile is fully rewritten on every `prepare`, but cordova-ios seeds
13
+ * its in-memory declarations by parsing whatever already sits between the
14
+ * `platform :ios, 'x.y'` and `target 'App' do` lines (any non-comment line there is
15
+ * treated as a raw declaration and re-emitted verbatim). So patching the file before
16
+ * cordova-ios's own prepare runs (before_prepare, ahead of `preparePlatforms`) is
17
+ * enough for our line to survive the regeneration.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const DECLARATION = 'use_modular_headers!';
24
+
25
+ function exists(filePath) {
26
+ try {
27
+ fs.accessSync(filePath, fs.constants.F_OK);
28
+ return true;
29
+ } catch (_) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function alreadyPresent(text) {
35
+ return new RegExp(`^\\s*${DECLARATION}\\s*$`, 'm').test(text);
36
+ }
37
+
38
+ function insertIntoExisting(text) {
39
+ const platformLineRe = /(platform :ios,\s*'[^']+'\s*\n)/;
40
+ if (!platformLineRe.test(text)) return null;
41
+
42
+ return text.replace(platformLineRe, `$1${DECLARATION}\n`);
43
+ }
44
+
45
+ function seedTemplate() {
46
+ return (
47
+ '# DO NOT MODIFY -- auto-generated by Apache Cordova\n' +
48
+ "platform :ios, '15.0'\n" +
49
+ `${DECLARATION}\n` +
50
+ "target 'App' do\n" +
51
+ "\tproject 'App.xcodeproj'\n" +
52
+ 'end\n'
53
+ );
54
+ }
55
+
56
+ module.exports = function (context) {
57
+ const opts = (context && context.opts) || {};
58
+ // HooksRunner.fire() normalizes `opts.cordova.platforms` for every hook type
59
+ // (see cordova-lib/src/hooks/HooksRunner.js). `opts.platforms` is only ever set
60
+ // by the `prepare` command itself (before_prepare/after_prepare) -- for
61
+ // before_plugin_install/after_plugin_install (fired during `cordova plugin add`,
62
+ // including plugin restoration onto a freshly added platform) the platform list
63
+ // only exists at `opts.cordova.platforms`. Check both so this hook actually runs
64
+ // on every path that can trigger `pod install`.
65
+ const platformsList = (opts.cordova && opts.cordova.platforms) || opts.platforms || [];
66
+ if (!platformsList.includes('ios')) return;
67
+
68
+ const projectRoot =
69
+ opts.projectRoot ||
70
+ (opts.cordova && opts.cordova.project && opts.cordova.project.root) ||
71
+ null;
72
+
73
+ if (!projectRoot) return;
74
+
75
+ const iosPlatformDir = path.join(projectRoot, 'platforms', 'ios');
76
+ if (!exists(iosPlatformDir)) return;
77
+
78
+ const podfilePath = path.join(iosPlatformDir, 'Podfile');
79
+
80
+ let result;
81
+ let action;
82
+
83
+ if (exists(podfilePath)) {
84
+ const original = fs.readFileSync(podfilePath, 'utf8');
85
+ if (alreadyPresent(original)) {
86
+ action = 'already-present';
87
+ result = original;
88
+ } else {
89
+ const patched = insertIntoExisting(original);
90
+ if (!patched) {
91
+ // eslint-disable-next-line no-console
92
+ console.log('cordova-plugin-reteno: iOS setup: Podfile format not recognized, skipping use_modular_headers! patch.');
93
+ return;
94
+ }
95
+ action = 'patched';
96
+ result = patched;
97
+ }
98
+ } else {
99
+ action = 'seeded';
100
+ result = seedTemplate();
101
+ }
102
+
103
+ if (action !== 'already-present') {
104
+ fs.writeFileSync(podfilePath, result, 'utf8');
105
+ }
106
+
107
+ // eslint-disable-next-line no-console
108
+ console.log(`cordova-plugin-reteno: iOS setup: Podfile use_modular_headers!=${action}.`);
109
+ };
@@ -79,17 +79,45 @@ module.exports = function (context) {
79
79
  (opts.options && opts.options.options && opts.options.options.cli_variables) ||
80
80
  {};
81
81
 
82
- const accessKey =
83
- variables.SDK_ACCESS_KEY ||
84
- readVarFromCmdLine('SDK_ACCESS_KEY') ||
85
- readVarFromPackageJson('SDK_ACCESS_KEY') ||
86
- process.env.SDK_ACCESS_KEY;
87
- const accessKeyStr = accessKey == null ? '' : String(accessKey).trim();
82
+ function firstValidValue(candidates) {
83
+ for (const candidate of candidates) {
84
+ const value = candidate == null ? '' : String(candidate).trim();
85
+ if (
86
+ value.length > 0 &&
87
+ value !== 'MISSING' &&
88
+ value !== '$RETENO_ACCESS_KEY' &&
89
+ value !== '$SDK_ACCESS_KEY'
90
+ ) {
91
+ return value;
92
+ }
93
+ }
94
+ return '';
95
+ }
88
96
 
89
- if (accessKeyStr.length === 0 || accessKeyStr === 'MISSING') {
97
+ const retenoAccessKey = firstValidValue([
98
+ variables.RETENO_ACCESS_KEY,
99
+ readVarFromCmdLine('RETENO_ACCESS_KEY'),
100
+ readVarFromPackageJson('RETENO_ACCESS_KEY'),
101
+ process.env.RETENO_ACCESS_KEY,
102
+ ]);
103
+ const legacyAccessKey = firstValidValue([
104
+ variables.SDK_ACCESS_KEY,
105
+ readVarFromCmdLine('SDK_ACCESS_KEY'),
106
+ readVarFromPackageJson('SDK_ACCESS_KEY'),
107
+ process.env.SDK_ACCESS_KEY,
108
+ ]);
109
+ const accessKey = retenoAccessKey || legacyAccessKey;
110
+
111
+ if (!accessKey) {
90
112
  throw new Error(
91
- 'cordova-plugin-reteno: SDK_ACCESS_KEY is required. Install with: ' +
92
- 'cordova plugin add cordova-plugin-reteno --variable SDK_ACCESS_KEY=YOUR_KEY'
113
+ 'cordova-plugin-reteno: RETENO_ACCESS_KEY is required. Install with: ' +
114
+ 'cordova plugin add cordova-plugin-reteno --variable RETENO_ACCESS_KEY=YOUR_KEY'
115
+ );
116
+ }
117
+
118
+ if (!retenoAccessKey && legacyAccessKey) {
119
+ console.warn(
120
+ 'cordova-plugin-reteno: SDK_ACCESS_KEY is deprecated; use RETENO_ACCESS_KEY instead.'
93
121
  );
94
122
  }
95
123
  };
@@ -1,6 +1,6 @@
1
1
  apply plugin: 'com.android.application'
2
2
 
3
- def retenoFcmVersion = '2.10.1'
3
+ def retenoFcmVersion = '2.10.2'
4
4
 
5
5
  android {
6
6
  namespace 'com.reteno.plugin'
@@ -68,7 +68,8 @@ import java.util.Map;
68
68
  public class RetenoPlugin extends CordovaPlugin {
69
69
  private static final int REQ_CODE_POST_NOTIFICATIONS = 10001;
70
70
  private static final String PERMISSION_POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";
71
- private static final String SDK_ACCESS_KEY_META = "com.reteno.SDK_ACCESS_KEY";
71
+ private static final String RETENO_ACCESS_KEY_META = "com.reteno.RETENO_ACCESS_KEY";
72
+ private static final String LEGACY_SDK_ACCESS_KEY_META = "com.reteno.SDK_ACCESS_KEY";
72
73
 
73
74
  private static volatile RetenoPlugin activeInstance;
74
75
 
@@ -561,9 +562,9 @@ public class RetenoPlugin extends CordovaPlugin {
561
562
  }
562
563
  if (TextUtils.isEmpty(accessKey)) {
563
564
  callbackContext.error(
564
- "Missing SDK_ACCESS_KEY. Provide it when installing the Cordova plugin " +
565
- "(e.g. --variable SDK_ACCESS_KEY=YOUR_KEY), pass it to RetenoPlugin.init({accessKey: ...}), " +
566
- "or set AndroidManifest meta-data 'com.reteno.SDK_ACCESS_KEY'."
565
+ "Missing RETENO_ACCESS_KEY. Provide it when installing the Cordova plugin " +
566
+ "(e.g. --variable RETENO_ACCESS_KEY=YOUR_KEY), pass it to RetenoPlugin.init({accessKey: ...}), " +
567
+ "or set AndroidManifest meta-data 'com.reteno.RETENO_ACCESS_KEY'."
567
568
  );
568
569
  return;
569
570
  }
@@ -779,7 +780,9 @@ public class RetenoPlugin extends CordovaPlugin {
779
780
  if (options == null) {
780
781
  return null;
781
782
  }
782
- String[] keys = new String[] {"accessKey", "access_key", "sdkAccessKey", "SDK_ACCESS_KEY"};
783
+ String[] keys = new String[] {
784
+ "accessKey", "access_key", "retenoAccessKey", "RETENO_ACCESS_KEY", "sdkAccessKey", "SDK_ACCESS_KEY"
785
+ };
783
786
  for (String keyName : keys) {
784
787
  String raw = options.optString(keyName, null);
785
788
  String normalized = normalizeAccessKey(raw);
@@ -1246,6 +1249,12 @@ public class RetenoPlugin extends CordovaPlugin {
1246
1249
  if (extras == null || extras.isEmpty()) {
1247
1250
  return;
1248
1251
  }
1252
+ // Reteno uses the interaction id as the canonical identifier for push interactions.
1253
+ // App/deep-link SDK extras (for example Branch) must not be exposed as an initial
1254
+ // Reteno notification.
1255
+ if (!extras.containsKey("es_interaction_id")) {
1256
+ return;
1257
+ }
1249
1258
 
1250
1259
  initialNotification = RetenoUtil.bundleToJson(extras);
1251
1260
  }
@@ -1262,12 +1271,18 @@ public class RetenoPlugin extends CordovaPlugin {
1262
1271
  if (info == null || info.metaData == null) {
1263
1272
  return readAccessKeyFromCordovaPreferences();
1264
1273
  }
1265
- String key = info.metaData.getString(SDK_ACCESS_KEY_META);
1274
+ String key = info.metaData.getString(RETENO_ACCESS_KEY_META);
1266
1275
  String normalized = normalizeAccessKey(key);
1267
1276
  if (normalized != null) {
1268
1277
  return normalized;
1269
1278
  }
1270
1279
 
1280
+ key = info.metaData.getString(LEGACY_SDK_ACCESS_KEY_META);
1281
+ normalized = normalizeAccessKey(key);
1282
+ if (normalized != null) {
1283
+ return normalized;
1284
+ }
1285
+
1271
1286
  return readAccessKeyFromCordovaPreferences();
1272
1287
  } catch (Exception ignored) {
1273
1288
  return readAccessKeyFromCordovaPreferences();
@@ -1276,8 +1291,13 @@ public class RetenoPlugin extends CordovaPlugin {
1276
1291
 
1277
1292
  private String readAccessKeyFromCordovaPreferences() {
1278
1293
  try {
1279
- String key = this.preferences != null ? this.preferences.getString("SDK_ACCESS_KEY", null) : null;
1280
- return normalizeAccessKey(key);
1294
+ if (this.preferences == null) {
1295
+ return null;
1296
+ }
1297
+ String key = normalizeAccessKey(this.preferences.getString("RETENO_ACCESS_KEY", null));
1298
+ return key != null
1299
+ ? key
1300
+ : normalizeAccessKey(this.preferences.getString("SDK_ACCESS_KEY", null));
1281
1301
  } catch (Exception ignored) {
1282
1302
  return null;
1283
1303
  }
@@ -1288,7 +1308,7 @@ public class RetenoPlugin extends CordovaPlugin {
1288
1308
  return null;
1289
1309
  }
1290
1310
  key = key.trim();
1291
- if (key.length() == 0 || "$SDK_ACCESS_KEY".equals(key) || "MISSING".equals(key)) {
1311
+ if (key.length() == 0 || "$RETENO_ACCESS_KEY".equals(key) || "$SDK_ACCESS_KEY".equals(key) || "MISSING".equals(key)) {
1292
1312
  return null;
1293
1313
  }
1294
1314
  return key;
@@ -5,7 +5,7 @@ allprojects {
5
5
  }
6
6
  }
7
7
 
8
- def retenoFcmVersion = '2.10.1'
8
+ def retenoFcmVersion = '2.10.2'
9
9
 
10
10
  def firebaseMessagingVersion = '23.1.0'
11
11
 
@@ -16,7 +16,8 @@ dependencies {
16
16
  }
17
17
 
18
18
  // ---------------------------------------------------------------------------
19
- // Capacitor: auto-substitute SDK_ACCESS_KEY in the merged plugin manifest.
19
+ // Capacitor: auto-substitute RETENO_ACCESS_KEY (or its legacy alias) in the
20
+ // merged plugin manifest.
20
21
  //
21
22
  // TODO: Migrate to a dedicated Gradle task with declared inputs/outputs when
22
23
  // Configuration Cache becomes the default in Capacitor. Running file I/O
@@ -24,8 +25,8 @@ dependencies {
24
25
  // executes on every Gradle invocation (including `./gradlew tasks`).
25
26
  //
26
27
  // Capacitor does NOT resolve $VARIABLE placeholders from plugin.xml; it uses
27
- // the <preference default="…"> value instead. For SDK_ACCESS_KEY the default
28
- // is "MISSING", which makes the SDK fail at runtime.
28
+ // the <preference default="…"> value instead. For access-key preferences the
29
+ // default is "MISSING", which makes the SDK fail at runtime.
29
30
  //
30
31
  // This block reads the real value from capacitor.config.json (generated by
31
32
  // Capacitor CLI from capacitor.config.ts) and patches the manifest source
@@ -49,7 +50,7 @@ try {
49
50
  new File(project.projectDir, 'src/main/AndroidManifest.xml'), // when applied from the plugin module itself
50
51
  ]
51
52
  for (def f : manifestCandidates) {
52
- if (f.exists() && f.text.contains('com.reteno.SDK_ACCESS_KEY')) {
53
+ if (f.exists() && (f.text.contains('com.reteno.RETENO_ACCESS_KEY') || f.text.contains('com.reteno.SDK_ACCESS_KEY'))) {
53
54
  capPluginManifest = f
54
55
  break
55
56
  }
@@ -71,16 +72,25 @@ try {
71
72
  def slurper = new groovy.json.JsonSlurper()
72
73
  def config = slurper.parse(capConfigJson)
73
74
  def prefs = config?.cordova?.preferences
74
- def accessKey = prefs?.SDK_ACCESS_KEY?.toString()?.trim()
75
+ def normalizeAccessKey = { value ->
76
+ def key = value?.toString()?.trim()
77
+ return key != null && key.length() > 0 && key != 'MISSING' && key != '$RETENO_ACCESS_KEY' && key != '$SDK_ACCESS_KEY'
78
+ ? key
79
+ : null
80
+ }
81
+ def retenoAccessKey = normalizeAccessKey(prefs?.RETENO_ACCESS_KEY)
82
+ def legacyAccessKey = normalizeAccessKey(prefs?.SDK_ACCESS_KEY)
83
+ def accessKey = retenoAccessKey ?: legacyAccessKey
84
+ def manifestKey = retenoAccessKey != null ? 'RETENO_ACCESS_KEY' : 'SDK_ACCESS_KEY'
75
85
 
76
- if (accessKey != null && accessKey.length() > 0 && accessKey != 'MISSING' && accessKey != '$SDK_ACCESS_KEY') {
86
+ if (accessKey != null) {
77
87
  def manifestText = capPluginManifest.text
78
- def pattern = ~/(<meta-data\s+android:name="com\.reteno\.SDK_ACCESS_KEY"\s+android:value=")([^"]*?)(")/
88
+ def pattern = ~/(<meta-data\s+android:name="com\.reteno\.${manifestKey}"\s+android:value=")([^"]*?)(")/
79
89
  def matcher = pattern.matcher(manifestText)
80
90
  if (matcher.find() && matcher.group(2) != accessKey) {
81
91
  def patched = matcher.replaceFirst('$1' + java.util.regex.Matcher.quoteReplacement(accessKey) + '$3')
82
92
  capPluginManifest.text = patched
83
- logger.lifecycle("cordova-plugin-reteno: Patched SDK_ACCESS_KEY in Capacitor plugin manifest.")
93
+ logger.lifecycle("cordova-plugin-reteno: Patched ${manifestKey} in Capacitor plugin manifest.")
84
94
  }
85
95
  }
86
96
  }
@@ -11,6 +11,7 @@ import FirebaseMessaging
11
11
  @objc(RetenoPlugin)
12
12
  class RetenoPlugin: CDVPlugin {
13
13
  private static weak var activeInstance: RetenoPlugin?
14
+ private static var hasStartedDelayedInitialization = false
14
15
  private var inboxCountCallbackId: String?
15
16
  private var isManualTokenMode = false
16
17
  #if canImport(FirebaseMessaging)
@@ -21,6 +22,10 @@ class RetenoPlugin: CDVPlugin {
21
22
 
22
23
  override func pluginInitialize() {
23
24
  RetenoPlugin.activeInstance = self
25
+ // The Objective-C bootstrap normally calls this from AppDelegate's
26
+ // didFinishLaunchingWithOptions. Keep this call as an idempotent fallback
27
+ // for unusual hosts that replace or bypass UIApplication's delegate setup.
28
+ RetenoPlugin.delayedStartIfNeeded()
24
29
  #if canImport(FirebaseMessaging)
25
30
  if let messaging = messagingIfAvailable() {
26
31
  messaging.delegate = self
@@ -28,6 +33,15 @@ class RetenoPlugin: CDVPlugin {
28
33
  #endif
29
34
  }
30
35
 
36
+ /// Called by RetenoPluginBootstrap from didFinishLaunchingWithOptions.
37
+ /// The Objective-C entry point lets Cordova and Capacitor start notification
38
+ /// response buffering before either framework lazily creates the plugin.
39
+ @objc static func delayedStartIfNeeded() {
40
+ guard !hasStartedDelayedInitialization else { return }
41
+ hasStartedDelayedInitialization = true
42
+ Reteno.delayedStart()
43
+ }
44
+
31
45
  override func onAppTerminate() {
32
46
  if RetenoPlugin.activeInstance === self {
33
47
  RetenoPlugin.activeInstance = nil
@@ -42,12 +56,14 @@ class RetenoPlugin: CDVPlugin {
42
56
  func initialize(_ command: CDVInvokedUrlCommand) {
43
57
  let options = (command.arguments.first as? [String: Any]) ?? [:]
44
58
 
45
- let providedKey = (options["accessKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
46
- let preferenceKey = getPreference("sdk_access_key")?.trimmingCharacters(in: .whitespacesAndNewlines)
47
- let accessKey = (providedKey?.isEmpty == false) ? providedKey : preferenceKey
59
+ let providedKey = normalizedAccessKey(options["accessKey"] as? String)
60
+ let retenoPreferenceKey = normalizedAccessKey(getPreference("reteno_access_key"))
61
+ let legacyPreferenceKey = normalizedAccessKey(getPreference("sdk_access_key"))
62
+ let preferenceKey = retenoPreferenceKey ?? legacyPreferenceKey
63
+ let accessKey = providedKey ?? preferenceKey
48
64
 
49
65
  guard let apiKey = accessKey, !apiKey.isEmpty else {
50
- sendError("Missing SDK access key. Provide options.accessKey or set preference SDK_ACCESS_KEY.", to: command)
66
+ sendError("Missing SDK access key. Provide options.accessKey or set preference RETENO_ACCESS_KEY.", to: command)
51
67
  return
52
68
  }
53
69
 
@@ -131,7 +147,12 @@ class RetenoPlugin: CDVPlugin {
131
147
  self.isManualTokenMode = (deviceTokenMode == .manual)
132
148
 
133
149
  DispatchQueue.main.async {
134
- Reteno.start(apiKey: apiKey, deviceTokenHandlingMode: deviceTokenMode, configuration: configuration)
150
+ // delayedSetup (paired with delayedStartIfNeeded() during native bootstrap)
151
+ // completes initialization and replays any push response buffered since
152
+ // launch -- e.g. a push-linked in-app opened while the SDK was still
153
+ // cold-starting. Requires delayedStart() to have run first, or this is a
154
+ // silent no-op (see Reteno.delayedSetup's IsDelayedInitialization guard).
155
+ Reteno.delayedSetup(apiKey: apiKey, deviceTokenHandlingMode: deviceTokenMode, configuration: configuration)
135
156
 
136
157
  // When IOS_DEVICE_TOKEN_HANDLING_MODE is manual and FirebaseMessaging is available,
137
158
  // configure Firebase if the developer hasn't done it yet, then hook FCM
@@ -1331,6 +1352,17 @@ class RetenoPlugin: CDVPlugin {
1331
1352
  return nil
1332
1353
  }
1333
1354
 
1355
+ private func normalizedAccessKey(_ key: String?) -> String? {
1356
+ guard let value = key?.trimmingCharacters(in: .whitespacesAndNewlines),
1357
+ !value.isEmpty,
1358
+ value != "MISSING",
1359
+ value != "$RETENO_ACCESS_KEY",
1360
+ value != "$SDK_ACCESS_KEY" else {
1361
+ return nil
1362
+ }
1363
+ return value
1364
+ }
1365
+
1334
1366
  private func sendError(_ message: String, to command: CDVInvokedUrlCommand) {
1335
1367
  let result = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: message)
1336
1368
  commandDelegate.send(result, callbackId: command.callbackId)