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",