linktrail-react-native 0.0.1
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/LICENSE +21 -0
- package/LinktrailReactNative.podspec +28 -0
- package/README.md +92 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt +243 -0
- package/android/src/main/java/io/linktrail/reactnative/LinkTrailPackage.kt +26 -0
- package/ios/LinkTrailModule.mm +129 -0
- package/ios/LinkTrailModuleImpl.swift +238 -0
- package/lib/module/NativeLinkTrail.js +15 -0
- package/lib/module/NativeLinkTrail.js.map +1 -0
- package/lib/module/index.js +141 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeLinkTrail.d.ts +47 -0
- package/lib/typescript/src/NativeLinkTrail.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +78 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +99 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +91 -0
- package/src/NativeLinkTrail.ts +63 -0
- package/src/index.ts +183 -0
- package/src/types.ts +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LinkTrail
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "LinktrailReactNative"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "15.1" }
|
|
14
|
+
s.source = { :git => "https://github.com/linktrail-io/react-native.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.swift_version = "5.9"
|
|
18
|
+
|
|
19
|
+
# The binary LinkTrail iOS SDK, published to the CocoaPods trunk — CocoaPods
|
|
20
|
+
# resolves it automatically, so the consuming app needs no extra Podfile line.
|
|
21
|
+
s.dependency "LinkTrailSDK", "~> 0.0.8"
|
|
22
|
+
|
|
23
|
+
s.pod_target_xcconfig = {
|
|
24
|
+
"DEFINES_MODULE" => "YES",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
install_modules_dependencies(s)
|
|
28
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# LinkTrail React Native SDK
|
|
2
|
+
|
|
3
|
+
Mobile attribution and deferred deep linking for React Native. A New Architecture **TurboModule**
|
|
4
|
+
that wraps the native [LinkTrail iOS](https://github.com/linktrail-io/ios-sdk) and
|
|
5
|
+
[Android](https://github.com/linktrail-io/android-sdk) SDKs — the API type is `LinkTrail`.
|
|
6
|
+
|
|
7
|
+
Requires React Native **0.76+** (New Architecture, the default), iOS **15.1+**, Android **minSdk 26+**.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install linktrail-react-native
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The native SDKs resolve automatically — `LinkTrailSDK` from the CocoaPods trunk, `io.linktrail:sdk`
|
|
16
|
+
from Maven Central — so there's nothing else to add.
|
|
17
|
+
|
|
18
|
+
- **iOS** — `cd ios && pod install`.
|
|
19
|
+
- **Android** — nothing extra; just make sure your app's `minSdkVersion` is **26** or higher.
|
|
20
|
+
- **Expo** — needs a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (not Expo Go). Set `minSdkVersion` via [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/), then `npx expo prebuild`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "expo": { "plugins": [["expo-build-properties", { "android": { "minSdkVersion": 26 } }]] } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import LinkTrail from 'linktrail-react-native';
|
|
30
|
+
|
|
31
|
+
// At app launch. The API key is required.
|
|
32
|
+
await LinkTrail.configure('lt_live_…');
|
|
33
|
+
|
|
34
|
+
// One listener handles both first-launch (deferred) AND re-engagement links:
|
|
35
|
+
LinkTrail.onLink((link, source) => {
|
|
36
|
+
router.navigate(link.path, link.customData); // e.g. "/products/aj1" + { voucher: "SUMMER25" }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Observe failures:
|
|
40
|
+
LinkTrail.onError((error) => console.warn(`[LinkTrail] ${error.code}: ${error.message}`));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The install is tracked automatically by `configure`, and incoming links (Universal Links / App
|
|
44
|
+
Links / custom schemes) are forwarded via React Native's `Linking` API — no extra wiring.
|
|
45
|
+
|
|
46
|
+
## More
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Custom post-install events:
|
|
50
|
+
await LinkTrail.trackEvent('purchase', { value: 59.99, currency: 'USD' });
|
|
51
|
+
|
|
52
|
+
// Cached results:
|
|
53
|
+
const attribution = await LinkTrail.getLastAttribution();
|
|
54
|
+
const lastLink = await LinkTrail.getLastDeepLink();
|
|
55
|
+
|
|
56
|
+
// Consent-gated install (defer configure's auto-track, then call manually):
|
|
57
|
+
await LinkTrail.configure('lt_live_…', { autoTrackInstall: false });
|
|
58
|
+
await LinkTrail.trackInstall();
|
|
59
|
+
|
|
60
|
+
// iOS ATT / SKAdNetwork (no-ops on Android):
|
|
61
|
+
await LinkTrail.requestTrackingAuthorization();
|
|
62
|
+
LinkTrail.registerForSKAdAttribution();
|
|
63
|
+
LinkTrail.updateConversionValue(42, 'medium');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`configure` also takes `{ logEnabled, logLevel, requestTimeoutMillis, retryPolicy, linkDomains,
|
|
67
|
+
autoTrackInstall, autoHandleLinks }`. Set `autoHandleLinks: false` to forward URLs yourself via
|
|
68
|
+
`LinkTrail.handleDeepLink(url)`.
|
|
69
|
+
|
|
70
|
+
## Deep-link setup
|
|
71
|
+
|
|
72
|
+
Standard app deep-link config — the wrapper handles the rest. Declare your LinkTrail host as a
|
|
73
|
+
Universal Link (iOS Associated Domains: `applinks:kick.linktrail.io`) and an App Links
|
|
74
|
+
`intent-filter` (Android), plus any custom scheme. LinkTrail infra hosts the
|
|
75
|
+
`apple-app-site-association` / `assetlinks.json` files for your link domains.
|
|
76
|
+
|
|
77
|
+
## Example app
|
|
78
|
+
|
|
79
|
+
[`example/`](example/) is **KickFlip**, a storefront that shows deferred deep linking end to end,
|
|
80
|
+
consuming this package the same way your app would:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
cd example && npm install && npm run ios # or: npm run android
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set your `lt_live_…` key in [`src/attribution.ts`](example/src/attribution.ts), then tap the 🔗
|
|
87
|
+
button to fire the four scenarios (home · category · product · product+voucher). The simulator
|
|
88
|
+
works without a key.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = { name, fallback ->
|
|
3
|
+
rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.6.0"
|
|
13
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion', '2.0.21')}"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def isNewArchitectureEnabled() {
|
|
18
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!isNewArchitectureEnabled()) {
|
|
22
|
+
throw new GradleException(
|
|
23
|
+
"linktrail-react-native is a TurboModule and requires the React Native New Architecture. " +
|
|
24
|
+
"Enable it with newArchEnabled=true in gradle.properties (the default since React Native 0.76)."
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
apply plugin: "com.android.library"
|
|
29
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
30
|
+
apply plugin: "com.facebook.react"
|
|
31
|
+
|
|
32
|
+
android {
|
|
33
|
+
namespace "io.linktrail.reactnative"
|
|
34
|
+
|
|
35
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion", 35)
|
|
36
|
+
|
|
37
|
+
defaultConfig {
|
|
38
|
+
// The LinkTrail Android SDK requires API 26+.
|
|
39
|
+
minSdkVersion getExtOrDefault("minSdkVersion", 26)
|
|
40
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion", 35)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
kotlinOptions {
|
|
49
|
+
jvmTarget = "17"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sourceSets {
|
|
53
|
+
main {
|
|
54
|
+
java.srcDirs += ["generated/java", "generated/jni"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
react {
|
|
60
|
+
jsRootDir = file("../src/")
|
|
61
|
+
libraryName = "LinkTrailSpec"
|
|
62
|
+
codegenJavaPackageName = "io.linktrail.reactnative"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
repositories {
|
|
66
|
+
google()
|
|
67
|
+
mavenCentral() // the LinkTrail SDK (io.linktrail:sdk) is published to Maven Central
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
dependencies {
|
|
71
|
+
implementation "com.facebook.react:react-android"
|
|
72
|
+
implementation "io.linktrail:sdk:0.0.3"
|
|
73
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
package io.linktrail.reactnative
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.bridge.Promise
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.ReadableType
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import io.linktrail.LinkTrail
|
|
11
|
+
import io.linktrail.LinkTrailError
|
|
12
|
+
import io.linktrail.LinkTrailLogLevel
|
|
13
|
+
import io.linktrail.LinkTrailOptions
|
|
14
|
+
import io.linktrail.LinkTrailRetryPolicy
|
|
15
|
+
import io.linktrail.model.LinkTrailAttribution
|
|
16
|
+
import io.linktrail.model.LinkTrailDeepLink
|
|
17
|
+
import io.linktrail.model.LinkTrailLinkSource
|
|
18
|
+
import kotlinx.coroutines.CoroutineScope
|
|
19
|
+
import kotlinx.coroutines.Dispatchers
|
|
20
|
+
import kotlinx.coroutines.SupervisorJob
|
|
21
|
+
import kotlinx.coroutines.cancel
|
|
22
|
+
import kotlinx.coroutines.launch
|
|
23
|
+
|
|
24
|
+
class LinkTrailModule(reactContext: ReactApplicationContext) :
|
|
25
|
+
NativeLinkTrailSpec(reactContext) {
|
|
26
|
+
|
|
27
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
28
|
+
|
|
29
|
+
override fun invalidate() {
|
|
30
|
+
scope.cancel()
|
|
31
|
+
super.invalidate()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
override fun configure(apiKey: String, options: ReadableMap?, promise: Promise) {
|
|
37
|
+
try {
|
|
38
|
+
val sdk = LinkTrail.configure(reactApplicationContext, apiKey, parseOptions(options))
|
|
39
|
+
sdk.onAttribution { attribution -> emitOnAttribution(attributionToMap(attribution)) }
|
|
40
|
+
sdk.onLink { link, source ->
|
|
41
|
+
emitOnLink(
|
|
42
|
+
Arguments.createMap().apply {
|
|
43
|
+
putMap("link", deepLinkToMap(link))
|
|
44
|
+
putString("source", sourceName(source))
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
sdk.onError { error ->
|
|
49
|
+
emitOnError(
|
|
50
|
+
Arguments.createMap().apply {
|
|
51
|
+
putString("code", errorCode(error))
|
|
52
|
+
putString("message", error.message ?: "Unknown error")
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
promise.resolve(null)
|
|
57
|
+
} catch (t: Throwable) {
|
|
58
|
+
promise.reject(errorCode(t), t.message, t)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Install / events ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
override fun trackInstall(force: Boolean, promise: Promise) {
|
|
65
|
+
val sdk = requireSdk(promise) ?: return
|
|
66
|
+
scope.launch {
|
|
67
|
+
try {
|
|
68
|
+
promise.resolve(attributionToMap(sdk.trackInstallAsync(force)))
|
|
69
|
+
} catch (t: Throwable) {
|
|
70
|
+
promise.reject(errorCode(t), t.message, t)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun trackEvent(name: String, value: Double?, currency: String?, promise: Promise) {
|
|
76
|
+
val sdk = requireSdk(promise) ?: return
|
|
77
|
+
scope.launch {
|
|
78
|
+
try {
|
|
79
|
+
val result = sdk.trackEventAsync(name, value, currency)
|
|
80
|
+
promise.resolve(
|
|
81
|
+
Arguments.createMap().apply {
|
|
82
|
+
result.id?.let { putInt("id", it) }
|
|
83
|
+
putBoolean("attributed", result.attributed)
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
} catch (t: Throwable) {
|
|
87
|
+
promise.reject(errorCode(t), t.message, t)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Deep linking ───────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
override fun handleDeepLink(url: String, promise: Promise) {
|
|
95
|
+
val sdk = requireSdk(promise) ?: return
|
|
96
|
+
scope.launch {
|
|
97
|
+
try {
|
|
98
|
+
promise.resolve(deepLinkToMap(sdk.handleDeepLinkAsync(Uri.parse(url))))
|
|
99
|
+
} catch (e: LinkTrailError.NotALinkTrailUrl) {
|
|
100
|
+
promise.resolve(null)
|
|
101
|
+
} catch (t: Throwable) {
|
|
102
|
+
promise.reject(errorCode(t), t.message, t)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override fun getLastAttribution(promise: Promise) {
|
|
108
|
+
promise.resolve(LinkTrail.shared?.lastAttribution?.let(::attributionToMap))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override fun getLastDeepLink(promise: Promise) {
|
|
112
|
+
promise.resolve(LinkTrail.shared?.lastDeepLink?.let(::deepLinkToMap))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── iOS-only APIs (no-ops on Android) ──────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
override fun requestTrackingAuthorization(promise: Promise) {
|
|
118
|
+
promise.resolve(false)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
override fun registerForSKAdAttribution() = Unit
|
|
122
|
+
|
|
123
|
+
override fun updateConversionValue(value: Double, coarseValue: String?) = Unit
|
|
124
|
+
|
|
125
|
+
// ── Testing ────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
override fun resetForTesting() {
|
|
128
|
+
LinkTrail.resetForTesting(reactApplicationContext)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
private fun requireSdk(promise: Promise): LinkTrail? {
|
|
134
|
+
val sdk = LinkTrail.shared
|
|
135
|
+
if (sdk == null) {
|
|
136
|
+
promise.reject(
|
|
137
|
+
"not_configured",
|
|
138
|
+
"LinkTrail is not configured. Call LinkTrail.configure(apiKey) first."
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return sdk
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private fun parseOptions(map: ReadableMap?): LinkTrailOptions {
|
|
145
|
+
if (map == null) return LinkTrailOptions()
|
|
146
|
+
val defaults = LinkTrailOptions()
|
|
147
|
+
|
|
148
|
+
val retryMap = if (map.hasKey("retryPolicy")) map.getMap("retryPolicy") else null
|
|
149
|
+
val defaultRetry = LinkTrailRetryPolicy.DEFAULT
|
|
150
|
+
val retryPolicy = if (retryMap == null) defaultRetry else LinkTrailRetryPolicy(
|
|
151
|
+
maxAttempts = retryMap.intOrDefault("maxAttempts", defaultRetry.maxAttempts),
|
|
152
|
+
baseDelayMillis = retryMap.longOrDefault("baseDelayMillis", defaultRetry.baseDelayMillis),
|
|
153
|
+
maxDelayMillis = retryMap.longOrDefault("maxDelayMillis", defaultRetry.maxDelayMillis),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
val linkDomains = mutableListOf<String>()
|
|
157
|
+
if (map.hasKey("linkDomains")) {
|
|
158
|
+
map.getArray("linkDomains")?.let { array ->
|
|
159
|
+
for (i in 0 until array.size()) array.getString(i)?.let(linkDomains::add)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return LinkTrailOptions(
|
|
164
|
+
logEnabled = map.booleanOrDefault("logEnabled", defaults.logEnabled),
|
|
165
|
+
logLevel = logLevel(map.stringOrNull("logLevel"), defaults.logLevel),
|
|
166
|
+
requestTimeoutMillis = map.longOrDefault("requestTimeoutMillis", defaults.requestTimeoutMillis),
|
|
167
|
+
retryPolicy = retryPolicy,
|
|
168
|
+
linkDomains = linkDomains,
|
|
169
|
+
autoTrackInstall = map.booleanOrDefault("autoTrackInstall", defaults.autoTrackInstall),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun logLevel(name: String?, fallback: LinkTrailLogLevel): LinkTrailLogLevel =
|
|
174
|
+
when (name) {
|
|
175
|
+
"debug" -> LinkTrailLogLevel.DEBUG
|
|
176
|
+
"info" -> LinkTrailLogLevel.INFO
|
|
177
|
+
"warning" -> LinkTrailLogLevel.WARNING
|
|
178
|
+
"error" -> LinkTrailLogLevel.ERROR
|
|
179
|
+
"none" -> LinkTrailLogLevel.NONE
|
|
180
|
+
else -> fallback
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private fun sourceName(source: LinkTrailLinkSource): String = when (source) {
|
|
184
|
+
LinkTrailLinkSource.DEFERRED -> "deferred"
|
|
185
|
+
LinkTrailLinkSource.REENGAGEMENT -> "reengagement"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private fun attributionToMap(attribution: LinkTrailAttribution): WritableMap =
|
|
189
|
+
Arguments.createMap().apply {
|
|
190
|
+
attribution.id?.let { putInt("id", it) }
|
|
191
|
+
putBoolean("attributed", attribution.attributed)
|
|
192
|
+
attribution.deepLink?.let { putMap("deepLink", deepLinkToMap(it)) }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private fun deepLinkToMap(link: LinkTrailDeepLink): WritableMap =
|
|
196
|
+
Arguments.createMap().apply {
|
|
197
|
+
link.slug?.let { putString("slug", it) }
|
|
198
|
+
link.url?.let { putString("url", it) }
|
|
199
|
+
link.deepLinkPath?.let { putString("deepLinkPath", it) }
|
|
200
|
+
link.iosUrl?.let { putString("iosUrl", it) }
|
|
201
|
+
link.androidUrl?.let { putString("androidUrl", it) }
|
|
202
|
+
link.fallbackUrl?.let { putString("fallbackUrl", it) }
|
|
203
|
+
link.campaign?.let { putString("campaign", it) }
|
|
204
|
+
link.channel?.let { putString("channel", it) }
|
|
205
|
+
link.utm?.let { putMap("utm", stringMapToMap(it)) }
|
|
206
|
+
link.customData?.let { putMap("customData", stringMapToMap(it)) }
|
|
207
|
+
putString("path", link.path)
|
|
208
|
+
putBoolean("hasRoutableDestination", link.hasRoutableDestination)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun stringMapToMap(values: Map<String, String>): WritableMap =
|
|
212
|
+
Arguments.createMap().apply {
|
|
213
|
+
values.forEach { (key, value) -> putString(key, value) }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun errorCode(error: Throwable): String = when (error) {
|
|
217
|
+
is LinkTrailError.MissingApiKey -> "missing_api_key"
|
|
218
|
+
is LinkTrailError.InvalidApiKey -> "invalid_api_key"
|
|
219
|
+
is LinkTrailError.Transport -> "transport"
|
|
220
|
+
is LinkTrailError.Server -> "server"
|
|
221
|
+
is LinkTrailError.Decoding -> "decoding"
|
|
222
|
+
is LinkTrailError.EmptyResponse -> "empty_response"
|
|
223
|
+
is LinkTrailError.InvalidUrl -> "invalid_url"
|
|
224
|
+
is LinkTrailError.NotALinkTrailUrl -> "not_a_linktrail_url"
|
|
225
|
+
else -> "unknown"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun ReadableMap.booleanOrDefault(key: String, fallback: Boolean): Boolean =
|
|
229
|
+
if (hasKey(key) && getType(key) == ReadableType.Boolean) getBoolean(key) else fallback
|
|
230
|
+
|
|
231
|
+
private fun ReadableMap.intOrDefault(key: String, fallback: Int): Int =
|
|
232
|
+
if (hasKey(key) && getType(key) == ReadableType.Number) getInt(key) else fallback
|
|
233
|
+
|
|
234
|
+
private fun ReadableMap.longOrDefault(key: String, fallback: Long): Long =
|
|
235
|
+
if (hasKey(key) && getType(key) == ReadableType.Number) getDouble(key).toLong() else fallback
|
|
236
|
+
|
|
237
|
+
private fun ReadableMap.stringOrNull(key: String): String? =
|
|
238
|
+
if (hasKey(key) && getType(key) == ReadableType.String) getString(key) else null
|
|
239
|
+
|
|
240
|
+
companion object {
|
|
241
|
+
const val NAME = "LinkTrail"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package io.linktrail.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class LinkTrailPackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
11
|
+
if (name == LinkTrailModule.NAME) LinkTrailModule(reactContext) else null
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
|
|
14
|
+
ReactModuleInfoProvider {
|
|
15
|
+
mapOf(
|
|
16
|
+
LinkTrailModule.NAME to ReactModuleInfo(
|
|
17
|
+
name = LinkTrailModule.NAME,
|
|
18
|
+
className = LinkTrailModule::class.java.name,
|
|
19
|
+
canOverrideExistingModule = false,
|
|
20
|
+
needsEagerInit = false,
|
|
21
|
+
isCxxModule = false,
|
|
22
|
+
isTurboModule = true,
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The codegen spec header (and the -Swift.h bridge) are C++-heavy; importing
|
|
2
|
+
// them only from this ObjC++ (.mm) translation unit — never from a public
|
|
3
|
+
// header — keeps them out of the pod's Clang module umbrella, which otherwise
|
|
4
|
+
// fails to build (libc++ headers unresolved under explicit modules).
|
|
5
|
+
#if __has_include(<LinkTrailSpec/LinkTrailSpec.h>)
|
|
6
|
+
#import <LinkTrailSpec/LinkTrailSpec.h>
|
|
7
|
+
#else
|
|
8
|
+
#import "LinkTrailSpec.h"
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
#if __has_include("LinktrailReactNative-Swift.h")
|
|
12
|
+
#import "LinktrailReactNative-Swift.h"
|
|
13
|
+
#else
|
|
14
|
+
#import <LinktrailReactNative/LinktrailReactNative-Swift.h>
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
// TurboModule registered as "LinkTrail". Delegates to LinkTrailModuleImpl
|
|
18
|
+
// (Swift), which talks to the binary LinkTrailSDK framework. Declared here
|
|
19
|
+
// (not in a public header) on purpose — see the note above.
|
|
20
|
+
@interface LinkTrailModule : NativeLinkTrailSpecBase <NativeLinkTrailSpec>
|
|
21
|
+
@end
|
|
22
|
+
|
|
23
|
+
@implementation LinkTrailModule {
|
|
24
|
+
LinkTrailModuleImpl *_impl;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
RCT_EXPORT_MODULE(LinkTrail)
|
|
28
|
+
|
|
29
|
+
- (instancetype)init
|
|
30
|
+
{
|
|
31
|
+
if (self = [super init]) {
|
|
32
|
+
_impl = [LinkTrailModuleImpl new];
|
|
33
|
+
__weak LinkTrailModule *weakSelf = self;
|
|
34
|
+
_impl.onLink = ^(NSDictionary *payload) {
|
|
35
|
+
[weakSelf emitOnLink:payload];
|
|
36
|
+
};
|
|
37
|
+
_impl.onAttribution = ^(NSDictionary *payload) {
|
|
38
|
+
[weakSelf emitOnAttribution:payload];
|
|
39
|
+
};
|
|
40
|
+
_impl.onError = ^(NSDictionary *payload) {
|
|
41
|
+
[weakSelf emitOnError:payload];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return self;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
- (void)configure:(NSString *)apiKey
|
|
48
|
+
options:(NSDictionary *)options
|
|
49
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
50
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
51
|
+
{
|
|
52
|
+
[_impl configure:apiKey
|
|
53
|
+
options:options
|
|
54
|
+
resolve:resolve
|
|
55
|
+
reject:^(NSString *code, NSString *message) { reject(code, message, nil); }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)trackInstall:(BOOL)force
|
|
59
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
60
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
61
|
+
{
|
|
62
|
+
[_impl trackInstall:force
|
|
63
|
+
resolve:resolve
|
|
64
|
+
reject:^(NSString *code, NSString *message) { reject(code, message, nil); }];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (void)trackEvent:(NSString *)name
|
|
68
|
+
value:(NSNumber *)value
|
|
69
|
+
currency:(NSString *)currency
|
|
70
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
71
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
72
|
+
{
|
|
73
|
+
[_impl trackEvent:name
|
|
74
|
+
value:value
|
|
75
|
+
currency:currency
|
|
76
|
+
resolve:resolve
|
|
77
|
+
reject:^(NSString *code, NSString *message) { reject(code, message, nil); }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
- (void)handleDeepLink:(NSString *)url
|
|
81
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
82
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
83
|
+
{
|
|
84
|
+
[_impl handleDeepLink:url
|
|
85
|
+
resolve:resolve
|
|
86
|
+
reject:^(NSString *code, NSString *message) { reject(code, message, nil); }];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)getLastAttribution:(RCTPromiseResolveBlock)resolve
|
|
90
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
91
|
+
{
|
|
92
|
+
[_impl getLastAttribution:resolve];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
- (void)getLastDeepLink:(RCTPromiseResolveBlock)resolve
|
|
96
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
97
|
+
{
|
|
98
|
+
[_impl getLastDeepLink:resolve];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
|
|
102
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
103
|
+
{
|
|
104
|
+
[_impl requestTrackingAuthorization:resolve
|
|
105
|
+
reject:^(NSString *code, NSString *message) { reject(code, message, nil); }];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
- (void)registerForSKAdAttribution
|
|
109
|
+
{
|
|
110
|
+
[_impl registerForSKAdAttribution];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (void)updateConversionValue:(double)value coarseValue:(NSString *)coarseValue
|
|
114
|
+
{
|
|
115
|
+
[_impl updateConversionValue:(NSInteger)llround(value) coarseValue:coarseValue];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (void)resetForTesting
|
|
119
|
+
{
|
|
120
|
+
[_impl resetForTesting];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
124
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
125
|
+
{
|
|
126
|
+
return std::make_shared<facebook::react::NativeLinkTrailSpecJSI>(params);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@end
|