@amplytools/react-native-amply-sdk 0.13.0 → 0.15.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 (45) hide show
  1. package/AmplyReactNative.podspec +1 -1
  2. package/README.md +47 -0
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +30 -0
  5. package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +13 -0
  6. package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +46 -0
  7. package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt +2 -0
  8. package/android/src/main/java/tools/amply/sdk/reactnative/model/CampaignTouchPayload.kt +12 -0
  9. package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +12 -0
  10. package/android/src/newarch/jni/AmplyReactNative-generated.cpp +18 -0
  11. package/dist/src/__tests__/appLanguage.test.d.ts +2 -0
  12. package/dist/src/__tests__/appLanguage.test.d.ts.map +1 -0
  13. package/dist/src/__tests__/appLanguage.test.js +211 -0
  14. package/dist/src/__tests__/handleUrl.test.d.ts +2 -0
  15. package/dist/src/__tests__/handleUrl.test.d.ts.map +1 -0
  16. package/dist/src/__tests__/handleUrl.test.js +114 -0
  17. package/dist/src/__tests__/lastCampaignTouch.test.d.ts +2 -0
  18. package/dist/src/__tests__/lastCampaignTouch.test.d.ts.map +1 -0
  19. package/dist/src/__tests__/lastCampaignTouch.test.js +119 -0
  20. package/dist/src/__tests__/systemEventUtils.test.js +16 -0
  21. package/dist/src/campaignTouch.d.ts +36 -0
  22. package/dist/src/campaignTouch.d.ts.map +1 -0
  23. package/dist/src/campaignTouch.js +39 -0
  24. package/dist/src/index.d.ts +50 -0
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +198 -1
  27. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +40 -0
  28. package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
  29. package/dist/src/systemEventUtils.d.ts.map +1 -1
  30. package/dist/src/systemEventUtils.js +3 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/docs/ARCHITECTURE.md +4 -0
  33. package/docs/DEVELOPMENT.md +2 -1
  34. package/ios/Sources/AmplyReactNative/AmplyModule.mm +81 -0
  35. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +21 -0
  36. package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +11 -0
  37. package/package.json +1 -1
  38. package/src/__tests__/appLanguage.test.ts +298 -0
  39. package/src/__tests__/handleUrl.test.ts +155 -0
  40. package/src/__tests__/lastCampaignTouch.test.ts +151 -0
  41. package/src/__tests__/systemEventUtils.test.ts +22 -0
  42. package/src/campaignTouch.ts +74 -0
  43. package/src/index.ts +223 -1
  44. package/src/nativeSpecs/NativeAmplyModule.ts +41 -0
  45. package/src/systemEventUtils.ts +3 -1
@@ -22,7 +22,7 @@ Pod::Spec.new do |s|
22
22
  s.dependency 'React-RCTFabric'
23
23
 
24
24
  # Amply KMP SDK from CocoaPods
25
- s.dependency 'AmplySDK', '~> 0.13.0'
25
+ s.dependency 'AmplySDK', '~> 0.15.0'
26
26
 
27
27
  s.libraries = 'c++'
