From fc8db61e59b15e19198fec93929d77ed58e1c0de Mon Sep 17 00:00:00 2001 From: Shofiya <86974918+Shofiya2003@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:47:22 +0530 Subject: [PATCH 1/5] docs(ios): add Google Integrated Conversion Measurement setup [LIN-2078] Adds an optional ICM section to the iOS SDK guide covering the three setup steps, the consent API, the TCF alternative, and how to verify the integration from the debug log. Also corrects the Google Ads discrepancies page, which described iOS as effectively SKAN-only with deterministic attribution available only when ATT is granted. That stops being true for the EEA, UK, and Switzerland once ICM is in place. Co-Authored-By: Claude Opus 5 (1M context) --- ad-networks/google-ads-discrepancies.mdx | 3 +- sdk/ios.mdx | 94 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/ad-networks/google-ads-discrepancies.mdx b/ad-networks/google-ads-discrepancies.mdx index 6dfd40f..9615d45 100644 --- a/ad-networks/google-ads-discrepancies.mdx +++ b/ad-networks/google-ads-discrepancies.mdx @@ -80,6 +80,7 @@ Google is a Self-Reporting Network (SRN), so Google Ads reports its own attribut - **Android:** Requires GAID collection. The Linkrunner SDK captures GAID automatically when the user has not opted out. - **iOS:** Measurement happens via **SKAdNetwork (SKAN)**. Google Ads sends SKAN postbacks to Linkrunner, and installs with `did_win=TRUE` are counted. Deterministic SDK attribution only works when the user grants ATT and the IDFA is available. See [SKAdNetwork integration](/features/skadnetwork-integration) and [iOS Campaign Data Limitations](/features/ios-campaign-data-limitations). +- **iOS in the EEA, UK, and Switzerland:** [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) can attribute installs in these regions even when ATT is denied and no IDFA is available. It complements SKAN rather than replacing it, and needs setup in your iOS app. See the [Google Ads integration guide](/ad-networks/google-ads) for full setup on each platform. @@ -98,7 +99,7 @@ Google Ads dates conversions to the **ad click**. Linkrunner dates them to the * As an SRN, Google attributes every install that follows any Google Ads engagement within its window. Linkrunner uses **last-click attribution**, deduplicates across networks, and counts one install per user. -On iOS, Linkrunner counts only SKAN postbacks with `did_win=TRUE`. Google Ads treats all SKAN events the same, so its dashboard typically reports more iOS installs. +On iOS, Linkrunner counts only SKAN postbacks with `did_win=TRUE`. Google Ads treats all SKAN events the same, so its dashboard typically reports more iOS installs. [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) narrows this gap in the EEA, UK, and Switzerland. diff --git a/sdk/ios.mdx b/sdk/ios.mdx index 94af2ef..966dae5 100644 --- a/sdk/ios.mdx +++ b/sdk/ios.mdx @@ -657,6 +657,100 @@ func capturePurchase() async { > **Note:** For more information on testing and verifying your ecommerce events, please see our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide. +## Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. + +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. + + + + Linkrunner does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your app yourself. + + If you already use the Firebase iOS SDK 11.14.0 or later, you have it and can move to the next step. + + + + Add `https://github.com/googleads/google-ads-on-device-conversion-ios-sdk` in **File** → **Add Package Dependencies...** and select the `GoogleAdsOnDeviceConversion` product. + + + ```ruby + pod 'GoogleAdsOnDeviceConversion' + ``` + + + + + + Google's framework needs these in **Build Settings** → **Other Linker Flags** on your app target: + + ``` + -ObjC -lc++ + ``` + + CocoaPods adds them for you. Swift Package Manager does not. + + + + Google requires consent signals for users in these regions. See [Consent](#consent) below. + + + +That is the whole integration. Linkrunner detects the SDK at runtime and there is no API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + ICM complements SKAdNetwork, it does not replace it. Keep your existing [SKAdNetwork integration](/features/skadnetwork-integration) in place. + + +### Consent + +Google needs to know whether European regulations apply to a user and what that user agreed to. Pass the values from your Consent Management Platform (CMP) before you call `initialize`, and again whenever the user changes their choice. + +```swift +LinkrunnerSDK.shared.setConsent( + LinkrunnerConsent( + isEEA: .granted, + hasConsentForDataUsage: .granted, + hasConsentForAdsPersonalization: .denied + ) +) + +await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") +``` + +| Parameter | Meaning | +| --- | --- | +| `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | +| `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | +| `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + +Each takes `.granted`, `.denied`, or `.unknown`. Anything left `.unknown` is omitted rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + + + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after your user has withdrawn it. + + +#### Using a TCF Consent Management Platform + +If your CMP supports IAB TCF v2.2 or v2.3, Linkrunner can read consent directly instead: + +```swift +LinkrunnerSDK.shared.enableTCFConsentCollection(true) +await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") +``` + +Anything you set explicitly with `setConsent` still takes priority, per signal. This setting is not stored between launches, so call it on every launch before `initialize`. + +### Verifying your setup + +Initialize with `debug: true` and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Recheck step 1. + ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: From 151078d113fd2d75948b8f50002372b4e5bed658 Mon Sep 17 00:00:00 2001 From: Shofiya <86974918+Shofiya2003@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:32:17 +0530 Subject: [PATCH 2/5] docs(ios): correct the -ObjC linker flag requirement [LIN-2078] The page said CocoaPods applies the linker flags for you. That holds for -lc++, which Google's podspec declares, but not for -ObjC, which is the flag that actually matters. Branch documents -ObjC as required regardless of integration method, and Adjust gives the same reason we have: the SDK discovers Google's class at runtime, so nothing references it at link time and the linker strips it from the static library without the flag. Also notes that a missing flag is the more likely cause of odm_available=false, since it produces no build warning. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/ios.mdx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sdk/ios.mdx b/sdk/ios.mdx index 966dae5..44ee2ad 100644 --- a/sdk/ios.mdx +++ b/sdk/ios.mdx @@ -681,14 +681,18 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth - - Google's framework needs these in **Build Settings** → **Other Linker Flags** on your app target: + + Add this to **Build Settings** → **Other Linker Flags** on your app target: ``` - -ObjC -lc++ + -ObjC ``` - CocoaPods adds them for you. Swift Package Manager does not. + + Required no matter how you added the SDK, including CocoaPods. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time. Google ships a static library, and linkers only include static library members that something references. Without `-ObjC` the class is stripped, and ICM silently does nothing while your build succeeds. + + + You do not need `-lc++`. Google's SDK applies it for you. @@ -749,7 +753,7 @@ Initialize with `debug: true` and look for this line in the Xcode console: Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 ``` -`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Recheck step 1. +`odm_available=false` with `odm_fetch_result=unavailable` means either Google's SDK is not linked or the `-ObjC` flag is missing. Check the flag first, because a missing flag produces no build warning. ## Enhanced Privacy Controls From d77fbfe686b9c9544dfe36cb2217abb648d1a226 Mon Sep 17 00:00:00 2001 From: Shofiya <86974918+Shofiya2003@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:46:28 +0530 Subject: [PATCH 3/5] docs(ios): scope the -ObjC linker flag to Swift Package Manager [LIN-2078] The step told every reader to add -ObjC, including CocoaPods users who do not need it. CocoaPods writes the flag into its generated xcconfig itself when a target links a vendored static framework, so pod users need no build settings at all. Tested in one project, changing only the flag: SPM, -ObjC set -> ODCConversionManager present (13 symbols) SPM, -ObjC absent -> stripped (0 symbols), build still succeeded CocoaPods, nothing set -> present (13 symbols) The step is now titled for Swift Package Manager and opens with a note telling CocoaPods users to skip it. The troubleshooting line is scoped the same way. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/ios.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/ios.mdx b/sdk/ios.mdx index 44ee2ad..6d74f77 100644 --- a/sdk/ios.mdx +++ b/sdk/ios.mdx @@ -681,18 +681,20 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth - - Add this to **Build Settings** → **Other Linker Flags** on your app target: + + + Using CocoaPods? Skip this step. CocoaPods adds the required flags for you. + + + If you added Google's SDK with Swift Package Manager, add this to **Build Settings** → **Other Linker Flags** on your app target: ``` -ObjC ``` - Required no matter how you added the SDK, including CocoaPods. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time. Google ships a static library, and linkers only include static library members that something references. Without `-ObjC` the class is stripped, and ICM silently does nothing while your build succeeds. + Without it, ICM silently does nothing and your build still succeeds. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time, and the linker drops it from Google's static library. - - You do not need `-lc++`. Google's SDK applies it for you. @@ -753,7 +755,7 @@ Initialize with `debug: true` and look for this line in the Xcode console: Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 ``` -`odm_available=false` with `odm_fetch_result=unavailable` means either Google's SDK is not linked or the `-ObjC` flag is missing. Check the flag first, because a missing flag produces no build warning. +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. On Swift Package Manager, check the `-ObjC` flag first, because a missing flag strips the class with no build warning. ## Enhanced Privacy Controls From f5b0c0eea606d2ab3d169eb0774ba18391403918 Mon Sep 17 00:00:00 2001 From: Shofiya <86974918+Shofiya2003@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:27:43 +0530 Subject: [PATCH 4/5] docs: document Google ICM setup across every SDK [LIN-2078] ICM shipped in the 4.1.0 native SDKs and the wrappers built on them, but only the iOS page documented it. Adds a shared feature page and the per-SDK installation steps. - features/google-icm.mdx: what ICM is, prerequisites (minimum SDK version, an iOS link ID in Google Ads, Google's ODM SDK, setConsent), platform support, what each consent signal means and why Google needs it. - React Native, Flutter, Expo, Unity: ICM sections covering the ODM SDK, the linker flag where it is not automatic, setConsent, and verification. - Android: a Google Ads consent section. ODM is iOS-only, but Android reaches Google through the App Conversion API, which reads the same signals. - Capacitor and Cordova: state plainly that ICM is not supported yet. Both pin LinkrunnerKit 4.0.x and android-sdk 4.0.1. - iOS: the Firebase path is FirebaseAnalytics 11.14.0+, note the $(inherited) case where CocoaPods will not supply -ObjC, and initialize early because ODM matches on first launch time. Version pins in the install steps were stale and are corrected to the published releases: iOS SPM 3.12.0 to 4.1.0, Android gradle 3.10.0 to 4.1.0, Unity android-sdk 3.6.0 to 4.1.0. The Unity Swift bridge also did not compile against LinkrunnerKit 4.x, which has required a non-optional paymentId since 4.0.0, and gains a setConsent bridge on both platforms. Checked against how Adjust, Branch, AppsFlyer and Kochava document the same integration: all of them have the app supply Google's SDK. The linker flag claims are verified against Google's Package.swift (linkedLibrary("c++")) and podspec (libraries 'c++'). Co-Authored-By: Claude Opus 5 (1M context) --- ad-networks/google-ads.mdx | 6 ++ docs.json | 1 + features/google-icm.mdx | 131 ++++++++++++++++++++++++++++++++++ sdk/android.mdx | 43 +++++++++++- sdk/capacitor.mdx | 4 ++ sdk/cordova.mdx | 4 ++ sdk/expo.mdx | 69 ++++++++++++++++++ sdk/flutter.mdx | 75 ++++++++++++++++++++ sdk/ios.mdx | 20 ++++-- sdk/react-native.mdx | 83 ++++++++++++++++++++++ sdk/unity.mdx | 139 ++++++++++++++++++++++++++++++++++++- 11 files changed, 566 insertions(+), 9 deletions(-) create mode 100644 features/google-icm.mdx diff --git a/ad-networks/google-ads.mdx b/ad-networks/google-ads.mdx index 85da2cc..7665067 100644 --- a/ad-networks/google-ads.mdx +++ b/ad-networks/google-ads.mdx @@ -134,6 +134,12 @@ After creating the link ID and mapping events in Linkrunner, you can import app events. + + Running App Campaigns with iOS users in the EEA, the UK, or Switzerland? Set up [Google Integrated Conversion + Measurement](/features/google-icm) so Google can attribute installs when no click identifier or advertising ID is + available. + + ## FAQ and Discrepancies For answers to common questions and an explanation of why numbers in Google Ads may not exactly match Linkrunner, see [Google Ads FAQ and Discrepancies](/ad-networks/google-ads-discrepancies). diff --git a/docs.json b/docs.json index fa2f5da..8456156 100644 --- a/docs.json +++ b/docs.json @@ -70,6 +70,7 @@ "features/uninstall-tracking", "features/retention", "features/meta-install-referrer", + "features/google-icm", "features/ios-campaign-data-limitations", "features/webhooks", "features/mcp", diff --git a/features/google-icm.mdx b/features/google-icm.mdx new file mode 100644 index 0000000..778f93a --- /dev/null +++ b/features/google-icm.mdx @@ -0,0 +1,131 @@ +--- +title: "Google Integrated Conversion Measurement" +description: "Recover Google App Campaign attribution on iOS when no click identifier or advertising ID is available" +icon: "google" +sidebarTitle: "Google ICM" +--- + +## What it is + +Google App Campaigns lose iOS installs when there is no click identifier and no advertising ID to match on. Integrated Conversion Measurement (ICM) closes that gap. + +Google's [On-Device Measurement (ODM) SDK](https://github.com/googleads/google-ads-on-device-conversion-ios-sdk) keeps the ad click context on the device and turns it into an encrypted value, `odm_info`. Linkrunner reads that value when the SDK initializes and sends it with the install, so Google can match the conversion without an identifier ever leaving the device. + +Set this up if you run Google App Campaigns and have iOS users in the European Economic Area, the United Kingdom, or Switzerland. Outside those regions ODM usually returns nothing and the field is left out. + + + ICM complements SKAdNetwork, it does not replace it. Keep your [SKAdNetwork integration](/features/skadnetwork-integration) in place. + + +## Prerequisites + +Four things have to be in place. ICM stays inactive until all four are done, and it fails quietly rather than loudly, so work through them in order. + + + + ICM shipped in the 4.1.0 line of the native SDKs. Anything older collects no `odm_info` and has no consent API. + + | Platform | Minimum version | + | --- | --- | + | Android | `io.linkrunner:android-sdk:4.1.0` | + | iOS | LinkrunnerKit 4.1.0 | + | React Native | `rn-linkrunner` 3.1.0 | + | Flutter | `linkrunner` 4.1.0 | + | Expo | `rn-linkrunner` 3.1.0 | + | Unity | LinkrunnerKit 4.1.0 and `io.linkrunner:android-sdk:4.1.0` | + + Capacitor and Cordova do not support ICM yet. Contact [support@linkrunner.io](mailto:support@linkrunner.io) if you need it there. + + + + Link IDs are per platform. An Android link ID does not cover your iOS app, and without an iOS one Google has nowhere to send the conversion, so ICM produces nothing no matter what the app does. + + In Google Ads, go to **Tools** → **Data Manager** → **Third-party app analytics**, create a link ID with provider ID `9936233049`, select the **iOS** platform and your app, then paste the link ID into [Settings → Google Ads integration](https://dashboard.linkrunner.io/settings?s=google-ads-integration) in your Linkrunner dashboard. + + Full walkthrough with screenshots: [Create Link ID for Android/iOS App](/ad-networks/google-ads#2-create-link-id-for-androidios-app). + + + + Linkrunner does not bundle it. The SDK is detected at runtime, so apps that skip ICM carry no extra dependency and no size increase. Your platform's guide below has the exact pod or package to add. + + If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and there is nothing to add. + + + + Google's App Conversion API treats the consent parameters as required whenever their value is known, so an install that arrives with no consent state is matched with less information. Call `setConsent` with the values from your Consent Management Platform (CMP) before you initialize the SDK, and again whenever the user changes their choice. + + This applies on Android too, even though there is no ODM SDK to add there. See [Consent signals](#consent-signals) below for what each parameter means. + + + +Everything else is automatic. There is no ICM API to call and attribution still comes back through `getAttributionData`. + +## Platform support + +| Platform | Collects `odm_info` | Consent API | +| --- | --- | --- | +| [Android](/sdk/android#google-ads-consent) | Not applicable | `setConsent` | +| [iOS](/sdk/ios#google-integrated-conversion-measurement-optional) | Yes | `setConsent`, TCF | +| [React Native](/sdk/react-native#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent`, TCF (iOS) | +| [Flutter](/sdk/flutter#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` | +| [Expo](/sdk/expo#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent`, TCF (iOS) | +| [Unity](/sdk/unity#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` on both platforms | +| Capacitor, Cordova | Not yet supported | No | + + + ODM is an iOS-only SDK. There is no Android equivalent and none is needed, because Android installs reach Google through the App Conversion API. That API reads the same consent signals, so `setConsent` still matters on Android. + + +## Consent signals + +`setConsent` takes three signals, each of which is `granted`, `denied`, or `unknown`. Linkrunner forwards them to Google with your installs and events. + +| Parameter | Sent to Google as | What it means | Why Google needs it | +| --- | --- | --- | --- | +| `isEEA` | `is_eea` | European regulations apply to this user (the EEA, the UK, or Switzerland) | Tells Google which rule set to apply to the conversion. It is also the population ICM exists to measure, so getting it wrong here excludes the users you set ICM up for | +| `hasConsentForDataUsage` | `ad_user_data` | The user agreed to their data being sent to Google for advertising | Governs whether Google may use the conversion at all. Without it the install can be dropped from measurement | +| `hasConsentForAdsPersonalization` | `ad_personalization` | The user agreed to their data being used to personalize ads | Governs whether the conversion may feed audiences and remarketing. Denying this while granting data usage is a normal combination | + +Read the values from your CMP rather than hardcoding them. App Tracking Transparency is not a substitute: it governs IDFA access, not Google's use of ad user data or personalization, and Linkrunner reports ATT status separately. + +A signal left `unknown` is dropped from the request rather than sent as a denial, so Linkrunner never reports a choice your user did not make. Never map `unknown` to `granted`. + + + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + +## Set it up + + + + Report consent for the App Conversion API + + + Add the ODM SDK and report consent + + + Add the pod and report consent + + + Add the pod and report consent + + + +## Troubleshooting + + + + Google's SDK is not linked into your app. Add it as described in your platform's guide. If you added it with Swift Package Manager, check that `-ObjC` is set in **Build Settings → Other Linker Flags**, because without it the linker strips Google's class and the build still succeeds. + + + ODM only returns a value for an install that came from a Google App Campaign click in an approved region. Organic installs and installs from other networks return nothing, which is expected. + + + `setConsent` was never called, or it ran after `init`. Call it before you initialize the SDK so the first payload carries the right state. + + + ODM matches on the time of first launch, which Linkrunner records when you initialize the SDK. Initialize as early as you can in the launch sequence, in `application(_:didFinishLaunchingWithOptions:)` or the equivalent for your framework. + + + +**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io) diff --git a/sdk/android.mdx b/sdk/android.mdx index 4600e66..40fcdc4 100644 --- a/sdk/android.mdx +++ b/sdk/android.mdx @@ -28,7 +28,7 @@ Add the Linkrunner SDK to your app's `build.gradle` file: ```gradle dependencies { - implementation 'io.linkrunner:android-sdk:3.10.0' + implementation 'io.linkrunner:android-sdk:4.1.0' } ``` @@ -732,6 +732,47 @@ private fun capturePurchase() { > **Note:** For more information on testing and verifying your ecommerce events, please see our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide. +## Google Ads Consent + + + **SDK Version Requirement:** Requires Android SDK version 4.1.0 or higher. + + +If you run Google App Campaigns and have users in the European Economic Area, the United Kingdom, or Switzerland, report their consent choices. Google reads these signals when it matches your installs, so an install with no consent state attached is harder to attribute. See [Google ICM](/features/google-icm) for the wider picture. + +Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: + +```kotlin +import io.linkrunner.sdk.models.ConsentStatus +import io.linkrunner.sdk.models.LinkrunnerConsent + +LinkRunner.getInstance().setConsent( + LinkrunnerConsent( + isEEA = ConsentStatus.GRANTED, + hasConsentForDataUsage = ConsentStatus.GRANTED, + hasConsentForAdsPersonalization = ConsentStatus.DENIED + ) +) +``` + +| Parameter | Meaning | +| --- | --- | +| `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | +| `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | +| `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + +Each takes `ConsentStatus.GRANTED`, `ConsentStatus.DENIED`, or `ConsentStatus.UNKNOWN`. Anything left `UNKNOWN` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + +Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. A missing advertising ID is not a consent signal and is never treated as one. + + + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + + + Google's On-Device Measurement SDK is iOS only, so there is nothing to add to your Android build. Android installs reach Google through the App Conversion API, which reads the consent signals above. + + ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: diff --git a/sdk/capacitor.mdx b/sdk/capacitor.mdx index 9df2340..22e059a 100644 --- a/sdk/capacitor.mdx +++ b/sdk/capacitor.mdx @@ -62,6 +62,10 @@ To enable SKAdNetwork postback copies to be sent to Linkrunner, add the followin For complete SKAdNetwork integration details, see the [SKAdNetwork Integration Guide](/features/skadnetwork-integration). + + **Google Integrated Conversion Measurement (ICM)** is not available in the Capacitor plugin yet. `capacitor-linkrunner` 2.1.1 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns with users in the EEA, the UK, or Switzerland, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. + + ### Step 5: Android Backup Configuration For Android apps, the SDK provides backup rules to exclude Shared Preferences data from backup. This prevents the retention of the Linkrunner install ID during reinstallation, ensuring accurate detection of new installs and re-installs. diff --git a/sdk/cordova.mdx b/sdk/cordova.mdx index dd08335..f7e01e5 100644 --- a/sdk/cordova.mdx +++ b/sdk/cordova.mdx @@ -83,6 +83,10 @@ For complete SKAdNetwork integration details, see the [SKAdNetwork Integration G For detailed Android backup configuration instructions, refer to the [Android SDK Backup Configuration](/sdk/android#backup-configuration). + + **Google Integrated Conversion Measurement (ICM)** is not available in the Cordova plugin yet. `cordova-linkrunner` 1.2.0 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns with users in the EEA, the UK, or Switzerland, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. + + ## Initialization (Required) To initialize the Linkrunner SDK, add this code inside the `deviceready` event. No import or require is needed — `linkrunner` is globally available after plugin installation. diff --git a/sdk/expo.mdx b/sdk/expo.mdx index 1995021..6c093de 100644 --- a/sdk/expo.mdx +++ b/sdk/expo.mdx @@ -85,6 +85,75 @@ For Android apps, the SDK provides backup rules to exclude Shared Preferences da For detailed backup configuration instructions, please refer to the [Android SDK Backup Configuration](/sdk/android/installation#backup-configuration). +## Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `rn-linkrunner` 3.1.0 or later. + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + `rn-linkrunner` does not bundle this SDK, so apps that skip ICM carry none of its weight. Expo generates the `Podfile`, so add the pod through `expo-build-properties`: + + ```bash + npx expo install expo-build-properties + ``` + + ```json + { + "expo": { + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "extraPods": [{ "name": "GoogleAdsOnDeviceConversion" }] + } + } + ] + ] + } + } + ``` + + If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. + + + If you manage `ios/` yourself instead of generating it, add `pod 'GoogleAdsOnDeviceConversion'` to `ios/Podfile` and run `pod install`. + + + + + ```bash + npx expo prebuild --clean + ``` + + ICM needs native code, so it does not work in Expo Go. Build a development client or run an EAS build. + + + + Pass the values from your Consent Management Platform (CMP) before you call `init`: + + ```javascript + import linkrunner from "rn-linkrunner"; + + linkrunner.setConsent({ + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied", + }); + + await linkrunner.init("YOUR_PROJECT_TOKEN"); + ``` + + See [Google Integrated Conversion Measurement](/sdk/react-native#google-integrated-conversion-measurement-optional) in the React Native guide for what each signal means, the TCF Consent Management Platform option, and how to verify the setup. + + + ## SDK Usage diff --git a/sdk/flutter.mdx b/sdk/flutter.mdx index 2bf97e1..58ddea4 100644 --- a/sdk/flutter.mdx +++ b/sdk/flutter.mdx @@ -728,6 +728,81 @@ Future trackPurchaseEvent() async { or int), not as a string. +## Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `linkrunner` 4.1.0 or later. + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + The plugin does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your `ios/Podfile`: + + ```ruby + pod 'GoogleAdsOnDeviceConversion' + ``` + + Then install the pods: + + ```bash + cd ios && pod install + ``` + + If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. + + + + Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: + + ```dart + import 'package:linkrunner/linkrunner.dart'; + import 'package:linkrunner/models/lr_consent.dart'; + + await LinkRunner().setConsent( + LRConsent( + isEEA: ConsentStatus.GRANTED, + hasConsentForDataUsage: ConsentStatus.GRANTED, + hasConsentForAdsPersonalization: ConsentStatus.DENIED, + ), + ); + + await LinkRunner().init('YOUR_PROJECT_TOKEN'); + ``` + + Each signal takes `ConsentStatus.GRANTED`, `ConsentStatus.DENIED`, or `ConsentStatus.UNKNOWN`. Anything omitted or left `UNKNOWN` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + + | Parameter | Meaning | + | --- | --- | + | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | + | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | + | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. + + `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. + + + +That is the whole integration. The native SDK fetches the value at initialization and there is no other Dart API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + +### Verifying your setup + +Initialize with `debug` set to `true` and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. + ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: diff --git a/sdk/ios.mdx b/sdk/ios.mdx index 6d74f77..e73d093 100644 --- a/sdk/ios.mdx +++ b/sdk/ios.mdx @@ -39,7 +39,7 @@ Alternatively, you can add the package dependency to your `Package.swift` file: ```swift dependencies: [ - .package(url: "https://github.com/linkrunner-labs/linkrunner-ios.git", from: "3.12.0") + .package(url: "https://github.com/linkrunner-labs/linkrunner-ios.git", from: "4.1.0") ] ``` @@ -659,15 +659,19 @@ func capturePurchase() async { ## Google Integrated Conversion Measurement (Optional) -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires LinkrunnerKit 4.1.0 or later. + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + Linkrunner does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your app yourself. - If you already use the Firebase iOS SDK 11.14.0 or later, you have it and can move to the next step. + If you already use the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can move to the next step. @@ -683,7 +687,7 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth - Using CocoaPods? Skip this step. CocoaPods adds the required flags for you. + Using CocoaPods? Skip this step. CocoaPods adds `-ObjC` and `-lc++` for you when it links Google's static framework. The one exception is an app target whose **Other Linker Flags** no longer contain `$(inherited)`, in which case add `-ObjC` yourself. If you added Google's SDK with Swift Package Manager, add this to **Build Settings** → **Other Linker Flags** on your app target: @@ -704,6 +708,10 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth That is the whole integration. Linkrunner detects the SDK at runtime and there is no API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + ODM matches on the time of first launch, which Linkrunner records when you call `initialize`. Call it as early in the launch sequence as you can, ideally in `application(_:didFinishLaunchingWithOptions:)`. + + ICM complements SKAdNetwork, it does not replace it. Keep your existing [SKAdNetwork integration](/features/skadnetwork-integration) in place. @@ -732,6 +740,8 @@ await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") Each takes `.granted`, `.denied`, or `.unknown`. Anything left `.unknown` is omitted rather than reported as a denial, so Linkrunner never reports a choice your user did not make. +Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. App Tracking Transparency is not a substitute, because it governs IDFA access only and is reported separately. + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after your user has withdrawn it. diff --git a/sdk/react-native.mdx b/sdk/react-native.mdx index e9720f1..6de0363 100644 --- a/sdk/react-native.mdx +++ b/sdk/react-native.mdx @@ -560,6 +560,89 @@ const trackPurchaseEvent = async () => { string. +## Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `rn-linkrunner` 3.1.0 or later. + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + `rn-linkrunner` does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your `ios/Podfile`: + + ```ruby + pod 'GoogleAdsOnDeviceConversion' + ``` + + Then install the pods: + + ```bash + cd ios && pod install + ``` + + If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Build Settings to change. + + + + Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: + + ```javascript + import linkrunner from "rn-linkrunner"; + + linkrunner.setConsent({ + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied", + }); + + await linkrunner.init("YOUR_PROJECT_TOKEN"); + ``` + + Each signal takes `"granted"`, `"denied"`, or `"unknown"`. Anything omitted or left `"unknown"` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + + | Parameter | Meaning | + | --- | --- | + | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | + | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | + | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. + + `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. + + + +That is the whole integration. The native SDK fetches the value at initialization and there is no other API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + +### Using a TCF Consent Management Platform + +If your CMP supports IAB TCF v2.2 or v2.3, Linkrunner can read consent directly instead: + +```javascript +linkrunner.enableTCFConsentCollection(true); +await linkrunner.init("YOUR_PROJECT_TOKEN"); +``` + +Anything you set explicitly with `setConsent` still takes priority, per signal. This setting is iOS only and is not stored between launches, so call it on every launch before `init`. + +### Verifying your setup + +Initialize with debug mode on and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. + ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: diff --git a/sdk/unity.mdx b/sdk/unity.mdx index bcb483a..ec44b8d 100644 --- a/sdk/unity.mdx +++ b/sdk/unity.mdx @@ -59,7 +59,7 @@ In your Unity project, create or edit the file `Assets/Plugins/Android/mainTempl ```gradle dependencies { - implementation 'io.linkrunner:android-sdk:3.6.0' + implementation 'io.linkrunner:android-sdk:4.1.0' } ``` @@ -111,8 +111,11 @@ import android.util.Log; import com.unity3d.player.UnityPlayer; +import io.linkrunner.sdk.LinkRunner; import io.linkrunner.sdk.LinkrunnerJava; import io.linkrunner.sdk.LinkRunnerCallback; +import io.linkrunner.sdk.models.ConsentStatus; +import io.linkrunner.sdk.models.LinkrunnerConsent; import io.linkrunner.sdk.models.request.UserDataRequest; import io.linkrunner.sdk.models.request.CapturePaymentRequest; import io.linkrunner.sdk.models.request.RemovePaymentRequest; @@ -390,6 +393,25 @@ public class LinkrunnerBridge { LinkrunnerJava.getInstance().setDisableAaidCollection(disabled); } + private static ConsentStatus toConsentStatus(String value) { + if (value == null) return ConsentStatus.UNKNOWN; + try { + return ConsentStatus.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return ConsentStatus.UNKNOWN; + } + } + + public static void setConsent(String isEEA, String dataUsage, String adsPersonalization) { + LinkRunner.getInstance().setConsent( + new LinkrunnerConsent( + toConsentStatus(isEEA), + toConsentStatus(dataUsage), + toConsentStatus(adsPersonalization) + ) + ); + } + public static void handleDeeplink(String deeplinkUrl) { if (deeplinkUrl == null || deeplinkUrl.isEmpty()) { Log.d(TAG, "handleDeeplink called with null or empty URL, ignoring"); @@ -442,7 +464,7 @@ After building your Unity project for iOS and opening the generated Xcode projec ``` https://github.com/linkrunner-labs/linkrunner-ios.git ``` -3. Select the latest version (3.8.0+) +3. Select the latest version (4.1.0+) 4. Click **Add Package** and choose the **LinkrunnerKitStatic** library @@ -667,7 +689,8 @@ func linkrunnerCapturePayment(_ userId: UnsafePointer, await LinkrunnerSDK.shared.capturePayment( amount: amount, userId: uid, - paymentId: pid, + // LinkrunnerKit 4.x requires a payment id. An empty value is rejected by the SDK. + paymentId: pid ?? "", type: paymentType, status: paymentStatus, eventData: eventData.isEmpty ? nil : eventData @@ -710,6 +733,24 @@ func linkrunnerEnablePIIHashing(_ enabled: Bool) { LinkrunnerSDK.shared.enablePIIHashing(enabled) } +@_cdecl("_LinkrunnerSetConsent") +func linkrunnerSetConsent(_ isEEA: UnsafePointer?, + _ dataUsage: UnsafePointer?, + _ adsPersonalization: UnsafePointer?) { + func status(_ value: UnsafePointer?) -> ConsentStatus { + guard let raw = toOptionalString(value) else { return .unknown } + return ConsentStatus(rawValue: raw.lowercased()) ?? .unknown + } + + LinkrunnerSDK.shared.setConsent( + LinkrunnerConsent( + isEEA: status(isEEA), + hasConsentForDataUsage: status(dataUsage), + hasConsentForAdsPersonalization: status(adsPersonalization) + ) + ) +} + @_cdecl("_LinkrunnerHandleDeeplink") func linkrunnerHandleDeeplink(_ deeplinkUrl: UnsafePointer?) { let url = toOptionalString(deeplinkUrl) @@ -744,6 +785,76 @@ func linkrunnerHandleDeeplink(_ deeplinkUrl: UnsafePointer?) { Alternatively, you can automate this with a Unity `PostProcessBuild` script — see the [Automation Tips](#automating-ios-dependency-with-a-post-build-script) section. +## Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires LinkrunnerKit 4.1.0 or later and `io.linkrunner:android-sdk:4.1.0` or later. + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + Linkrunner does not bundle this SDK, so apps that skip ICM carry none of its weight. In the generated Xcode project, select **File** → **Add Package Dependencies...**, enter: + + ``` + https://github.com/googleads/google-ads-on-device-conversion-ios-sdk + ``` + + Select the `GoogleAdsOnDeviceConversion` product and add it to the same target you added **LinkrunnerKitStatic** to (**UnityFramework**). + + Like the LinkrunnerKit dependency, this has to be re-added each time Unity regenerates the Xcode project. Handle it in the same post-build script. + + + + In **Build Settings** → **Other Linker Flags** on the target that links Google's SDK (**UnityFramework**), add: + + ``` + -ObjC + ``` + + + Without it, ICM silently does nothing and your build still succeeds. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time, and the linker drops it from Google's static library. `-lc++` is applied for you by Google's `Package.swift`. + + + + + Call `SetConsent` with the values from your Consent Management Platform (CMP) before `Initialize`, and again whenever the user changes their choice: + + ```csharp + LinkrunnerSDK.SetConsent( + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied" + ); + + LinkrunnerSDK.Initialize("YOUR_PROJECT_TOKEN"); + ``` + + Each signal takes `"granted"`, `"denied"`, or `"unknown"`. Anything left `"unknown"` is dropped from the payload rather than reported as a denial. `SetConsent` works on both iOS and Android, using the bridge methods added in the [Java bridge](#step-3-create-the-java-bridge) and [Swift bridge](#step-3-create-the-swift-bridge) above. + + | Parameter | Meaning | + | --- | --- | + | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | + | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | + | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. + + + +### Verifying your setup + +Initialize with debug mode on and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check the `-ObjC` flag first, because a missing flag strips the class with no build warning. + --- ## C# Wrapper @@ -798,6 +909,9 @@ public class LinkrunnerSDK : MonoBehaviour [DllImport("__Internal")] private static extern void _LinkrunnerEnablePIIHashing(bool enabled); [DllImport("__Internal")] + private static extern void _LinkrunnerSetConsent(string isEEA, string dataUsage, + string adsPersonalization); + [DllImport("__Internal")] private static extern void _LinkrunnerHandleDeeplink(string deeplinkUrl); #endif @@ -974,6 +1088,24 @@ public class LinkrunnerSDK : MonoBehaviour #endif } + /// + /// Report Google Ads consent. Each value is "granted", "denied", or "unknown". + /// Call before Initialize(), and again whenever your CMP state changes. + /// + public static void SetConsent(string isEEA, string hasConsentForDataUsage, + string hasConsentForAdsPersonalization) + { +#if UNITY_ANDROID && !UNITY_EDITOR + Bridge.CallStatic("setConsent", isEEA ?? "unknown", + hasConsentForDataUsage ?? "unknown", hasConsentForAdsPersonalization ?? "unknown"); +#elif UNITY_IOS && !UNITY_EDITOR + _LinkrunnerSetConsent(isEEA, hasConsentForDataUsage, hasConsentForAdsPersonalization); +#else + Debug.Log("[Linkrunner] SetConsent: " + isEEA + ", " + hasConsentForDataUsage + ", " + + hasConsentForAdsPersonalization); +#endif + } + /// /// Handle a deeplink for re-engagement attribution. /// Call this when the app is opened via a deeplink (cold start or warm start). @@ -1352,6 +1484,7 @@ LinkrunnerSDK.SetDisableAaidCollection(true); | `CapturePayment` | Payment flow | When payment succeeds | | `RemovePayment` | Refund flow | When payment is reversed | | `SetPushToken` | After token refresh | When push token changes | +| `SetConsent` | Before `Initialize` | At launch and whenever your CMP state changes | | `HandleDeeplink` | Deeplink entry points | When app is opened via a deeplink | --- From 8ecbd6b5a6d80b492889e1cde5b70304de7380c8 Mon Sep 17 00:00:00 2001 From: Shofiya <86974918+Shofiya2003@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:03:20 +0530 Subject: [PATCH 5/5] docs: correct ICM geography, add Send Consent guide [LIN-2078] Google's on-device measurement is inactive in the EEA, the UK, and Switzerland, which is the opposite of what the ICM docs claimed. Correct the audience across every SDK guide and the feature page: ICM recovers iOS installs that have no click identifier and no IDFA, and the EEA framing belongs to the Android side, where Google applies the improvement automatically. Also: - Move the ICM section under Installation in each SDK guide - Point link ID setup at Integrations > Google Ads instead of Settings - Show the Podfile target block for the ODM pod, and lead the step with the Firebase skip note - Add features/send-consent.mdx covering the DMA consent signals, what to report outside the EEA, and how to collect it - Drop CMP phrasing and the undocumented TCF path for now Co-Authored-By: Claude Opus 5 (1M context) --- ad-networks/google-ads-discrepancies.mdx | 6 +- ad-networks/google-ads.mdx | 14 +- docs.json | 1 + features/google-icm.mdx | 65 +++---- features/send-consent.mdx | 179 ++++++++++++++++++ sdk/android.mdx | 11 +- sdk/capacitor.mdx | 2 +- sdk/cordova.mdx | 2 +- sdk/expo.mdx | 20 +- sdk/flutter.mdx | 163 +++++++++-------- sdk/ios.mdx | 224 ++++++++++++----------- sdk/react-native.mdx | 168 ++++++++--------- sdk/unity.mdx | 20 +- 13 files changed, 538 insertions(+), 337 deletions(-) create mode 100644 features/send-consent.mdx diff --git a/ad-networks/google-ads-discrepancies.mdx b/ad-networks/google-ads-discrepancies.mdx index 9615d45..3cbfbee 100644 --- a/ad-networks/google-ads-discrepancies.mdx +++ b/ad-networks/google-ads-discrepancies.mdx @@ -21,7 +21,7 @@ Walk through this checklist. -Open [Settings → Google Ads integration](https://dashboard.linkrunner.io/settings?s=google-ads-integration) and verify the Link ID is saved for the correct platform (Android or iOS). +Open [Integrations → Google Ads](https://dashboard.linkrunner.io/dashboard/integrations/google-ads?p_id=89), open the three-dot menu on the Google Ads row, and verify the link ID is saved for the correct platform (Android or iOS). On the Google Ads [Conversions summary](https://ads.google.com/aw/conversions) page, confirm the `first_open` conversion event is imported. If it was previously deleted, follow [Re-enabling Previously Deleted Events](/ad-networks/google-ads#re-enabling-previously-deleted-events). Full setup: [Importing Events for Conversion Actions Setup](/ad-networks/google-ads#4-importing-events-for-conversion-actions-setup). @@ -80,7 +80,7 @@ Google is a Self-Reporting Network (SRN), so Google Ads reports its own attribut - **Android:** Requires GAID collection. The Linkrunner SDK captures GAID automatically when the user has not opted out. - **iOS:** Measurement happens via **SKAdNetwork (SKAN)**. Google Ads sends SKAN postbacks to Linkrunner, and installs with `did_win=TRUE` are counted. Deterministic SDK attribution only works when the user grants ATT and the IDFA is available. See [SKAdNetwork integration](/features/skadnetwork-integration) and [iOS Campaign Data Limitations](/features/ios-campaign-data-limitations). -- **iOS in the EEA, UK, and Switzerland:** [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) can attribute installs in these regions even when ATT is denied and no IDFA is available. It complements SKAN rather than replacing it, and needs setup in your iOS app. +- **iOS without ATT consent:** [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) can attribute installs even when ATT is denied and no IDFA is available. It complements SKAN rather than replacing it, and needs setup in your iOS app. Google keeps it inactive for users in the EEA, the UK, and Switzerland. See the [Google Ads integration guide](/ad-networks/google-ads) for full setup on each platform. @@ -99,7 +99,7 @@ Google Ads dates conversions to the **ad click**. Linkrunner dates them to the * As an SRN, Google attributes every install that follows any Google Ads engagement within its window. Linkrunner uses **last-click attribution**, deduplicates across networks, and counts one install per user. -On iOS, Linkrunner counts only SKAN postbacks with `did_win=TRUE`. Google Ads treats all SKAN events the same, so its dashboard typically reports more iOS installs. [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) narrows this gap in the EEA, UK, and Switzerland. +On iOS, Linkrunner counts only SKAN postbacks with `did_win=TRUE`. Google Ads treats all SKAN events the same, so its dashboard typically reports more iOS installs. [Integrated Conversion Measurement](/sdk/ios#google-integrated-conversion-measurement-optional) narrows this gap for users who declined ATT. diff --git a/ad-networks/google-ads.mdx b/ad-networks/google-ads.mdx index 7665067..ae9febc 100644 --- a/ad-networks/google-ads.mdx +++ b/ad-networks/google-ads.mdx @@ -63,10 +63,10 @@ To connect your mobile app with Google Ads, you'll need to create a link ID that ### Step 3: Configure in Linkrunner Dashboard 1. Copy the received link ID from Google Ads -2. Navigate to your Linkrunner Dashboard -3. Go to [Settings → Google Ads integration](https://dashboard.linkrunner.io/settings?s=google-ads-integration) -4. Paste the link ID in the designated field -5. Save your configuration +2. Go to [Integrations → Google Ads](https://dashboard.linkrunner.io/dashboard/integrations/google-ads?p_id=89) in your Linkrunner dashboard +3. Open the three-dot menu on the Google Ads row and choose the link IDs option +4. Paste the link ID into **Android Link ID** or **iOS Link ID**, matching the platform you selected in Google Ads +5. Click **Save Link IDs** ![Google Ads Dashboard - Link ID Created](/images/google_ads/google-ads-dashboard-3.webp) @@ -135,9 +135,9 @@ After creating the link ID and mapping events in Linkrunner, you can import app - Running App Campaigns with iOS users in the EEA, the UK, or Switzerland? Set up [Google Integrated Conversion - Measurement](/features/google-icm) so Google can attribute installs when no click identifier or advertising ID is - available. + Running App Campaigns for an iOS app? Set up [Google Integrated Conversion + Measurement](/features/google-icm) so Google can attribute installs from users who declined App Tracking + Transparency. ## FAQ and Discrepancies diff --git a/docs.json b/docs.json index 8456156..09e17ce 100644 --- a/docs.json +++ b/docs.json @@ -71,6 +71,7 @@ "features/retention", "features/meta-install-referrer", "features/google-icm", + "features/send-consent", "features/ios-campaign-data-limitations", "features/webhooks", "features/mcp", diff --git a/features/google-icm.mdx b/features/google-icm.mdx index 778f93a..1979c45 100644 --- a/features/google-icm.mdx +++ b/features/google-icm.mdx @@ -7,11 +7,17 @@ sidebarTitle: "Google ICM" ## What it is -Google App Campaigns lose iOS installs when there is no click identifier and no advertising ID to match on. Integrated Conversion Measurement (ICM) closes that gap. +Google App Campaigns lose iOS installs when there is no click identifier and no IDFA to match on, which is the normal state once a user declines App Tracking Transparency. Integrated Conversion Measurement (ICM) closes that gap. Google's [On-Device Measurement (ODM) SDK](https://github.com/googleads/google-ads-on-device-conversion-ios-sdk) keeps the ad click context on the device and turns it into an encrypted value, `odm_info`. Linkrunner reads that value when the SDK initializes and sends it with the install, so Google can match the conversion without an identifier ever leaving the device. -Set this up if you run Google App Campaigns and have iOS users in the European Economic Area, the United Kingdom, or Switzerland. Outside those regions ODM usually returns nothing and the field is left out. +Set this up if you run Google App Campaigns for your iOS app. Google's SDK requires iOS 12 or later, and Google reports the improved coverage for users on iOS 14 and later. + + + Google keeps ODM inactive for users located in the European Economic Area, the United Kingdom, and Switzerland. For those users the SDK returns nothing and the field is left out, so ICM recovers no installs there. + + +On Android there is nothing to install. Google applies the same ICM improvements automatically through the App Conversion API, mainly for EEA users and users who opt out of device-level permissions. ICM complements SKAdNetwork, it does not replace it. Keep your [SKAdNetwork integration](/features/skadnetwork-integration) in place. @@ -33,28 +39,30 @@ Four things have to be in place. ICM stays inactive until all four are done, and | Flutter | `linkrunner` 4.1.0 | | Expo | `rn-linkrunner` 3.1.0 | | Unity | LinkrunnerKit 4.1.0 and `io.linkrunner:android-sdk:4.1.0` | - - Capacitor and Cordova do not support ICM yet. Contact [support@linkrunner.io](mailto:support@linkrunner.io) if you need it there. Link IDs are per platform. An Android link ID does not cover your iOS app, and without an iOS one Google has nowhere to send the conversion, so ICM produces nothing no matter what the app does. - In Google Ads, go to **Tools** → **Data Manager** → **Third-party app analytics**, create a link ID with provider ID `9936233049`, select the **iOS** platform and your app, then paste the link ID into [Settings → Google Ads integration](https://dashboard.linkrunner.io/settings?s=google-ads-integration) in your Linkrunner dashboard. + In Google Ads, go to **Tools** → **Data Manager** → **Third-party app analytics**, create a link ID with provider ID `9936233049`, and select the **iOS** platform and your app. + + Then in Linkrunner, go to [Integrations → Google Ads](https://dashboard.linkrunner.io/dashboard/integrations/google-ads?p_id=89), open the three-dot menu on the Google Ads row, and paste it into **iOS Link ID**. Click **Save Link IDs**. Full walkthrough with screenshots: [Create Link ID for Android/iOS App](/ad-networks/google-ads#2-create-link-id-for-androidios-app). - Linkrunner does not bundle it. The SDK is detected at runtime, so apps that skip ICM carry no extra dependency and no size increase. Your platform's guide below has the exact pod or package to add. + Linkrunner does not bundle it. The SDK is detected at runtime, so apps that skip ICM carry no extra dependency and no size increase. + + Your SDK's installation section has the exact pod or package to add, along with any linker flags: [iOS](/sdk/ios#google-integrated-conversion-measurement-optional), [React Native](/sdk/react-native#google-integrated-conversion-measurement-optional), [Flutter](/sdk/flutter#google-integrated-conversion-measurement-optional), [Expo](/sdk/expo#google-integrated-conversion-measurement-optional), [Unity](/sdk/unity#google-integrated-conversion-measurement-optional). If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and there is nothing to add. - Google's App Conversion API treats the consent parameters as required whenever their value is known, so an install that arrives with no consent state is matched with less information. Call `setConsent` with the values from your Consent Management Platform (CMP) before you initialize the SDK, and again whenever the user changes their choice. + Google's App Conversion API treats the consent parameters as required whenever their value is known, so an install that arrives with no consent state is matched with less information. Call `setConsent` with your consent values before you initialize the SDK, and again whenever the user changes their choice. - This applies on Android too, even though there is no ODM SDK to add there. See [Consent signals](#consent-signals) below for what each parameter means. + This applies on Android too, even though there is no ODM SDK to add there. See [Consent signals](#consent-signals) below for what each parameter means, and [Send Consent](/features/send-consent) for what to report and how to collect it. @@ -65,10 +73,10 @@ Everything else is automatic. There is no ICM API to call and attribution still | Platform | Collects `odm_info` | Consent API | | --- | --- | --- | | [Android](/sdk/android#google-ads-consent) | Not applicable | `setConsent` | -| [iOS](/sdk/ios#google-integrated-conversion-measurement-optional) | Yes | `setConsent`, TCF | -| [React Native](/sdk/react-native#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent`, TCF (iOS) | +| [iOS](/sdk/ios#google-integrated-conversion-measurement-optional) | Yes | `setConsent` | +| [React Native](/sdk/react-native#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` | | [Flutter](/sdk/flutter#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` | -| [Expo](/sdk/expo#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent`, TCF (iOS) | +| [Expo](/sdk/expo#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` | | [Unity](/sdk/unity#google-integrated-conversion-measurement-optional) | Yes (iOS) | `setConsent` on both platforms | | Capacitor, Cordova | Not yet supported | No | @@ -80,19 +88,17 @@ Everything else is automatic. There is no ICM API to call and attribution still `setConsent` takes three signals, each of which is `granted`, `denied`, or `unknown`. Linkrunner forwards them to Google with your installs and events. -| Parameter | Sent to Google as | What it means | Why Google needs it | -| --- | --- | --- | --- | -| `isEEA` | `is_eea` | European regulations apply to this user (the EEA, the UK, or Switzerland) | Tells Google which rule set to apply to the conversion. It is also the population ICM exists to measure, so getting it wrong here excludes the users you set ICM up for | -| `hasConsentForDataUsage` | `ad_user_data` | The user agreed to their data being sent to Google for advertising | Governs whether Google may use the conversion at all. Without it the install can be dropped from measurement | -| `hasConsentForAdsPersonalization` | `ad_personalization` | The user agreed to their data being used to personalize ads | Governs whether the conversion may feed audiences and remarketing. Denying this while granting data usage is a normal combination | - -Read the values from your CMP rather than hardcoding them. App Tracking Transparency is not a substitute: it governs IDFA access, not Google's use of ad user data or personalization, and Linkrunner reports ATT status separately. +| Parameter | Sent to Google as | What it means | +| --- | --- | --- | +| `isEEA` | `eea` | European regulations apply to this user (the EEA, the UK, or Switzerland) | +| `hasConsentForDataUsage` | `ad_user_data` | The user agreed to their data being sent to Google for advertising | +| `hasConsentForAdsPersonalization` | `ad_personalization` | The user agreed to their data being used to personalize ads | A signal left `unknown` is dropped from the request rather than sent as a denial, so Linkrunner never reports a choice your user did not make. Never map `unknown` to `granted`. - - Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. - +App Tracking Transparency is not a substitute: it governs IDFA access, not Google's use of ad user data or personalization, and Linkrunner reports ATT status separately. + +See [Send Consent](/features/send-consent) for what to report for users outside the EEA, and how to collect consent manually or from a Consent Management Platform. ## Set it up @@ -111,21 +117,4 @@ A signal left `unknown` is dropped from the request rather than sent as a denial -## Troubleshooting - - - - Google's SDK is not linked into your app. Add it as described in your platform's guide. If you added it with Swift Package Manager, check that `-ObjC` is set in **Build Settings → Other Linker Flags**, because without it the linker strips Google's class and the build still succeeds. - - - ODM only returns a value for an install that came from a Google App Campaign click in an approved region. Organic installs and installs from other networks return nothing, which is expected. - - - `setConsent` was never called, or it ran after `init`. Call it before you initialize the SDK so the first payload carries the right state. - - - ODM matches on the time of first launch, which Linkrunner records when you initialize the SDK. Initialize as early as you can in the launch sequence, in `application(_:didFinishLaunchingWithOptions:)` or the equivalent for your framework. - - - **Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io) diff --git a/features/send-consent.mdx b/features/send-consent.mdx new file mode 100644 index 0000000..8381ab0 --- /dev/null +++ b/features/send-consent.mdx @@ -0,0 +1,179 @@ +--- +title: "Send Consent" +description: "Report DMA consent signals with your installs and events" +icon: "user-shield" +sidebarTitle: "Send Consent" +--- + +## What this consent is for + +Google's [EU User Consent Policy](https://www.google.com/about/company/user-consent-policy/) and the EU **Digital Markets Act (DMA)** require you to tell Google what a user agreed to before Google may use their conversion. Enforcement began on 6 March 2024. + +It applies to end users located in the **European Economic Area** (the 27 EU states plus Iceland, Liechtenstein, and Norway), the **United Kingdom**, and **Switzerland**. It follows the user's location, not where your company is based. + +Linkrunner forwards these values with every install and event it sends to Google. Report nothing for an in-scope user and Google has less to match on, so conversions can be dropped and campaign performance suffers. + +**By default Linkrunner sends nothing.** Every signal starts as `unknown` until you set it, and unknown signals are left out of the request entirely. Linkrunner never fills in a default on your behalf, in either direction, so a user's consent state is only ever what your app reported. + + + Other privacy laws (India's DPDP Act, Brazil's LGPD, and so on) are separate frameworks with no Google conversion parameter. For a user outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset. + + +## The three signals + +| SDK field | Sent to Google as | Denied (`0`) | Granted (`1`) | +| --- | --- | --- | --- | +| `isEEA` | `eea` | European regulations do not apply to this user | They do apply | +| `hasConsentForDataUsage` | `ad_user_data` | The user refused sending user-level data to Google | The user agreed | +| `hasConsentForAdsPersonalization` | `ad_personalization` | The user refused ads personalization | The user agreed | + +Each signal is `granted`, `denied`, or `unknown`. Google treats all three as required whenever their value is known, which cuts both ways: + +- A value you know **must** be sent. +- A value you do not know **must not** be invented. + +Linkrunner drops any signal left `unknown` instead of sending it as a denial, so a choice your user never made is never reported on their behalf. Never map `unknown` to `granted`. + + + Google also requires `lat` (limit ad tracking), but you do not set it. Linkrunner derives it from the device. A missing advertising ID is not a consent signal and is never treated as one. + + +## What to report + + + **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** The DMA does not apply to them, so `ad_user_data` and `ad_personalization` are not required. + + + + + Report `isEEA` as denied and leave the other two unset. The DMA does not apply, so `ad_user_data` and `ad_personalization` are not required for this user. Send them only if you genuinely know them. + + ```kotlin + LinkRunner.getInstance().setConsent( + LinkrunnerConsent(isEEA = ConsentStatus.DENIED) + ) + ``` + + The two omitted fields default to `UNKNOWN` and are dropped from the payload. + + + + Report all three from the answers your consent screen actually collected. + + ```kotlin + LinkRunner.getInstance().setConsent( + LinkrunnerConsent( + isEEA = ConsentStatus.GRANTED, + hasConsentForDataUsage = ConsentStatus.GRANTED, + hasConsentForAdsPersonalization = ConsentStatus.DENIED + ) + ) + ``` + + The values above are only an example of the shape of the call, not what you should send. Report whatever the user actually chose. Denying personalization while granting data usage is a normal combination, not a mistake. + + + + Leave the signal `unknown`, which is what it is before you set anything. Linkrunner omits it. + + ```kotlin + LinkRunner.getInstance().setConsent( + LinkrunnerConsent(isEEA = ConsentStatus.UNKNOWN) + ) + ``` + + This is the correct state before your consent screen has been answered. Call `setConsent` again once the user chooses. + + + + + Do not guess the region. If you cannot determine whether the user is in scope, leave `isEEA` unknown rather than assuming denied. Reporting a European user as non-European applies the wrong rule set to their conversion. + + +## Collecting consent + +A **Consent Management Platform (CMP)** shows the consent screen at first launch and stores the answers, or you can build the screen yourself. Either way, you read the answers and report them with `setConsent`. + + + + Show your consent screen at first launch, before you initialize the SDK. Ask whether they agree to their data being sent to Google for advertising, and whether they agree to it being used to personalize ads. + + + + Most CMPs detect this for you. Otherwise resolve it server side from the IP address. If you cannot resolve it, leave `isEEA` unknown. + + + + Map each answer to `granted`, `denied`, or `unknown`, then pass them in. The first payload has to carry the right state, so this must run before `init`. + + The values below are an example. Send what the user actually chose, never a hardcoded set. + + + + ```kotlin + import io.linkrunner.sdk.models.ConsentStatus + import io.linkrunner.sdk.models.LinkrunnerConsent + + LinkRunner.getInstance().setConsent( + LinkrunnerConsent( + isEEA = ConsentStatus.GRANTED, + hasConsentForDataUsage = ConsentStatus.GRANTED, + hasConsentForAdsPersonalization = ConsentStatus.DENIED + ) + ) + ``` + + + ```swift + LinkrunnerSDK.shared.setConsent( + LinkrunnerConsent( + isEEA: .granted, + hasConsentForDataUsage: .granted, + hasConsentForAdsPersonalization: .denied + ) + ) + ``` + + + ```javascript + linkrunner.setConsent({ + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied", + }); + ``` + + + ```dart + await LinkRunner().setConsent( + LRConsent( + isEEA: ConsentStatus.GRANTED, + hasConsentForDataUsage: ConsentStatus.GRANTED, + hasConsentForAdsPersonalization: ConsentStatus.DENIED, + ), + ); + ``` + + + ```javascript + linkrunner.setConsent({ + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied", + }); + ``` + + + ```csharp + LinkrunnerSDK.SetConsent("granted", "granted", "denied"); + ``` + + + + + + Consent is stored between launches, so the last value you set keeps being sent. If the user reopens your privacy settings and withdraws consent, call `setConsent` again or you will keep reporting the old answer. + + + +**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io) diff --git a/sdk/android.mdx b/sdk/android.mdx index 40fcdc4..031591b 100644 --- a/sdk/android.mdx +++ b/sdk/android.mdx @@ -738,9 +738,9 @@ private fun capturePurchase() { **SDK Version Requirement:** Requires Android SDK version 4.1.0 or higher. -If you run Google App Campaigns and have users in the European Economic Area, the United Kingdom, or Switzerland, report their consent choices. Google reads these signals when it matches your installs, so an install with no consent state attached is harder to attribute. See [Google ICM](/features/google-icm) for the wider picture. +If you run Google App Campaigns, report your users' consent choices. Google's App Conversion API treats these values as required whenever they are known, and reads them when it matches your installs, so an install with no consent state attached is harder to attribute. This matters most for users in the European Economic Area, the United Kingdom, and Switzerland. -Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: +Set the values with `setConsent` before you call `init`, and again whenever the user changes their choice: ```kotlin import io.linkrunner.sdk.models.ConsentStatus @@ -763,14 +763,14 @@ LinkRunner.getInstance().setConsent( Each takes `ConsentStatus.GRANTED`, `ConsentStatus.DENIED`, or `ConsentStatus.UNKNOWN`. Anything left `UNKNOWN` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. -Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. A missing advertising ID is not a consent signal and is never treated as one. +Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). A missing advertising ID is not a consent signal and is never treated as one. - Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + Consent is stored between launches. Call `setConsent` again whenever the user's consent state changes, otherwise the previous value keeps being sent after the user has withdrawn it. - Google's On-Device Measurement SDK is iOS only, so there is nothing to add to your Android build. Android installs reach Google through the App Conversion API, which reads the consent signals above. + There is no extra SDK to add to your Android build. Android installs reach Google through the App Conversion API, which reads the consent signals above. ## Enhanced Privacy Controls @@ -964,6 +964,7 @@ Connect Firebase Cloud Messaging (FCM) with Linkrunner | `LinkRunner.getInstance().removePayment` | Refund flow | When payment needs to be removed | | `LinkRunner.getInstance().setPushToken` | Push notification setup | When FCM token is available | | `LinkRunner.getInstance().handleDeeplink` | Deep link entry points | When app is opened via a deep link | +| `LinkRunner.getInstance().setConsent` | App initialization or consent flow | Before `init`, and again when consent changes | | `LinkRunner.getInstance().setDisableAaidCollection` | App initialization or privacy settings | When you need to disable AAID collection | | `LinkRunner.getInstance().isAaidCollectionDisabled` | Privacy settings or compliance checks | When you need to check AAID collection status | diff --git a/sdk/capacitor.mdx b/sdk/capacitor.mdx index 22e059a..e39372e 100644 --- a/sdk/capacitor.mdx +++ b/sdk/capacitor.mdx @@ -63,7 +63,7 @@ To enable SKAdNetwork postback copies to be sent to Linkrunner, add the followin For complete SKAdNetwork integration details, see the [SKAdNetwork Integration Guide](/features/skadnetwork-integration). - **Google Integrated Conversion Measurement (ICM)** is not available in the Capacitor plugin yet. `capacitor-linkrunner` 2.1.1 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns with users in the EEA, the UK, or Switzerland, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. + **Google Integrated Conversion Measurement (ICM)** is not available in the Capacitor plugin yet. `capacitor-linkrunner` 2.1.1 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns for your iOS app, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. ### Step 5: Android Backup Configuration diff --git a/sdk/cordova.mdx b/sdk/cordova.mdx index f7e01e5..24362e2 100644 --- a/sdk/cordova.mdx +++ b/sdk/cordova.mdx @@ -84,7 +84,7 @@ For complete SKAdNetwork integration details, see the [SKAdNetwork Integration G For detailed Android backup configuration instructions, refer to the [Android SDK Backup Configuration](/sdk/android#backup-configuration). - **Google Integrated Conversion Measurement (ICM)** is not available in the Cordova plugin yet. `cordova-linkrunner` 1.2.0 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns with users in the EEA, the UK, or Switzerland, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. + **Google Integrated Conversion Measurement (ICM)** is not available in the Cordova plugin yet. `cordova-linkrunner` 1.2.0 pins LinkrunnerKit 4.0.x and `io.linkrunner:android-sdk:4.0.1`, and ICM needs 4.1.0 on both. If you run Google App Campaigns for your iOS app, contact [support@linkrunner.io](mailto:support@linkrunner.io). See [Google ICM](/features/google-icm) for what it does. ## Initialization (Required) diff --git a/sdk/expo.mdx b/sdk/expo.mdx index 6c093de..d9b917b 100644 --- a/sdk/expo.mdx +++ b/sdk/expo.mdx @@ -85,11 +85,15 @@ For Android apps, the SDK provides backup rules to exclude Shared Preferences da For detailed backup configuration instructions, please refer to the [Android SDK Backup Configuration](/sdk/android/installation#backup-configuration). -## Google Integrated Conversion Measurement (Optional) +### Google Integrated Conversion Measurement (Optional) -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. +Integrated Conversion Measurement (ICM) recovers Google App Campaign installs on iOS that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See [Google ICM](/features/google-icm) for how it works. -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `rn-linkrunner` 3.1.0 or later. +Set this up if you run Google App Campaigns for your iOS app. Requires `rn-linkrunner` 3.1.0 or later. + + + Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users. + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). @@ -97,6 +101,10 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth + + Already using the Firebase iOS SDK 11.14.0 or later? The `FirebaseAnalytics` pod brings this SDK in for you. Skip this step. + + `rn-linkrunner` does not bundle this SDK, so apps that skip ICM carry none of its weight. Expo generates the `Podfile`, so add the pod through `expo-build-properties`: ```bash @@ -120,7 +128,7 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth } ``` - If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. + CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. If you manage `ios/` yourself instead of generating it, add `pod 'GoogleAdsOnDeviceConversion'` to `ios/Podfile` and run `pod install`. @@ -136,7 +144,7 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth - Pass the values from your Consent Management Platform (CMP) before you call `init`: + Set the values with `setConsent` before you call `init`: ```javascript import linkrunner from "rn-linkrunner"; @@ -150,7 +158,7 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth await linkrunner.init("YOUR_PROJECT_TOKEN"); ``` - See [Google Integrated Conversion Measurement](/sdk/react-native#google-integrated-conversion-measurement-optional) in the React Native guide for what each signal means, the TCF Consent Management Platform option, and how to verify the setup. + See [Google Integrated Conversion Measurement](/sdk/react-native#google-integrated-conversion-measurement-optional) in the React Native guide for what each signal means and how to verify the setup. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). diff --git a/sdk/flutter.mdx b/sdk/flutter.mdx index 58ddea4..0de8447 100644 --- a/sdk/flutter.mdx +++ b/sdk/flutter.mdx @@ -151,6 +151,93 @@ platform :ios, '15.0' For complete SKAdNetwork integration details, see the [SKAdNetwork Integration Guide](/features/skadnetwork-integration). +### Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) recovers Google App Campaign installs on iOS that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns for your iOS app. Requires `linkrunner` 4.1.0 or later. + + + Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users. + + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + + Already using the Firebase iOS SDK 11.14.0 or later? The `FirebaseAnalytics` pod brings this SDK in for you. Skip this step. + + + The plugin does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it inside the `Runner` target in `ios/Podfile`: + + ```ruby + target 'Runner' do + # ...your existing config + + pod 'GoogleAdsOnDeviceConversion' + end + ``` + + Then install the pods: + + ```bash + cd ios && pod install + ``` + + CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. + + + + Set the values with `setConsent` before you call `init`, and again whenever the user changes their choice: + + ```dart + import 'package:linkrunner/linkrunner.dart'; + import 'package:linkrunner/models/lr_consent.dart'; + + await LinkRunner().setConsent( + LRConsent( + isEEA: ConsentStatus.GRANTED, + hasConsentForDataUsage: ConsentStatus.GRANTED, + hasConsentForAdsPersonalization: ConsentStatus.DENIED, + ), + ); + + await LinkRunner().init('YOUR_PROJECT_TOKEN'); + ``` + + Each signal takes `ConsentStatus.GRANTED`, `ConsentStatus.DENIED`, or `ConsentStatus.UNKNOWN`. Anything omitted or left `UNKNOWN` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + + | Parameter | Meaning | + | --- | --- | + | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | + | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | + | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). + + `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. + + + +That is the whole integration. The native SDK fetches the value at initialization and there is no other Dart API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + Consent is stored between launches. Call `setConsent` again whenever the user's consent state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + +#### Verifying your setup + +Initialize with `debug` set to `true` and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. + ## Initialization (Required) You'll need your [project token](https://dashboard.linkrunner.io/dashboard?m=documentation) to get started! @@ -728,81 +815,6 @@ Future trackPurchaseEvent() async { or int), not as a string. -## Google Integrated Conversion Measurement (Optional) - -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. - -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `linkrunner` 4.1.0 or later. - - - ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). - - - - - The plugin does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your `ios/Podfile`: - - ```ruby - pod 'GoogleAdsOnDeviceConversion' - ``` - - Then install the pods: - - ```bash - cd ios && pod install - ``` - - If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Xcode Build Settings to change. - - - - Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: - - ```dart - import 'package:linkrunner/linkrunner.dart'; - import 'package:linkrunner/models/lr_consent.dart'; - - await LinkRunner().setConsent( - LRConsent( - isEEA: ConsentStatus.GRANTED, - hasConsentForDataUsage: ConsentStatus.GRANTED, - hasConsentForAdsPersonalization: ConsentStatus.DENIED, - ), - ); - - await LinkRunner().init('YOUR_PROJECT_TOKEN'); - ``` - - Each signal takes `ConsentStatus.GRANTED`, `ConsentStatus.DENIED`, or `ConsentStatus.UNKNOWN`. Anything omitted or left `UNKNOWN` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. - - | Parameter | Meaning | - | --- | --- | - | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | - | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | - | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | - - Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. - - `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. - - - -That is the whole integration. The native SDK fetches the value at initialization and there is no other Dart API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. - - - Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. - - -### Verifying your setup - -Initialize with `debug` set to `true` and look for this line in the Xcode console: - -``` -Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 -``` - -`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. - ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: @@ -1024,6 +1036,7 @@ Connect APNs with Linkrunner | `LinkRunner().capturePayment` | Payment processing | When user makes a payment | | `LinkRunner().removePayment` | Refund flow | When payment needs to be removed | | `LinkRunner().handleDeeplink` | Deep link entry points | When app is opened via a deep link | +| `LinkRunner().setConsent` | App initialization or consent flow | Before `init`, and again when consent changes | | `LinkRunner().setDisableAaidCollection` | App initialization or privacy settings | When you need to disable AAID collection | | `LinkRunner().isAaidCollectionDisabled` | Privacy settings or compliance checks | When you need to check AAID collection status | diff --git a/sdk/ios.mdx b/sdk/ios.mdx index e73d093..3edbf2d 100644 --- a/sdk/ios.mdx +++ b/sdk/ios.mdx @@ -96,6 +96,119 @@ For complete SKAdNetwork integration details, see the [SKAdNetwork Integration G The SDK requires network access to communicate with Linkrunner services. Make sure your app has the appropriate permissions for network access. +### Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) recovers Google App Campaign installs that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns for your iOS app. Requires LinkrunnerKit 4.1.0 or later. + + + Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users. + + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + + Already using the Firebase iOS SDK 11.14.0 or later? The `FirebaseAnalytics` pod brings this SDK in for you. Skip to the next step. + + + Linkrunner does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your app yourself. + + + + Add `https://github.com/googleads/google-ads-on-device-conversion-ios-sdk` in **File** → **Add Package Dependencies...** and select the `GoogleAdsOnDeviceConversion` product. + + + Add the pod inside your app target in the `Podfile`: + + ```ruby + target 'YourApp' do + # ...your existing pods + + pod 'GoogleAdsOnDeviceConversion' + end + ``` + + Then run `pod install`. + + + + + + + Using CocoaPods? Skip this step. CocoaPods adds `-ObjC` and `-lc++` for you when it links Google's static framework. The one exception is an app target whose **Other Linker Flags** no longer contain `$(inherited)`, in which case add `-ObjC` yourself. + + + If you added Google's SDK with Swift Package Manager, add this to **Build Settings** → **Other Linker Flags** on your app target: + + ``` + -ObjC + ``` + + + Without it, ICM silently does nothing and your build still succeeds. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time, and the linker drops it from Google's static library. + + + + + Google reads consent signals when it matches your installs. See [Consent](#consent) below. + + + +That is the whole integration. Linkrunner detects the SDK at runtime and there is no API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + ODM matches on the time of first launch, which Linkrunner records when you call `initialize`. + + + + ICM complements SKAdNetwork, it does not replace it. Keep your existing [SKAdNetwork integration](/features/skadnetwork-integration) in place. + + +#### Consent + +Google needs to know whether European regulations apply to a user and what that user agreed to. Set the values with `setConsent` before you call `initialize`, and again whenever the user changes their choice. + +```swift +LinkrunnerSDK.shared.setConsent( + LinkrunnerConsent( + isEEA: .granted, + hasConsentForDataUsage: .granted, + hasConsentForAdsPersonalization: .denied + ) +) + +await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") +``` + +| Parameter | Meaning | +| --- | --- | +| `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | +| `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | +| `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + +Each takes `.granted`, `.denied`, or `.unknown`. Anything left `.unknown` is omitted rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + +Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). App Tracking Transparency is not a substitute, because it governs IDFA access only and is reported separately. + + + Consent is stored between launches. Call `setConsent` again whenever the user's consent state changes, otherwise the previous value keeps being sent after your user has withdrawn it. + + +#### Verifying your setup + +Initialize with `debug: true` and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. On Swift Package Manager, check the `-ObjC` flag first, because a missing flag strips the class with no build warning. + ## Initialization (Required) Initialize the Linkrunner SDK in your app's startup code, typically in your `AppDelegate` or `SceneDelegate`: @@ -657,116 +770,6 @@ func capturePurchase() async { > **Note:** For more information on testing and verifying your ecommerce events, please see our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide. -## Google Integrated Conversion Measurement (Optional) - -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. - -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires LinkrunnerKit 4.1.0 or later. - - - ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). - - - - - Linkrunner does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your app yourself. - - If you already use the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can move to the next step. - - - - Add `https://github.com/googleads/google-ads-on-device-conversion-ios-sdk` in **File** → **Add Package Dependencies...** and select the `GoogleAdsOnDeviceConversion` product. - - - ```ruby - pod 'GoogleAdsOnDeviceConversion' - ``` - - - - - - - Using CocoaPods? Skip this step. CocoaPods adds `-ObjC` and `-lc++` for you when it links Google's static framework. The one exception is an app target whose **Other Linker Flags** no longer contain `$(inherited)`, in which case add `-ObjC` yourself. - - - If you added Google's SDK with Swift Package Manager, add this to **Build Settings** → **Other Linker Flags** on your app target: - - ``` - -ObjC - ``` - - - Without it, ICM silently does nothing and your build still succeeds. Linkrunner finds Google's class through the Objective-C runtime, so nothing references it at link time, and the linker drops it from Google's static library. - - - - - Google requires consent signals for users in these regions. See [Consent](#consent) below. - - - -That is the whole integration. Linkrunner detects the SDK at runtime and there is no API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. - - - ODM matches on the time of first launch, which Linkrunner records when you call `initialize`. Call it as early in the launch sequence as you can, ideally in `application(_:didFinishLaunchingWithOptions:)`. - - - - ICM complements SKAdNetwork, it does not replace it. Keep your existing [SKAdNetwork integration](/features/skadnetwork-integration) in place. - - -### Consent - -Google needs to know whether European regulations apply to a user and what that user agreed to. Pass the values from your Consent Management Platform (CMP) before you call `initialize`, and again whenever the user changes their choice. - -```swift -LinkrunnerSDK.shared.setConsent( - LinkrunnerConsent( - isEEA: .granted, - hasConsentForDataUsage: .granted, - hasConsentForAdsPersonalization: .denied - ) -) - -await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") -``` - -| Parameter | Meaning | -| --- | --- | -| `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | -| `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | -| `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | - -Each takes `.granted`, `.denied`, or `.unknown`. Anything left `.unknown` is omitted rather than reported as a denial, so Linkrunner never reports a choice your user did not make. - -Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. App Tracking Transparency is not a substitute, because it governs IDFA access only and is reported separately. - - - Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after your user has withdrawn it. - - -#### Using a TCF Consent Management Platform - -If your CMP supports IAB TCF v2.2 or v2.3, Linkrunner can read consent directly instead: - -```swift -LinkrunnerSDK.shared.enableTCFConsentCollection(true) -await LinkrunnerSDK.shared.initialize(token: "YOUR_PROJECT_TOKEN") -``` - -Anything you set explicitly with `setConsent` still takes priority, per signal. This setting is not stored between launches, so call it on every launch before `initialize`. - -### Verifying your setup - -Initialize with `debug: true` and look for this line in the Xcode console: - -``` -Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 -``` - -`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. On Swift Package Manager, check the `-ObjC` flag first, because a missing flag strips the class with no build warning. - ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: @@ -913,6 +916,7 @@ Connect APNs with Linkrunner | `LinkrunnerSDK.shared.removePayment` | Refund flow | When payment needs to be removed | | `LinkrunnerSDK.shared.setPushToken` | Push notification setup | When APNs token is available | | `LinkrunnerSDK.shared.handleDeeplink` | SceneDelegate deep link entry points | When app is opened via a deep link | +| `LinkrunnerSDK.shared.setConsent` | App initialization or consent flow | Before `initialize`, and again when consent changes | ## Complete Example diff --git a/sdk/react-native.mdx b/sdk/react-native.mdx index 6de0363..deedddc 100644 --- a/sdk/react-native.mdx +++ b/sdk/react-native.mdx @@ -76,6 +76,90 @@ If you are upgrading from an earlier version, the SDK will transparently migrate If you are using Expo, follow the above steps to install the required packages. After this, you will need to use development builds since the Linkrunner SDK relies on native libraries. Follow the [Expo Development Builds Documentation](https://docs.expo.dev/develop/development-builds/introduction/) to get started. +### Google Integrated Conversion Measurement (Optional) + +Integrated Conversion Measurement (ICM) recovers Google App Campaign installs on iOS that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See [Google ICM](/features/google-icm) for how it works. + +Set this up if you run Google App Campaigns for your iOS app. Requires `rn-linkrunner` 3.1.0 or later. + + + Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users. + + + + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). + + + + + + Already using the Firebase iOS SDK 11.14.0 or later? The `FirebaseAnalytics` pod brings this SDK in for you. Skip this step. + + + `rn-linkrunner` does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it inside your app target in `ios/Podfile`: + + ```ruby + target 'YourApp' do + # ...your existing config + + pod 'GoogleAdsOnDeviceConversion' + end + ``` + + Then install the pods: + + ```bash + cd ios && pod install + ``` + + CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Build Settings to change. + + + + Set the values with `setConsent` before you call `init`, and again whenever the user changes their choice: + + ```javascript + import linkrunner from "rn-linkrunner"; + + linkrunner.setConsent({ + isEEA: "granted", + hasConsentForDataUsage: "granted", + hasConsentForAdsPersonalization: "denied", + }); + + await linkrunner.init("YOUR_PROJECT_TOKEN"); + ``` + + Each signal takes `"granted"`, `"denied"`, or `"unknown"`. Anything omitted or left `"unknown"` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. + + | Parameter | Meaning | + | --- | --- | + | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | + | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | + | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | + + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). + + `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. + + + +That is the whole integration. The native SDK fetches the value at initialization and there is no other API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. + + + Consent is stored between launches. Call `setConsent` again whenever the user's consent state changes, otherwise the previous value keeps being sent after the user has withdrawn it. + + +#### Verifying your setup + +Initialize with debug mode on and look for this line in the Xcode console: + +``` +Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 +``` + +`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. + ## Initialization (Required) To initialize the Linkrunner SDK, add this code to your `App.tsx` component: @@ -560,89 +644,6 @@ const trackPurchaseEvent = async () => { string. -## Google Integrated Conversion Measurement (Optional) - -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. - -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires `rn-linkrunner` 3.1.0 or later. - - - ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). - - - - - `rn-linkrunner` does not bundle this SDK, so apps that skip ICM carry none of its weight. Add it to your `ios/Podfile`: - - ```ruby - pod 'GoogleAdsOnDeviceConversion' - ``` - - Then install the pods: - - ```bash - cd ios && pod install - ``` - - If your app already uses the Firebase iOS SDK 11.14.0 or later, the `FirebaseAnalytics` pod brings it in and you can skip this step. CocoaPods adds the `-ObjC` and `-lc++` linker flags for you, so there are no Build Settings to change. - - - - Pass the values from your Consent Management Platform (CMP) before you call `init`, and again whenever the user changes their choice: - - ```javascript - import linkrunner from "rn-linkrunner"; - - linkrunner.setConsent({ - isEEA: "granted", - hasConsentForDataUsage: "granted", - hasConsentForAdsPersonalization: "denied", - }); - - await linkrunner.init("YOUR_PROJECT_TOKEN"); - ``` - - Each signal takes `"granted"`, `"denied"`, or `"unknown"`. Anything omitted or left `"unknown"` is dropped from the payload rather than reported as a denial, so Linkrunner never reports a choice your user did not make. - - | Parameter | Meaning | - | --- | --- | - | `isEEA` | European regulations apply to this user (the EEA, the UK, or Switzerland) | - | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | - | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | - - Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. - - `setConsent` works on both iOS and Android. Android has no ODM SDK to add, but its installs reach Google through the App Conversion API, which reads the same signals. - - - -That is the whole integration. The native SDK fetches the value at initialization and there is no other API to call. Attribution comes back through [`getAttributionData`](#getting-attribution-data) as usual. - - - Consent is stored between launches. Call `setConsent` again whenever your CMP state changes, otherwise the previous value keeps being sent after the user has withdrawn it. - - -### Using a TCF Consent Management Platform - -If your CMP supports IAB TCF v2.2 or v2.3, Linkrunner can read consent directly instead: - -```javascript -linkrunner.enableTCFConsentCollection(true); -await linkrunner.init("YOUR_PROJECT_TOKEN"); -``` - -Anything you set explicitly with `setConsent` still takes priority, per signal. This setting is iOS only and is not stored between launches, so call it on every launch before `init`. - -### Verifying your setup - -Initialize with debug mode on and look for this line in the Xcode console: - -``` -Linkrunner: odm_available=true odm_fetch_result=success odm_fetch_latency_ms=124 -``` - -`odm_available=false` with `odm_fetch_result=unavailable` means Google's SDK is not linked. Check that `pod install` picked up `GoogleAdsOnDeviceConversion`. - ## Enhanced Privacy Controls The SDK offers options to enhance user privacy: @@ -828,6 +829,7 @@ Connect APNs with Linkrunner | `linkrunner.removePayment` | Refund flow | When payment needs to be removed | | `linkrunner.setPushToken` | Push notification setup | When FCM/APNs token is available | | `linkrunner.handleDeeplink` | Deep link entry points | When app is opened via a deep link | +| `linkrunner.setConsent` | App initialization or consent flow | Before `init`, and again when consent changes | ## Next Steps diff --git a/sdk/unity.mdx b/sdk/unity.mdx index ec44b8d..9724d4a 100644 --- a/sdk/unity.mdx +++ b/sdk/unity.mdx @@ -785,11 +785,15 @@ func linkrunnerHandleDeeplink(_ deeplinkUrl: UnsafePointer?) { Alternatively, you can automate this with a Unity `PostProcessBuild` script — see the [Automation Tips](#automating-ios-dependency-with-a-post-build-script) section. -## Google Integrated Conversion Measurement (Optional) +### Google Integrated Conversion Measurement (Optional) -Integrated Conversion Measurement (ICM) improves Google App Campaign attribution for users in the European Economic Area, the United Kingdom, and Switzerland. It lets Google attribute installs when no click identifier or advertising ID is available. See [Google ICM](/features/google-icm) for how it works. +Integrated Conversion Measurement (ICM) recovers Google App Campaign installs on iOS that Google cannot attribute because there is no click identifier and no IDFA to match on. Google's On-Device Measurement (ODM) SDK turns the click context into an encrypted signal that never leaves the device, and Linkrunner sends it with the install. See [Google ICM](/features/google-icm) for how it works. -Set this up if you run Google App Campaigns and have users in those regions. Otherwise you can skip it. Requires LinkrunnerKit 4.1.0 or later and `io.linkrunner:android-sdk:4.1.0` or later. +Set this up if you run Google App Campaigns for your iOS app. Requires LinkrunnerKit 4.1.0 or later and `io.linkrunner:android-sdk:4.1.0` or later. + + + Google keeps ODM inactive for users in the European Economic Area, the United Kingdom, and Switzerland, so ICM recovers nothing for that traffic. Elsewhere, Google reports improved coverage for iOS 14+ users. + ICM also needs an **iOS link ID** configured in your Google Ads integration. Google has nowhere to send the conversion without one. See [Prerequisites](/features/google-icm#prerequisites). @@ -821,7 +825,7 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth - Call `SetConsent` with the values from your Consent Management Platform (CMP) before `Initialize`, and again whenever the user changes their choice: + Call `SetConsent` with your consent values before `Initialize`, and again whenever the user changes their choice: ```csharp LinkrunnerSDK.SetConsent( @@ -841,11 +845,11 @@ Set this up if you run Google App Campaigns and have users in those regions. Oth | `hasConsentForDataUsage` | The user agreed to their data being sent to Google for advertising | | `hasConsentForAdsPersonalization` | The user agreed to their data being used to personalize ads | - Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Read them from your CMP rather than hardcoding them. + Google treats these as required whenever their value is known. `hasConsentForDataUsage` decides whether Google may use the conversion at all, `hasConsentForAdsPersonalization` decides whether it may feed audiences and remarketing, and `isEEA` tells Google which rules apply. Set them from your app's real consent state rather than hardcoding them. **For users outside the EEA, the UK, and Switzerland, report `isEEA` as denied and leave the other two unset.** See [Send Consent](/features/send-consent). -### Verifying your setup +#### Verifying your setup Initialize with debug mode on and look for this line in the Xcode console: @@ -1090,7 +1094,7 @@ public class LinkrunnerSDK : MonoBehaviour /// /// Report Google Ads consent. Each value is "granted", "denied", or "unknown". - /// Call before Initialize(), and again whenever your CMP state changes. + /// Call before Initialize(), and again whenever the user's consent state changes. /// public static void SetConsent(string isEEA, string hasConsentForDataUsage, string hasConsentForAdsPersonalization) @@ -1484,7 +1488,7 @@ LinkrunnerSDK.SetDisableAaidCollection(true); | `CapturePayment` | Payment flow | When payment succeeds | | `RemovePayment` | Refund flow | When payment is reversed | | `SetPushToken` | After token refresh | When push token changes | -| `SetConsent` | Before `Initialize` | At launch and whenever your CMP state changes | +| `SetConsent` | Before `Initialize` | At launch and whenever consent changes | | `HandleDeeplink` | Deeplink entry points | When app is opened via a deeplink | ---