diff --git a/CHANGELOG.md b/CHANGELOG.md index f2690f8..1ec7420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +- New `updatePostbackConversionValue(value, { coarseValue, lockPostback, conversionTypes })` (iOS): + AdAttributionKit postback updates, with locking and per-conversion-type updates on iOS 18+. + Resolves `false` on Android. `registerForSKAdAttribution()` / `updateConversionValue()` now reach + AdAttributionKit too. +- Requires the iOS native SDK **`LinkTrailSDK ~> 0.0.13`** (the first release with AdAttributionKit). + ## 0.0.4 - Bump the iOS native SDK to **`LinkTrailSDK ~> 0.0.11`** (from `0.0.10`). diff --git a/LinktrailReactNative.podspec b/LinktrailReactNative.podspec index 37ddda6..2f8c0a3 100644 --- a/LinktrailReactNative.podspec +++ b/LinktrailReactNative.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| # The binary LinkTrail iOS SDK, published to the CocoaPods trunk — CocoaPods # resolves it automatically, so the consuming app needs no extra Podfile line. - s.dependency "LinkTrailSDK", "~> 0.0.11" + s.dependency "LinkTrailSDK", "~> 0.0.13" s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES", diff --git a/README.md b/README.md index 25b178f..87181c3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ const lastLink = await LinkTrail.getLastDeepLink(); await LinkTrail.requestTrackingAuthorization(); LinkTrail.registerForSKAdAttribution(); LinkTrail.updateConversionValue(42, 'medium'); +// iOS 17.4+ AdAttributionKit (SKAdNetwork before); conversionTypes applies on iOS 18+. +// Resolves false on Android. +await LinkTrail.updatePostbackConversionValue(12, { + coarseValue: 'high', + lockPostback: false, + conversionTypes: ['install'], +}); ``` `configure` also takes `{ logEnabled, logLevel, requestTimeoutMillis, retryPolicy, linkDomains, diff --git a/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt b/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt index 6f9673b..dce0913 100644 --- a/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt +++ b/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.WritableMap @@ -135,6 +136,17 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : override fun updateConversionValue(value: Double, coarseValue: String?) = Unit + override fun updatePostbackConversionValue( + value: Double, + coarseValue: String?, + lockPostback: Boolean, + conversionTypes: ReadableArray, + promise: Promise + ) { + // AdAttributionKit is Apple-only. + promise.resolve(false) + } + // ── Consent ────────────────────────────────────────────────────────────── override fun setConsent(granted: Boolean) { diff --git a/ios/LinkTrailModule.mm b/ios/LinkTrailModule.mm index c46bce2..48976a3 100644 --- a/ios/LinkTrailModule.mm +++ b/ios/LinkTrailModule.mm @@ -126,6 +126,21 @@ - (void)updateConversionValue:(double)value coarseValue:(NSString *)coarseValue [_impl updateConversionValue:(NSInteger)llround(value) coarseValue:coarseValue]; } +- (void)updatePostbackConversionValue:(double)value + coarseValue:(NSString *)coarseValue + lockPostback:(BOOL)lockPostback + conversionTypes:(NSArray *)conversionTypes + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_impl updatePostbackConversionValue:(NSInteger)llround(value) + coarseValue:coarseValue + lockPostback:lockPostback + conversionTypes:conversionTypes + resolve:resolve + reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; +} + - (void)setConsent:(BOOL)granted { [_impl setConsent:granted]; diff --git a/ios/LinkTrailModuleImpl.swift b/ios/LinkTrailModuleImpl.swift index a9befed..b42ce9b 100644 --- a/ios/LinkTrailModuleImpl.swift +++ b/ios/LinkTrailModuleImpl.swift @@ -153,6 +153,26 @@ public final class LinkTrailModuleImpl: NSObject { sdk?.updateConversionValue(value, coarseValue: coarse) } + @objc public func updatePostbackConversionValue( + _ value: Int, + coarseValue: String?, + lockPostback: Bool, + conversionTypes: [String], + resolve: @escaping Resolve, + reject: @escaping Reject + ) { + guard let sdk = requireSDK(reject) else { return } + let coarse = coarseValue.flatMap(LinkTrailCoarseConversionValue.init(rawValue:)) + // An empty list means "all types"; unknown strings are dropped rather than failing the call. + let types = Set(conversionTypes.compactMap(LinkTrailConversionType.init(rawValue:))) + Task { + resolve( + await sdk.updatePostbackConversionValue( + value, coarseValue: coarse, lockPostback: lockPostback, + conversionTypes: types.isEmpty ? nil : types)) + } + } + // MARK: - Consent @objc public func setConsent(_ granted: Bool) { diff --git a/src/NativeLinkTrail.ts b/src/NativeLinkTrail.ts index 8c72ed1..0ba7038 100644 --- a/src/NativeLinkTrail.ts +++ b/src/NativeLinkTrail.ts @@ -53,6 +53,18 @@ export interface Spec extends TurboModule { /** iOS: updates the SKAdNetwork conversion value. Android: no-op. */ updateConversionValue(value: number, coarseValue?: string): void; + /** + * iOS: updates pending AdAttributionKit / SKAdNetwork postbacks. `conversionTypes` + * is a list of 'install' / 'reengagement' (empty = all). Resolves whether every + * framework call succeeded. Android: resolves `false`. + */ + updatePostbackConversionValue( + value: number, + coarseValue: string | undefined, + lockPostback: boolean, + conversionTypes: Array + ): Promise; + /** Grants/revokes tracking consent (only meaningful with `requireConsent`). */ setConsent(granted: boolean): void; diff --git a/src/index.ts b/src/index.ts index a396586..1aaafd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import NativeLinkTrail from './NativeLinkTrail'; import type { LinkTrailAttribution, LinkTrailCoarseConversionValue, + LinkTrailPostbackUpdateOptions, LinkTrailDeepLink, LinkTrailErrorEvent, LinkTrailEventResult, @@ -190,6 +191,25 @@ export const LinkTrail = { NativeLinkTrail.updateConversionValue(value, coarseValue); }, + /** + * iOS: updates the conversion value of pending AdAttributionKit (iOS 17.4+) and + * SKAdNetwork postbacks, with postback locking and, on iOS 18+, per-conversion-type + * updates. Resolves `true` when every framework call succeeded (also when the + * user never saw an ad, since there is nothing to update). Android: resolves + * `false` — there is no equivalent. + */ + updatePostbackConversionValue( + value: number, + options: LinkTrailPostbackUpdateOptions = {} + ): Promise { + return NativeLinkTrail.updatePostbackConversionValue( + value, + options.coarseValue, + options.lockPostback ?? false, + options.conversionTypes ?? [] + ); + }, + /** * Grants or revokes the user's tracking consent. Only meaningful when * `configure` was called with `requireConsent: true` (the default). diff --git a/src/types.ts b/src/types.ts index a290324..5e2c050 100644 --- a/src/types.ts +++ b/src/types.ts @@ -122,6 +122,22 @@ export type LinkTrailClickTokenSource = 'pasteButton' | 'automatic' | 'none'; /** SKAdNetwork 4.0+ coarse conversion value bucket (iOS only). */ export type LinkTrailCoarseConversionValue = 'low' | 'medium' | 'high'; +/** + * Which conversions an AdAttributionKit postback update applies to (iOS 18+; + * earlier versions update every type). + */ +export type LinkTrailConversionType = 'install' | 'reengagement'; + +/** Options for `updatePostbackConversionValue` (iOS only). */ +export interface LinkTrailPostbackUpdateOptions { + /** Coarse value for lower crowd-anonymity tiers. Defaults to `'low'`. */ + coarseValue?: LinkTrailCoarseConversionValue; + /** Finalise the postback so no later update applies. Defaults to `false`. */ + lockPostback?: boolean; + /** Restrict the update to these conversion types (iOS 18+). Omit for all. */ + conversionTypes?: LinkTrailConversionType[]; +} + /** A subscription returned by the event-listener APIs. */ export interface LinkTrailSubscription { remove(): void;