28
28
  s.pod_target_xcconfig = {
package/README.md CHANGED
@@ -13,6 +13,7 @@ React Native TurboModule bridge for the Amply SDK. Enables event tracking, deepl
13
13
  | Debug/Logging | ✅ | Android, iOS |
14
14
  | Custom properties | ✅ | Android, iOS |
15
15
  | User identification | ✅ | Android, iOS |
16
+ | Last campaign touch (attribution in your analytics) | ✅ | Android, iOS |
16
17
 
17
18
  ## Requirements
18
19
 
@@ -60,6 +61,12 @@ await Amply.track({
60
61
  // Identify users
61
62
  Amply.setUserId('user-123');
62
63
 
64
+ // If your app picks its UI language itself (an in-app picker, i18next, …), tell Amply,
65
+ // so campaigns show in that language. Any common format: 'pt_BR', 'pt-BR', 'zh-TW', 'fr'.
66
+ // Persisted across launches; pass null to go back to following the device.
67
+ // Can also be given up front: Amply.initialize({ ..., appLanguage: 'pt_BR' }).
68
+ Amply.setAppLanguage('pt_BR'); // e.g. i18next's i18n.language
69
+
63
70
  // Set custom properties for targeting
64
71
  Amply.setCustomProperties({ plan: 'premium', onboarded: true });
65
72
 
@@ -69,6 +76,46 @@ const unsubscribe = await Amply.addDeepLinkListener((event) => {
69
76
  });
70
77
  ```
71
78
 
79
+ ### Debug launch
80
+
81
+ Amply can open a campaign on a real device from a link or QR code made in the dashboard, so you
82
+ can click through it inside your app. The link uses your app's URL scheme, so the app has to pass
83
+ incoming links to Amply:
84
+
85
+ ```typescript
86
+ import {Linking} from 'react-native';
87
+ import Amply from '@amplytools/react-native-amply-sdk';
88
+
89
+ const forward = (url: string | null) => {
90
+ if (url && Amply.handleUrl(url)) {
91
+ return; // an Amply link: Amply took it, do not route it
92
+ }
93
+ // ...your own link handling
94
+ };
95
+
96
+ Linking.getInitialURL().then(forward);
97
+ Linking.addEventListener('url', ({url}) => forward(url));
98
+ ```
99
+
100
+ `handleUrl` returns `true` when Amply recognised the link as its own, and `false` for any other
101
+ link (it never throws). It is fine to call it before `initialize` finishes, as on a cold start: an Amply link that arrives early is held (the newest one) and opened as soon as `initialize` succeeds; it is dropped if `initialize` fails. A
102
+ campaign launched this way is not counted in statistics.
103
+
104
+ With Expo Router, links are routed before your code sees them, so forward only from
105
+ `app/+native-intent.tsx` (and do not also forward with `Linking` above). An Amply link reaches the
106
+ SDK instead of the not-found screen, and every other link is routed as usual:
107
+
108
+ ```typescript
109
+ import Amply from '@amplytools/react-native-amply-sdk';
110
+
111
+ export function redirectSystemPath({path}: {path: string; initial: boolean}) {
112
+ if (Amply.handleUrl(path)) {
113
+ return '/'; // an Amply link: Amply took it
114
+ }
115
+ return path;
116
+ }
117
+ ```
118
+
72
119
  ### About the keys
73
120
 
74
121
  `apiKeySecret` ships inside your app: the SDK signs every request with it on the device. Keep it
@@ -15,7 +15,7 @@ buildscript {
15
15
  apply plugin: 'com.android.library'
16
16
  apply plugin: 'org.jetbrains.kotlin.android'
17
17
 
18
- def amplySdkVersion = project.findProperty('amplySdkVersion') ?: System.getenv('AMPLY_SDK_VERSION') ?: '0.13.0'
18
+ def amplySdkVersion = project.findProperty('amplySdkVersion') ?: System.getenv('AMPLY_SDK_VERSION') ?: '0.15.0'
19
19
  group = 'tools.amply'
20
20
  version = amplySdkVersion
21
21
 
@@ -5,6 +5,7 @@ import tools.amply.sdk.reactnative.core.AmplyClient
5
5
  import tools.amply.sdk.reactnative.core.DefaultAmplyClient
6
6
  import tools.amply.sdk.reactnative.core.GateDecision
7
7
  import tools.amply.sdk.reactnative.model.AmplyInitializationOptions
8
+ import tools.amply.sdk.reactnative.model.CampaignTouchPayload
8
9
  import tools.amply.sdk.reactnative.model.DataSetType
9
10
  import tools.amply.sdk.reactnative.model.DataSetType.EventParam
10
11
  import tools.amply.sdk.reactnative.model.DataSetType.Events
@@ -153,6 +154,16 @@ class AmplyModule(reactContext: ReactApplicationContext) :
153
154
  }
154
155
  }
155
156
 
157
+ override fun getLastCampaignTouch(withinMs: Double, promise: Promise) {
158
+ scope.launch {
159
+ try {
160
+ promise.resolve(client.getLastCampaignTouch(withinMs.toLong())?.toWritableMap())
161
+ } catch (throwable: Throwable) {
162
+ promise.reject(CAMPAIGN_TOUCH_ERROR, throwable)
163
+ }
164
+ }
165
+ }
166
+
156
167
  override fun getDataSetSnapshot(type: ReadableMap, promise: Promise) {
157
168
  val datasetType = try {
158
169
  type.toDataSetType()
@@ -195,6 +206,12 @@ class AmplyModule(reactContext: ReactApplicationContext) :
195
206
  client.setUserId(userId)
196
207
  }
197
208
 
209
+ override fun setAppLanguage(tag: String?) {
210
+ client.setAppLanguage(tag)
211
+ }
212
+
213
+ override fun handleUrl(url: String): Boolean = client.handleUrl(url)
214
+
198
215
  override fun setLogLevel(level: String) {
199
216
  val newLevel = LogLevel.fromString(level)
200
217
  client.setLogLevel(newLevel)
@@ -354,6 +371,9 @@ class AmplyModule(reactContext: ReactApplicationContext) :
354
371
  val defaultConfig = if (hasKey("defaultConfig")) getString("defaultConfig") else null
355
372
  val debug = if (hasKey("debug")) getBoolean("debug") else null
356
373
  val logLevel = if (hasKey("logLevel")) LogLevel.fromString(getString("logLevel")) else null
374
+ // Blank names no language: left out rather than handed to the SDK to be warned about.
375
+ val appLanguage = (if (hasKey("appLanguage")) getString("appLanguage") else null)
376
+ ?.takeIf { it.isNotBlank() }
357
377
 
358
378
  return AmplyInitializationOptions(
359
379
  appId = appId,
@@ -365,6 +385,7 @@ class AmplyModule(reactContext: ReactApplicationContext) :
365
385
  defaultConfig = defaultConfig,
366
386
  debug = debug,
367
387
  logLevel = logLevel,
388
+ appLanguage = appLanguage,
368
389
  )
369
390
  }
370
391
 
@@ -419,6 +440,14 @@ class AmplyModule(reactContext: ReactApplicationContext) :
419
440
  putMap("properties", properties.toWritableMap())
420
441
  }
421
442
 
443
+ private fun CampaignTouchPayload.toWritableMap(): WritableMap =
444
+ Arguments.createMap().apply {
445
+ putString("campaignId", campaignId)
446
+ putString("campaignName", campaignName)
447
+ putString("kind", kind)
448
+ putDouble("atMs", atMs.toDouble())
449
+ }
450
+
422
451
  private fun List<EventEnvelope>.toWritableArray(): WritableArray {
423
452
  val array = Arguments.createArray()
424
453
  forEach { event ->
@@ -522,6 +551,7 @@ class AmplyModule(reactContext: ReactApplicationContext) :
522
551
  private const val ARGUMENT_ERROR = "AMP_INVALID_ARGUMENT"
523
552
  private const val TRACK_ERROR = "AMP_TRACK_FAILED"
524
553
  private const val EVENTS_ERROR = "AMP_EVENTS_FAILED"
554
+ private const val CAMPAIGN_TOUCH_ERROR = "AMP_CAMPAIGN_TOUCH_FAILED"
525
555
  private const val DATASET_ERROR = "AMP_DATASET_FAILED"
526
556
  private const val CUSTOM_PROP_ERROR = "AMP_CUSTOM_PROP_FAILED"
527
557
  }
@@ -2,6 +2,7 @@ package tools.amply.sdk.reactnative.core
2
2
 
3
3
  import android.app.Activity
4
4
  import tools.amply.sdk.reactnative.model.AmplyInitializationOptions
5
+ import tools.amply.sdk.reactnative.model.CampaignTouchPayload
5
6
  import tools.amply.sdk.reactnative.model.DataSetType
6
7
  import tools.amply.sdk.reactnative.model.DeepLinkPayload
7
8
  import tools.amply.sdk.reactnative.model.EventEnvelope
@@ -65,6 +66,13 @@ interface AmplyClient {
65
66
 
66
67
  suspend fun getRecentEvents(limit: Int): List<EventEnvelope>
67
68
 
69
+ /**
70
+ * The SDK's `getLastCampaignTouch(withinMs)`: the newest campaign touch at most [withinMs] ago
71
+ * (clamped by the SDK to [0, 30 days]). Null when there is none, and also before
72
+ * initialisation — the SDK answers null there rather than throwing, and so does the bridge.
73
+ */
74
+ suspend fun getLastCampaignTouch(withinMs: Long): CampaignTouchPayload?
75
+
68
76
  suspend fun getDataSetSnapshot(type: DataSetType): Map<String, Any?>
69
77
 
70
78
  fun registerDeepLinkListener()
@@ -72,6 +80,11 @@ interface AmplyClient {
72
80
 
73
81
  fun setUserId(userId: String?)
74
82
 
83
+ fun setAppLanguage(tag: String?)
84
+
85
+ /** True when the SDK recognised [url] as its own and took it over; false otherwise or when not initialised. */
86
+ fun handleUrl(url: String): Boolean
87
+
75
88
  fun setLogLevel(level: LogLevel)
76
89
  fun getLogLevel(): LogLevel
77
90
 
@@ -5,6 +5,7 @@ import android.app.Application
5
5
  import android.os.Handler
6
6
  import android.os.Looper
7
7
  import tools.amply.sdk.reactnative.model.AmplyInitializationOptions
8
+ import tools.amply.sdk.reactnative.model.CampaignTouchPayload
8
9
  import tools.amply.sdk.reactnative.model.DataSetType
9
10
  import tools.amply.sdk.reactnative.model.DeepLinkPayload
10
11
  import tools.amply.sdk.reactnative.model.EventEnvelope
@@ -30,6 +31,8 @@ import tools.amply.sdk.actions.CampaignPresenter
30
31
  import tools.amply.sdk.actions.CampaignResolution
31
32
  import tools.amply.sdk.actions.CampaignResult
32
33
  import tools.amply.sdk.actions.DeepLinkListener
34
+ import tools.amply.sdk.campaigns.CampaignTouch
35
+ import tools.amply.sdk.campaigns.CampaignTouchKind
33
36
  import tools.amply.sdk.core.GateDecision as KmpGateDecision
34
37
  import tools.amply.sdk.core.ProceedReason as KmpProceedReason
35
38
  import tools.amply.sdk.config.AmplyConfig
@@ -291,6 +294,26 @@ class DefaultAmplyClient(
291
294
  }
292
295
  }
293
296
 
297
+ override suspend fun getLastCampaignTouch(withinMs: Long): CampaignTouchPayload? {
298
+ // Not requireInstance(): "not initialised" is an answer here (null), as it is in the SDK.
299
+ val instance = amplyInstance ?: return null
300
+ return withContext(Dispatchers.IO) {
301
+ instance.getLastCampaignTouch(withinMs)?.toPayload()
302
+ }
303
+ }
304
+
305
+ private fun CampaignTouch.toPayload(): CampaignTouchPayload = CampaignTouchPayload(
306
+ campaignId = campaignId,
307
+ campaignName = campaignName,
308
+ // Exhaustive on purpose: a kind added to the SDK must fail this build, not reach JS as a
309
+ // value the TS type does not admit.
310
+ kind = when (kind) {
311
+ CampaignTouchKind.SHOWN -> "shown"
312
+ CampaignTouchKind.ENGAGED -> "engaged"
313
+ },
314
+ atMs = atMs,
315
+ )
316
+
294
317
  override suspend fun getDataSetSnapshot(type: DataSetType): Map<String, Any?> {
295
318
  val instance = requireInstance()
296
319
  val nativeType = type.toNativeDataSetType()
@@ -385,6 +408,27 @@ class DefaultAmplyClient(
385
408
  }
386
409
  }
387
410
 
411
+ override fun setAppLanguage(tag: String?) {
412
+ // Buffered like setUserId. The JS layer already holds a pre-init call and folds it into the
413
+ // config, so this only catches a call that reaches native before the instance exists anyway.
414
+ synchronized(propertyLock) {
415
+ val instance = amplyInstance
416
+ if (instance != null) {
417
+ instance.setAppLanguage(tag)
418
+ android.util.Log.i("AmplyReactNative", "App language set to: ${tag ?: "<null>"}")
419
+ } else {
420
+ android.util.Log.i("AmplyReactNative", "Buffering setAppLanguage until init")
421
+ pendingPropertyOps.add { it.setAppLanguage(tag) }
422
+ }
423
+ }
424
+ }
425
+
426
+ override fun handleUrl(url: String): Boolean {
427
+ // Not buffered: a link is only meaningful to a live SDK, and the answer is needed right now.
428
+ val instance = amplyInstance ?: return false
429
+ return instance.handleUrl(url)
430
+ }
431
+
388
432
  override fun setCustomProperty(key: String, value: Any) {
389
433
  synchronized(propertyLock) {
390
434
  val instance = amplyInstance
@@ -534,6 +578,8 @@ class DefaultAmplyClient(
534
578
  }
535
579
  }
536
580
  options.defaultConfig?.let { defaultConfig = it }
581
+ // Same as setAppLanguage at init; the SDK applies it while building the instance.
582
+ options.appLanguage?.let { appLanguage = it }
537
583
  }
538
584
  }
539
585
 
@@ -35,6 +35,8 @@ data class AmplyInitializationOptions(
35
35
  val defaultConfig: String?,
36
36
  val debug: Boolean?,
37
37
  val logLevel: LogLevel?,
38
+ /** The app's UI language, any common tag format. Same as setAppLanguage at init. */
39
+ val appLanguage: String? = null,
38
40
  ) {
39
41
  /**
40
42
  * Get the effective log level, resolving debug vs logLevel precedence.
@@ -0,0 +1,12 @@
1
+ package tools.amply.sdk.reactnative.model
2
+
3
+ /**
4
+ * The bridge's copy of the SDK's `CampaignTouch`, with [kind] already in its JS spelling
5
+ * (`"shown"` / `"engaged"`).
6
+ */
7
+ data class CampaignTouchPayload(
8
+ val campaignId: String,
9
+ val campaignName: String,
10
+ val kind: String,
11
+ val atMs: Long,
12
+ )
@@ -78,6 +78,10 @@ public abstract class NativeAmplyModuleSpec extends ReactContextBaseJavaModule i
78
78
  @DoNotStrip
79
79
  public abstract void getRecentEvents(double limit, Promise promise);
80
80
 
81
+ @ReactMethod
82
+ @DoNotStrip
83
+ public abstract void getLastCampaignTouch(double withinMs, Promise promise);
84
+
81
85
  @ReactMethod
82
86
  @DoNotStrip
83
87
  public abstract void getDataSetSnapshot(ReadableMap type, Promise promise);
@@ -90,6 +94,14 @@ public abstract class NativeAmplyModuleSpec extends ReactContextBaseJavaModule i
90
94
  @DoNotStrip
91
95
  public abstract void setUserId(@Nullable String userId);
92
96
 
97
+ @ReactMethod
98
+ @DoNotStrip
99
+ public abstract void setAppLanguage(@Nullable String tag);
100
+
101
+ @ReactMethod(isBlockingSynchronousMethod = true)
102
+ @DoNotStrip
103
+ public abstract boolean handleUrl(String url);
104
+
93
105
  @ReactMethod
94
106
  @DoNotStrip
95
107
  public abstract void setLogLevel(String level);
@@ -52,6 +52,11 @@ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getRecentEve
52
52
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "getRecentEvents", "(DLcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
53
53
  }
54
54
 
55
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getLastCampaignTouch(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
56
+ static jmethodID cachedMethodId = nullptr;
57
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "getLastCampaignTouch", "(DLcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
58
+ }
59
+
55
60
  static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_getDataSetSnapshot(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
56
61
  static jmethodID cachedMethodId = nullptr;
57
62
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "getDataSetSnapshot", "(Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
@@ -67,6 +72,16 @@ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_setUserId(fa
67
72
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "setUserId", "(Ljava/lang/String;)V", args, count, cachedMethodId);
68
73
  }
69
74
 
75
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_setAppLanguage(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
76
+ static jmethodID cachedMethodId = nullptr;
77
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "setAppLanguage", "(Ljava/lang/String;)V", args, count, cachedMethodId);
78
+ }
79
+
80
+ static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_handleUrl(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
81
+ static jmethodID cachedMethodId = nullptr;
82
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, BooleanKind, "handleUrl", "(Ljava/lang/String;)Z", args, count, cachedMethodId);
83
+ }
84
+
70
85
  static facebook::jsi::Value __hostFunction_NativeAmplyModuleSpecJSI_setLogLevel(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
71
86
  static jmethodID cachedMethodId = nullptr;
72
87
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "setLogLevel", "(Ljava/lang/String;)V", args, count, cachedMethodId);
@@ -117,9 +132,12 @@ NativeAmplyModuleSpecJSI::NativeAmplyModuleSpecJSI(const JavaTurboModule::InitPa
117
132
  methodMap_["registerGate"] = MethodMetadata {3, __hostFunction_NativeAmplyModuleSpecJSI_registerGate};
118
133
  methodMap_["unregisterGate"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_unregisterGate};
119
134
  methodMap_["getRecentEvents"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getRecentEvents};
135
+ methodMap_["getLastCampaignTouch"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getLastCampaignTouch};
120
136
  methodMap_["getDataSetSnapshot"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_getDataSetSnapshot};
121
137
  methodMap_["registerDeepLinkListener"] = MethodMetadata {0, __hostFunction_NativeAmplyModuleSpecJSI_registerDeepLinkListener};
122
138
  methodMap_["setUserId"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setUserId};
139
+ methodMap_["setAppLanguage"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setAppLanguage};
140
+ methodMap_["handleUrl"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_handleUrl};
123
141
  methodMap_["setLogLevel"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setLogLevel};
124
142
  methodMap_["getLogLevel"] = MethodMetadata {0, __hostFunction_NativeAmplyModuleSpecJSI_getLogLevel};
125
143
  methodMap_["setCustomProperties"] = MethodMetadata {1, __hostFunction_NativeAmplyModuleSpecJSI_setCustomProperties};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=appLanguage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appLanguage.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/appLanguage.test.ts"],"names":[],"mappings":"AAySA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,211 @@
1
+ function createNativeModule() {
2
+ let initialized = false;
3
+ return {
4
+ initialize: jest.fn(async () => {
5
+ initialized = true;
6
+ }),
7
+ isInitialized: jest.fn(() => initialized),
8
+ setAppLanguage: jest.fn(),
9
+ setUserId: jest.fn(),
10
+ addListener: jest.fn(),
11
+ removeListeners: jest.fn(),
12
+ onSystemEvent: jest.fn(() => ({ remove: jest.fn() })),
13
+ onDeepLink: jest.fn(() => ({ remove: jest.fn() })),
14
+ onCampaignPresent: jest.fn(() => ({ remove: jest.fn() })),
15
+ /** Test-only: pretend the SDK is (or is not) up without going through initialize. */
16
+ setInitialized(value) {
17
+ initialized = value;
18
+ },
19
+ };
20
+ }
21
+ const KEYS = { appId: 'id', apiKeyPublic: 'public', apiKeySecret: 'secret' };
22
+ function load() {
23
+ let Amply;
24
+ const native = createNativeModule();
25
+ jest.isolateModules(() => {
26
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
27
+ require('../nativeModule').__setNativeModule(native);
28
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
29
+ Amply = require('../index');
30
+ });
31
+ return { Amply, native };
32
+ }
33
+ describe('setAppLanguage after initialization', () => {
34
+ it.each(['pt_BR', 'zh-TW', 'FR', 'Français'])('forwards %p to the native SDK verbatim — parsing and the warning for junk are the SDK’s job', tag => {
35
+ const { Amply, native } = load();
36
+ native.setInitialized(true);
37
+ Amply.setAppLanguage(tag);
38
+ expect(native.setAppLanguage).toHaveBeenCalledTimes(1);
39
+ expect(native.setAppLanguage).toHaveBeenCalledWith(tag);
40
+ });
41
+ it('forwards null, which returns the SDK to automatic detection', () => {
42
+ const { Amply, native } = load();
43
+ native.setInitialized(true);
44
+ Amply.setAppLanguage(null);
45
+ expect(native.setAppLanguage).toHaveBeenCalledWith(null);
46
+ });
47
+ it('is on the default export as well', () => {
48
+ const { Amply, native } = load();
49
+ native.setInitialized(true);
50
+ Amply.default.setAppLanguage('de');
51
+ expect(native.setAppLanguage).toHaveBeenCalledWith('de');
52
+ });
53
+ });
54
+ describe('appLanguage in the initialization config', () => {
55
+ it('is passed through to the native initialize', async () => {
56
+ const { Amply, native } = load();
57
+ await Amply.initialize({ ...KEYS, appLanguage: 'pt_BR' });
58
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'pt_BR' }));
59
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
60
+ });
61
+ it('is left out of the native config when the caller did not give one', async () => {
62
+ const { Amply, native } = load();
63
+ await Amply.initialize(KEYS);
64
+ const [config] = native.initialize.mock.calls[0];
65
+ expect(config).not.toHaveProperty('appLanguage');
66
+ });
67
+ });
68
+ describe('setAppLanguage before initialization', () => {
69
+ it('is not sent to a native SDK that is not up yet', () => {
70
+ const { Amply, native } = load();
71
+ Amply.setAppLanguage('fr');
72
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
73
+ });
74
+ it('reaches the SDK as the config appLanguage when initialize names none', async () => {
75
+ const { Amply, native } = load();
76
+ Amply.setAppLanguage('fr');
77
+ await Amply.initialize(KEYS);
78
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'fr' }));
79
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
80
+ });
81
+ it('keeps only the last of several calls', async () => {
82
+ const { Amply, native } = load();
83
+ Amply.setAppLanguage('fr');
84
+ Amply.setAppLanguage('de');
85
+ await Amply.initialize(KEYS);
86
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'de' }));
87
+ });
88
+ it('loses to an appLanguage given to the later initialize call', async () => {
89
+ const { Amply, native } = load();
90
+ Amply.setAppLanguage('fr');
91
+ await Amply.initialize({ ...KEYS, appLanguage: 'de' });
92
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'de' }));
93
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
94
+ });
95
+ it('wins over a blank appLanguage in the config, which names no language', async () => {
96
+ const { Amply, native } = load();
97
+ Amply.setAppLanguage('fr');
98
+ await Amply.initialize({ ...KEYS, appLanguage: ' ' });
99
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'fr' }));
100
+ });
101
+ // null cannot travel in the config — there it means "leave the setting alone" — so a parked
102
+ // clear is applied as a call once the SDK is up.
103
+ it('applies a parked null right after initialization', async () => {
104
+ const { Amply, native } = load();
105
+ Amply.setAppLanguage(null);
106
+ await Amply.initialize(KEYS);
107
+ const [config] = native.initialize.mock.calls[0];
108
+ expect(config).not.toHaveProperty('appLanguage');
109
+ expect(native.setAppLanguage).toHaveBeenCalledTimes(1);
110
+ expect(native.setAppLanguage).toHaveBeenCalledWith(null);
111
+ });
112
+ it('drops a parked null when the later initialize names a language', async () => {
113
+ const { Amply, native } = load();
114
+ Amply.setAppLanguage(null);
115
+ await Amply.initialize({ ...KEYS, appLanguage: 'de' });
116
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'de' }));
117
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
118
+ });
119
+ it('survives an initialize that fails, and is used by the retry', async () => {
120
+ const { Amply, native } = load();
121
+ Amply.setAppLanguage('fr');
122
+ native.initialize.mockImplementationOnce(async () => {
123
+ throw new Error('AMP_INIT_FAILED');
124
+ });
125
+ await expect(Amply.initialize(KEYS)).rejects.toThrow('AMP_INIT_FAILED');
126
+ await Amply.initialize(KEYS);
127
+ expect(native.initialize).toHaveBeenLastCalledWith(expect.objectContaining({ appLanguage: 'fr' }));
128
+ });
129
+ it('survives an initialize rejected for a missing key', async () => {
130
+ const { Amply, native } = load();
131
+ const error = jest.spyOn(console, 'error').mockImplementation(() => { });
132
+ Amply.setAppLanguage('fr');
133
+ await expect(Amply.initialize({ ...KEYS, apiKeySecret: '' })).rejects.toMatchObject({
134
+ code: 'AMP_INVALID_CONFIG',
135
+ });
136
+ await Amply.initialize(KEYS);
137
+ expect(native.initialize).toHaveBeenCalledTimes(1);
138
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'fr' }));
139
+ error.mockRestore();
140
+ });
141
+ });
142
+ describe('setAppLanguage while initialize is in flight', () => {
143
+ it('is applied after initialization, so it wins over the config value it came after', async () => {
144
+ const { Amply, native } = load();
145
+ let finishInit;
146
+ native.initialize.mockImplementationOnce(() => new Promise(resolve => {
147
+ finishInit = () => {
148
+ native.setInitialized(true);
149
+ resolve();
150
+ };
151
+ }));
152
+ const init = Amply.initialize({ ...KEYS, appLanguage: 'de' });
153
+ Amply.setAppLanguage('fr');
154
+ expect(native.setAppLanguage).not.toHaveBeenCalled();
155
+ finishInit();
156
+ await init;
157
+ expect(native.initialize).toHaveBeenCalledWith(expect.objectContaining({ appLanguage: 'de' }));
158
+ expect(native.setAppLanguage).toHaveBeenCalledTimes(1);
159
+ expect(native.setAppLanguage).toHaveBeenCalledWith('fr');
160
+ });
161
+ // The native SDK reports itself up before the initialize promise settles, so a call made in
162
+ // that window goes straight through. A value held from before initialize is older than it and
163
+ // must not be replayed over it when the promise finally resolves.
164
+ function hangInitialize(native) {
165
+ const control = { comeUp: () => { }, resolve: () => { }, reject: (_error) => { } };
166
+ native.initialize.mockImplementationOnce(() => new Promise((resolve, reject) => {
167
+ control.comeUp = () => native.setInitialized(true);
168
+ control.resolve = resolve;
169
+ control.reject = reject;
170
+ }));
171
+ return control;
172
+ }
173
+ it('does not replay an older held null over a call the SDK already received', async () => {
174
+ const { Amply, native } = load();
175
+ Amply.setAppLanguage(null);
176
+ const init = hangInitialize(native);
177
+ const pending = Amply.initialize(KEYS);
178
+ init.comeUp();
179
+ Amply.setAppLanguage('fr');
180
+ init.resolve();
181
+ await pending;
182
+ expect(native.setAppLanguage.mock.calls).toEqual([['fr']]);
183
+ });
184
+ it('does not replay an older held tag over a call the SDK already received', async () => {
185
+ const { Amply, native } = load();
186
+ const init = hangInitialize(native);
187
+ const pending = Amply.initialize(KEYS);
188
+ Amply.setAppLanguage('de');
189
+ init.comeUp();
190
+ Amply.setAppLanguage('fr');
191
+ init.resolve();
192
+ await pending;
193
+ expect(native.setAppLanguage.mock.calls).toEqual([['fr']]);
194
+ });
195
+ it('does not hand a superseded held tag to the retry of a failed initialize', async () => {
196
+ const { Amply, native } = load();
197
+ Amply.setAppLanguage('de');
198
+ const init = hangInitialize(native);
199
+ const pending = Amply.initialize(KEYS);
200
+ init.comeUp();
201
+ Amply.setAppLanguage('fr');
202
+ init.reject(new Error('AMP_INIT_FAILED'));
203
+ await expect(pending).rejects.toThrow('AMP_INIT_FAILED');
204
+ native.setInitialized(false);
205
+ await Amply.initialize(KEYS);
206
+ const [retryConfig] = native.initialize.mock.calls[1];
207
+ expect(retryConfig).not.toHaveProperty('appLanguage');
208
+ expect(native.setAppLanguage.mock.calls).toEqual([['fr']]);
209
+ });
210
+ });
211
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=handleUrl.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handleUrl.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/handleUrl.test.ts"],"names":[],"mappings":"AA0JA,OAAO,EAAE,CAAC"}