diff --git a/.gitignore b/.gitignore
index ab43486..1ead3f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,7 @@ DerivedData/
.DS_Store
*.tgz
coverage/
+
+# Android upload/release signing keys — never commit
+*.jks
+example/android/upload-keystore.jks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4a926f..299f849 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.0.3
+
+- Bump the Android native SDK to **`io.linktrail:sdk:0.0.5`** (from `0.0.4`).
+
+## 0.0.2
+
+Consent gating, a native paste button, and the latest native SDKs.
+
+- Native SDK bumps: iOS `LinkTrailSDK ~> 0.0.10`, Android `io.linktrail:sdk:0.0.4`.
+- **Consent gating** (GDPR / ePrivacy): `requireConsent` option (deny-by-default) +
+ `setConsent(granted)`. Deep links still route without consent; only tracking is gated.
+- iOS deferred click token: `clickTokenSource` option + `` component
+ (Apple `UIPasteControl`, themeable) + `trackInstallWithClickToken`.
+
## 0.0.1
Initial release. React Native New Architecture TurboModule wrapping the native LinkTrail
diff --git a/android/build.gradle b/android/build.gradle
index dd09549..8af7e28 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -69,6 +69,6 @@ repositories {
dependencies {
implementation "com.facebook.react:react-android"
- implementation "io.linktrail:sdk:0.0.4"
+ implementation "io.linktrail:sdk:0.0.5"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
}
diff --git a/example/.npmrc b/example/.npmrc
new file mode 100644
index 0000000..94b6546
--- /dev/null
+++ b/example/.npmrc
@@ -0,0 +1,5 @@
+# React Native's native build tooling (the Gradle autolinking plugin, CocoaPods)
+# assumes a flat node_modules. pnpm's default isolated store leaves
+# @react-native/gradle-plugin under .pnpm/, breaking the hardcoded
+# ../node_modules/@react-native/gradle-plugin path. Hoist so those paths resolve.
+node-linker=hoisted
diff --git a/example/App.tsx b/example/App.tsx
index 9b6d3e1..688b9d7 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -46,15 +46,16 @@ export default function App() {
// Route the picked scenario once the simulator sheet has closed (+ a beat for
// the dismiss animation), so the navigation sticks — the iOS example's pattern.
+ // NOTE: clear `pendingScenario` inside the timeout, not synchronously here —
+ // clearing it synchronously re-runs this effect and its cleanup cancels the
+ // timer before it ever fires (which broke navigation on both platforms).
useEffect(() => {
if (state.simulatorOpen || !pendingScenario) return;
const scenario = pendingScenario;
- setPendingScenario(null);
- const timer = setTimeout(
- () =>
- dispatch({ type: 'route-link', link: scenario.link, source: 'deferred' }),
- 350,
- );
+ const timer = setTimeout(() => {
+ dispatch({ type: 'route-link', link: scenario.link, source: 'deferred' });
+ setPendingScenario(null);
+ }, 350);
return () => clearTimeout(timer);
}, [state.simulatorOpen, pendingScenario]);
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 74c28ae..97c04a3 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -72,6 +72,17 @@ def enableProguardInReleaseBuilds = false
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
+// Release signing credentials are read from local.properties (gitignored), e.g.
+// KICKFLIP_UPLOAD_STORE_FILE=upload-keystore.jks
+// KICKFLIP_UPLOAD_STORE_PASSWORD=…
+// KICKFLIP_UPLOAD_KEY_ALIAS=…
+// KICKFLIP_UPLOAD_KEY_PASSWORD=…
+def uploadKeystoreProps = new Properties()
+def uploadKeystorePropsFile = rootProject.file('local.properties')
+if (uploadKeystorePropsFile.exists()) {
+ uploadKeystorePropsFile.withInputStream { uploadKeystoreProps.load(it) }
+}
+
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
@@ -79,11 +90,11 @@ android {
namespace "com.kickflip"
defaultConfig {
- applicationId "com.kickflip"
+ applicationId "io.linktrail.example"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
+ versionCode 17
+ versionName "0.0.17"
}
signingConfigs {
debug {
@@ -92,15 +103,24 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
+ release {
+ // Credentials read from local.properties (gitignored). The keystore
+ // path is relative to android/ (rootProject). See release buildType.
+ storeFile rootProject.file(uploadKeystoreProps.getProperty('KICKFLIP_UPLOAD_STORE_FILE', 'upload-keystore.jks'))
+ storePassword uploadKeystoreProps.getProperty('KICKFLIP_UPLOAD_STORE_PASSWORD', '')
+ keyAlias uploadKeystoreProps.getProperty('KICKFLIP_UPLOAD_KEY_ALIAS', '')
+ keyPassword uploadKeystoreProps.getProperty('KICKFLIP_UPLOAD_KEY_PASSWORD', '')
+ }
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
- // Caution! In production, you need to generate your own keystore file.
- // see https://reactnative.dev/docs/signed-apk-android.
- signingConfig signingConfigs.debug
+ // Sign with the upload keystore when its credentials are configured
+ // in local.properties; otherwise fall back to debug so other builds
+ // (CI, dev) don't break.
+ signingConfig uploadKeystoreProps.getProperty('KICKFLIP_UPLOAD_STORE_PASSWORD') ? signingConfigs.release : signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index e922a2c..0852809 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -37,7 +37,7 @@ PODS:
- hermes-engine (0.80.3):
- hermes-engine/Pre-built (= 0.80.3)
- hermes-engine/Pre-built (0.80.3)
- - LinktrailReactNative (0.0.1):
+ - LinktrailReactNative (0.0.3):
- boost
- DoubleConversion
- fast_float
@@ -2521,7 +2521,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: fff23161928673afcd95f63146e651726f68b384
- LinktrailReactNative: 3cc4dc425ca445f9363c594b219f4176f80ef339
+ LinktrailReactNative: 7ec4b501092c29257ae7ab64d4b44a8748139c32
LinkTrailSDK: b243fcedba4f7437df1171e460294cf81f256e25
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: 2ebc9cc0329ede460c5d52956ce5e1ab715c2c2c
diff --git a/example/package.json b/example/package.json
index 2d5751e..ae81c5d 100644
--- a/example/package.json
+++ b/example/package.json
@@ -7,7 +7,10 @@
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
- "test": "jest"
+ "test": "jest",
+ "android:aab": "cd android && ./gradlew :app:bundleRelease",
+ "android:apk": "cd android && ./gradlew :app:assembleRelease",
+ "android:clean": "cd android && ./gradlew clean"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^3.1.1",
diff --git a/example/pnpm-lock.yaml b/example/pnpm-lock.yaml
index 5370362..7449f66 100644
--- a/example/pnpm-lock.yaml
+++ b/example/pnpm-lock.yaml
@@ -2464,12 +2464,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@3.15.0:
- resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
+ js-yaml@3.15.1:
+ resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
hasBin: true
- js-yaml@4.3.0:
- resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
+ js-yaml@4.3.1:
+ resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
jsc-safe-url@0.2.4:
@@ -4384,7 +4384,7 @@ snapshots:
globals: 13.24.0
ignore: 5.3.2
import-fresh: 3.3.1
- js-yaml: 4.3.0
+ js-yaml: 4.3.1
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
@@ -4417,7 +4417,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
- js-yaml: 3.15.0
+ js-yaml: 3.15.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -5598,14 +5598,14 @@ snapshots:
dependencies:
import-fresh: 2.0.0
is-directory: 0.3.1
- js-yaml: 3.15.0
+ js-yaml: 3.15.1
parse-json: 4.0.0
cosmiconfig@9.0.2(typescript@5.0.4):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
- js-yaml: 4.3.0
+ js-yaml: 4.3.1
parse-json: 5.2.0
optionalDependencies:
typescript: 5.0.4
@@ -5967,7 +5967,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
- js-yaml: 4.3.0
+ js-yaml: 4.3.1
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
@@ -6814,12 +6814,12 @@ snapshots:
js-tokens@4.0.0: {}
- js-yaml@3.15.0:
+ js-yaml@3.15.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
- js-yaml@4.3.0:
+ js-yaml@4.3.1:
dependencies:
argparse: 2.0.1
diff --git a/example/src/attribution.ts b/example/src/attribution.ts
index b6bb36c..c4420be 100644
--- a/example/src/attribution.ts
+++ b/example/src/attribution.ts
@@ -1,4 +1,4 @@
-import { Linking } from 'react-native';
+import { Linking, Platform } from 'react-native';
import LinkTrail, { type LinkTrailDeepLink } from 'linktrail-react-native';
import type { StoreAction } from './store';
import { ConsentManager, type ConsentState } from './consent';
@@ -92,13 +92,15 @@ export function startAttribution(
await LinkTrail.configure(API_KEY, {
linkDomains: ['link.kynxlabs.com', 'kick.linktrail.io'],
requireConsent: true,
- // 'pasteButton' → the deferred-attribution click token is read from the
- // clipboard only when the user taps (no iOS
- // "Allow Paste" alert). autoTrackInstall MUST be false so the install
- // waits for that tap instead of firing token-less at launch (which would
- // mark the install tracked and make the paste tap a no-op).
+ // 'pasteButton' → iOS reads the deferred click token only when the user
+ // taps (no "Allow Paste" alert). Ignored on Android.
clickTokenSource: 'pasteButton',
- autoTrackInstall: false,
+ // iOS: false so the install waits for the paste tap (firing token-less at
+ // launch would mark the install tracked and make the tap a no-op).
+ // Android: true — there's no paste button; it uses the Play Install
+ // Referrer, so the install must auto-track for deferred attribution to
+ // resolve. (Consent still gates what's *recorded*; links route regardless.)
+ autoTrackInstall: Platform.OS !== 'ios',
});
} catch (error) {
console.warn('LinkTrail configuration failed:', error);
diff --git a/package.json b/package.json
index e61f139..68a9a2e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "linktrail-react-native",
- "version": "0.0.2",
+ "version": "0.0.3",
"description": "React Native SDK for LinkTrail — mobile attribution and deferred deep linking. A New Architecture TurboModule wrapping the native LinkTrail iOS and Android SDKs.",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",