@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.
- package/AmplyReactNative.podspec +1 -1
- package/README.md +47 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/tools/amply/sdk/reactnative/AmplyModule.kt +30 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/AmplyClient.kt +13 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/core/DefaultAmplyClient.kt +46 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt +2 -0
- package/android/src/main/java/tools/amply/sdk/reactnative/model/CampaignTouchPayload.kt +12 -0
- package/android/src/newarch/java/tools/amply/sdk/reactnative/NativeAmplyModuleSpec.java +12 -0
- package/android/src/newarch/jni/AmplyReactNative-generated.cpp +18 -0
- package/dist/src/__tests__/appLanguage.test.d.ts +2 -0
- package/dist/src/__tests__/appLanguage.test.d.ts.map +1 -0
- package/dist/src/__tests__/appLanguage.test.js +211 -0
- package/dist/src/__tests__/handleUrl.test.d.ts +2 -0
- package/dist/src/__tests__/handleUrl.test.d.ts.map +1 -0
- package/dist/src/__tests__/handleUrl.test.js +114 -0
- package/dist/src/__tests__/lastCampaignTouch.test.d.ts +2 -0
- package/dist/src/__tests__/lastCampaignTouch.test.d.ts.map +1 -0
- package/dist/src/__tests__/lastCampaignTouch.test.js +119 -0
- package/dist/src/__tests__/systemEventUtils.test.js +16 -0
- package/dist/src/campaignTouch.d.ts +36 -0
- package/dist/src/campaignTouch.d.ts.map +1 -0
- package/dist/src/campaignTouch.js +39 -0
- package/dist/src/index.d.ts +50 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +198 -1
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts +40 -0
- package/dist/src/nativeSpecs/NativeAmplyModule.d.ts.map +1 -1
- package/dist/src/systemEventUtils.d.ts.map +1 -1
- package/dist/src/systemEventUtils.js +3 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/ARCHITECTURE.md +4 -0
- package/docs/DEVELOPMENT.md +2 -1
- package/ios/Sources/AmplyReactNative/AmplyModule.mm +81 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative-generated.mm +21 -0
- package/ios/Sources/AmplyReactNative/AmplyReactNative/AmplyReactNative.h +11 -0
- package/package.json +1 -1
- package/src/__tests__/appLanguage.test.ts +298 -0
- package/src/__tests__/handleUrl.test.ts +155 -0
- package/src/__tests__/lastCampaignTouch.test.ts +151 -0
- package/src/__tests__/systemEventUtils.test.ts +22 -0
- package/src/campaignTouch.ts +74 -0
- package/src/index.ts +223 -1
- package/src/nativeSpecs/NativeAmplyModule.ts +41 -0
- package/src/systemEventUtils.ts +3 -1
package/AmplyReactNative.podspec
CHANGED
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
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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
|
|
package/android/src/main/java/tools/amply/sdk/reactnative/model/AmplyInitializationOptions.kt
CHANGED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"handleUrl.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/handleUrl.test.ts"],"names":[],"mappings":"AA0JA,OAAO,EAAE,CAAC"}
|