From bb681613ad4b078ce3650e36bbb4182f1b45a7d5 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:33:25 +0300 Subject: [PATCH 01/10] Update README.md --- README.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d8b039b..d6e8674 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Add AppsFlyer device mode support to your applications via this plugin for [Anal ⚠️ **Github Issues disabled in this repository** ⚠️ -Please direct all issues, bug reports, and feature enhancements to `friends@segment.com` so they can be resolved as efficiently as possible. +Please direct Segment issues, bug reports, and feature enhancements to `friends@segment.com` so they can be resolved as efficiently as possible. +Please direct Appsflyer issues, bug reports, and feature enhancements to `support@appsflyer.com` so they can be resolved as efficiently as possible. ## Adding the dependency @@ -53,6 +54,227 @@ analytics.add(plugin: AppsFlyerDestination()) Your events will now begin to flow to AppsFlyer in device mode. +## Manual mode +We support a manual mode to seperate the initialization of the AppsFlyer SDK and the start of the SDK. In this case, the AppsFlyer SDK won't start automatically, giving the developer more freedom when to start the AppsFlyer SDK. Please note that in manual mode, the developper is required to implement the API ``startAppsflyerSDK()`` in order to start the SDK. +
If you are using CMP to collect consent data this feature is needed. See explanation [here](#dma_support). +### Example: +#### swift +```swift +struct NewAnalyticsAppsflyerIntegrationApp: App { + static var analytics: Analytics? = nil + static var appsflyerDest: AppsFlyerDestination! + init() { + self.requestTrackingAuthorization() + NewAnalyticsAppsflyerIntegrationApp.analytics = Analytics(configuration: Configuration(writeKey: "") + .flushAt(3) + .trackApplicationLifecycleEvents(true) + ) + AppsFlyerLib.shared().isDebug = true +// AppsFlyerLib.shared().waitForATTUserAuthorization(timeoutInterval: 60) + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest = AppsFlyerDestination(segDelegate: sfdelegate, segDLDelegate: sfdelegate, manualMode: true) + NewAnalyticsAppsflyerIntegrationApp.analytics?.add(plugin: NewAnalyticsAppsflyerIntegrationApp.appsflyerDest) + } +... +``` + +To start the AppsFlyer SDK, use the `startAppsflyerSDK()` API, like the following : +#### swift +```swift +// check cmp response or check manually for the User's response. +// if decided to start the Appsflyer SDK manually do it like here: +NewAnalyticsAppsflyerIntegrationApp.appsflyerDest.startAppsflyerSDK() +``` + +##
Get Conversion Data + + In order for Conversion Data to be sent to Segment, make sure you have enabled "Track Attribution Data" and specified App ID in AppsFlyer destination settings: + +![image](https://user-images.githubusercontent.com/50541317/69795158-51b86780-11d4-11ea-9ab3-be3e669e4e3b.png) + +### Swift + + In order to get Conversion Data you need to: + + 1. Create a class applies the AppsFlyerLibDelegate delgeate + 2. Pass the initialized class to the AppsflyerDestination + 3. Implement methods of the protocol in the class, passed as a delegate. See sample code below where AppDelegate is used for that: + + ```swift +struct NewAnalyticsAppsflyerIntegrationApp: App { + static var afDelegate: AFDelgate! // Add strong reference to delegate + + init() { +... + NewAnalyticsAppsflyerIntegrationApp.afDelegate = AFDelgate() + + NewAnalyticsAppsflyerIntegrationApp.analytics = Analytics(configuration: Configuration(writeKey: "") + .flushAt(3) + .trackApplicationLifecycleEvents(true) + ) + + AppsFlyerLib.shared().isDebug = true + + // Use the stored delegate + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest = AppsFlyerDestination( + segDelegate: NewAnalyticsAppsflyerIntegrationApp.afDelegate, + segDLDelegate: nil + ) + NewAnalyticsAppsflyerIntegrationApp.analytics?.add(plugin: NewAnalyticsAppsflyerIntegrationApp.appsflyerDest) + } +... +... + class AFDelgate: NSObject, AppsFlyerLibDelegate{ + func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { + print("moris testing onConversionDataSuccess") + } + + func onConversionDataFail(_ error: any Error) { + print("moris testing onConversionDataFail") + } +} + ``` + +## Unified Deep linking +### Swift +In order to use Unified Deep linking you need to: + + 1. Create a class applies the DeepLinkDelegate delgeate + 2. Pass the initialized class to the AppsflyerDestination + ```swift + let factoryWithDelegate: SEGAppsFlyerIntegrationFactory = SEGAppsFlyerIntegrationFactory.create(withLaunch: self, andDeepLinkDelegate: self) + ``` + + 3. Implement methods of the protocol in the class, passed as a delegate. See sample code below where AppDelegate is used for that: + + ```swift +struct NewAnalyticsAppsflyerIntegrationApp: App { + static var afDelegate: AFDelgate! // Add strong reference to delegate + + init() { +... + NewAnalyticsAppsflyerIntegrationApp.afDelegate = AFDelgate() + + NewAnalyticsAppsflyerIntegrationApp.analytics = Analytics(configuration: Configuration(writeKey: "") + .flushAt(3) + .trackApplicationLifecycleEvents(true) + ) + + AppsFlyerLib.shared().isDebug = true + + // Use the stored delegate + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest = AppsFlyerDestination( + segDelegate: nil, + segDLDelegate: NewAnalyticsAppsflyerIntegrationApp.afDelegate + ) + NewAnalyticsAppsflyerIntegrationApp.analytics?.add(plugin: NewAnalyticsAppsflyerIntegrationApp.appsflyerDest) + } +... +... + class AFDelgate: NSObject, DeepLinkDelegate{ + func didResolveDeepLink(_ result: DeepLinkResult) { + print("Deep Link: \(result)") + } +} + ``` + +## Send consent for DMA compliance +For a general introduction to DMA consent data, see [here](https://dev.appsflyer.com/hc/docs/send-consent-for-dma-compliance). +The SDK offers two alternative methods for gathering consent data:
+- **Through a Consent Management Platform (CMP)**: If the app uses a CMP that complies with the [Transparency and Consent Framework (TCF) v2.2 protocol](https://iabeurope.eu/tcf-supporting-resources/), the SDK can automatically retrieve the consent details.
+
OR

+- **Through a dedicated SDK API**: Developers can pass Google's required consent data directly to the SDK using a specific API designed for this purpose. +### Use CMP to collect consent data +A CMP compatible with TCF v2.2 collects DMA consent data and stores it in NSUserDefaults. To enable the SDK to access this data and include it with every event, follow these steps:
+
    +
  1. Call AppsFlyerLib.shared().enableTCFDataCollection(true) to instruct the SDK to collect the TCF data from the device. +
  2. Initialize AppsFlyerDestination using manualMode = true. This will allow us to delay the Conversion call in order to provide the SDK with the user consent. +
  3. In the applicationDidBecomeActive lifecycle method, use the CMP to decide if you need the consent dialog in the current session to acquire the consent data. If you need the consent dialog move to step 4; otherwise move to step 5. +
  4. Get confirmation from the CMP that the user has made their consent decision and the data is available in NSUserDefaults. +
  5. Call startAppsflyerSDK(). +
+ + +```swift + +static var analytics: Analytics? = nil +static var appsflyerDest: AppsFlyerDestination! +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // For AppsFLyer debug logs uncomment the line below + AppsFlyerLib.shared().isDebug = true + AppsFlyerLib.shared().waitForATTUserAuthorization(timeoutInterval: 60) + AppsFlyerLib.shared().enableTCFDataCollection(true) + NewAnalyticsAppsflyerIntegrationApp.analytics = Analytics(configuration: Configuration(writeKey: "") + .flushAt(3) + .trackApplicationLifecycleEvents(true) + ) + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest = AppsFlyerDestination(segDelegate: sfdelegate, segDLDelegate: sfdelegate, manualMode: true) + NewAnalyticsAppsflyerIntegrationApp.analytics?.add(plugin: NewAnalyticsAppsflyerIntegrationApp.appsflyerDest) + return true +} + +func applicationDidBecomeActive(_ application: UIApplication) { + if(cmpManager!.hasConsent()){ + //CMP manager already has consent ready - you can start + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest.startAppsflyerSDK() + }else{ + //CMP doesn't have consent data ready yet + //Waiting for CMP completion and data ready and then start + cmpManager?.withOnCmpButtonClickedCallback({ CmpButtonEvent in + NewAnalyticsAppsflyerIntegrationApp.appsflyerDest.startAppsflyerSDK() + }) + } + + if #available(iOS 14, *) { + ATTrackingManager.requestTrackingAuthorization { (status) in + switch status { + case .denied: + print("AuthorizationSatus is denied") + case .notDetermined: + print("AuthorizationSatus is notDetermined") + case .restricted: + print("AuthorizationSatus is restricted") + case .authorized: + print("AuthorizationSatus is authorized") + @unknown default: + fatalError("Invalid authorization status") + } + } + } +} +``` + +### Manually collect consent data +If your app does not use a CMP compatible with TCF v2.2, use the SDK API detailed below to provide the consent data directly to the SDK. +
    +
  1. Initialize AppsFlyerDestination using manual mode. This will allow us to delay the Conversion call in order to provide the SDK with the user consent. +
  2. In the applicationDidBecomeActive lifecycle method determine whether the GDPR applies or not to the user.
    + - If GDPR applies to the user, perform the following: +
      +
    1. Given that GDPR is applicable to the user, determine whether the consent data is already stored for this session. +
        +
      1. If there is no consent data stored, show the consent dialog to capture the user consent decision. +
      2. If there is consent data stored continue to the next step. +
      +
    2. To transfer the consent data to the SDK create an AppsFlyerConsent object with the following parameters:
      + - forGDPRUserWithHasConsentForDataUsage- Indicates whether the user has consented to use their data for advertising purposes. + - hasConsentForAdsPersonalization- Indicates whether the user has consented to use their data for personalized advertising. +
    3. Call AppsFlyerLib.shared().setConsentData(AppsFlyerConsent(forGDPRUserWithHasConsentForDataUsage: Bool, hasConsentForAdsPersonalization: Bool)). +
    4. Call NewAnalyticsAppsflyerIntegrationApp.appsflyerDest.startAppsflyerSDK(). +

    + - If GDPR doesn’t apply to the user perform the following: +
      +
    1. Call AppsFlyerLib.shared().setConsentData(AppsFlyerConsent(nonGDPRUser: ())). +
    2. It is optional to initialize AppsFlyerDestination using manual mode not mandatory as before. +
    +
+ + +##
Usage + +First of all, you must provide values for AppsFlyer Dev Key, Apple App ID (iTunes) and client secret in Segment's **dashboard** for AppsFlyer integration + + ## Support From f516078d78cfd8cc0243186938ef64387d152026 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:37:55 +0300 Subject: [PATCH 02/10] Update AppsFlyerDestination.swift --- Sources/SegmentAppsFlyer/AppsFlyerDestination.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SegmentAppsFlyer/AppsFlyerDestination.swift b/Sources/SegmentAppsFlyer/AppsFlyerDestination.swift index 13e269d..6d8d5b5 100644 --- a/Sources/SegmentAppsFlyer/AppsFlyerDestination.swift +++ b/Sources/SegmentAppsFlyer/AppsFlyerDestination.swift @@ -162,7 +162,10 @@ public class AppsFlyerDestination: UIResponder, DestinationPlugin { event.event == "Organic Install" || event.event == "Deep Link Opened" || event.event == "Direct Deep Link" || - event.event == "Deferred Deep Link"){ + event.event == "Deferred Deep Link" || + event.event == "Application Backgrounded" || + event.event == "Application Opened" || + event.event == "Application Foregrounded" ){ return nil } var properties = event.properties?.dictionaryValue From 58a56ef8bddabf8dc8ba4b2f7ea42524b96e890f Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:48:16 +0300 Subject: [PATCH 03/10] Update BasicExampleApp.swift --- .../BasicExample/BasicExampleApp.swift | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/Example/BasicExample/BasicExample/BasicExampleApp.swift b/Example/BasicExample/BasicExample/BasicExampleApp.swift index 565d4e0..4f7a06f 100644 --- a/Example/BasicExample/BasicExample/BasicExampleApp.swift +++ b/Example/BasicExample/BasicExample/BasicExampleApp.swift @@ -9,9 +9,48 @@ import SwiftUI import Segment import SegmentAppsFlyer import AppsFlyerLib +import AppTrackingTransparency @main struct BasicExampleApp: App { + static var analytics: Analytics? = nil + static var appsflyerDest: AppsFlyerDestination! + static var afDelegate: AFDelgate! + init() { + // Initialize delegate first and store it + BasicExampleApp.afDelegate = AFDelgate() + BasicExampleApp.analytics = Analytics(configuration: Configuration(writeKey: "") + .flushAt(3) + .trackApplicationLifecycleEvents(true) + ) + + AppsFlyerLib.shared().isDebug = true + //If waiting to ATT status please wait for it using the below line. + //AppsFlyerLib.shared().waitForATTUserAuthorization(timeoutInterval: 60) + + // Use the stored delegate + BasicExampleApp.appsflyerDest = AppsFlyerDestination( + segDelegate: BasicExampleApp.afDelegate, + segDLDelegate: BasicExampleApp.afDelegate, + ) + BasicExampleApp.analytics?.add(plugin: NewAnalyticsAppsflyerIntegrationApp.appsflyerDest) + } + + func requestTrackingAuthorization() { + ATTrackingManager.requestTrackingAuthorization { status in + switch status { + case .authorized: + // Tracking authorization granted + break + case .denied, .notDetermined, .restricted: + // Handle the case when permission is denied or not yet determined + break + @unknown default: + break + } + } + } + var body: some Scene { WindowGroup { ContentView() @@ -19,21 +58,15 @@ struct BasicExampleApp: App { } } -class DeepLinkManager: NSObject, DeepLinkDelegate { +class AFDelgate: NSObject, AppsFlyerLibDelegate, DeepLinkDelegate{ + func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { + print("moris testing onConversionDataSuccess") + } + + func onConversionDataFail(_ error: any Error) { + print("moris testing onConversionDataFail") + } func didResolveDeepLink(_ result: DeepLinkResult) { print("Deep Link: \(result)") } } - -extension Analytics { - static var main: Analytics { - let analytics = Analytics(configuration: Configuration(writeKey: "") - .flushAt(3) - .trackApplicationLifecycleEvents(true)) - - let deepLinkHandler = DeepLinkManager() - let appsFlyer = AppsFlyerDestination(segDLDelegate: deepLinkHandler) - analytics.add(plugin: appsFlyer) - return analytics - } -} From 467fb2b8078854716ef025f8c7ca47b61e728ed4 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:49:02 +0300 Subject: [PATCH 04/10] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8115d31..823d93f 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .package( name: "AppsFlyerLib-Dynamic", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Dynamic", - from: "6.14.0" + from: "6.17.0" ) ], targets: [ From a89d8e18816bece89f5eeeecc9ec45ad81f17df8 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:24:50 +0300 Subject: [PATCH 05/10] Update BasicExampleApp.swift --- Example/BasicExample/BasicExample/BasicExampleApp.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Example/BasicExample/BasicExample/BasicExampleApp.swift b/Example/BasicExample/BasicExample/BasicExampleApp.swift index 4f7a06f..bfb4db6 100644 --- a/Example/BasicExample/BasicExample/BasicExampleApp.swift +++ b/Example/BasicExample/BasicExample/BasicExampleApp.swift @@ -54,6 +54,11 @@ struct BasicExampleApp: App { var body: some Scene { WindowGroup { ContentView() + .onOpenURL { url in + // Handle deep links in SwiftUI + print("Deep link received: \(url)") + AppsFlyerLib.shared().handleOpen(url, options: nil) + } } } } From 5e12f2d9d85780b6288b55c49678ba9bbe09f346 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:26:59 +0300 Subject: [PATCH 06/10] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 823d93f..2121866 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .package( name: "AppsFlyerLib-Dynamic", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Dynamic", - from: "6.17.0" + .exact: "6.17.0" ) ], targets: [ From 8558695d461f838b5f161c4d781fe94f37da1a55 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:29:00 +0300 Subject: [PATCH 07/10] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2121866..823d93f 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .package( name: "AppsFlyerLib-Dynamic", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Dynamic", - .exact: "6.17.0" + from: "6.17.0" ) ], targets: [ From a7e84827942c1878e8ffd4ac8eac1efe3e10fec2 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:30:17 +0300 Subject: [PATCH 08/10] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 823d93f..2121866 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .package( name: "AppsFlyerLib-Dynamic", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Dynamic", - from: "6.17.0" + .exact: "6.17.0" ) ], targets: [ From 15ba7de71dc29491dfb5a7580d9b62bb07153447 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:31:09 +0300 Subject: [PATCH 09/10] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2121866..3591fbb 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .package( name: "AppsFlyerLib-Dynamic", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Dynamic", - .exact: "6.17.0" + .exact("6.17.0") ) ], targets: [ From ed28d7461fbb63dc1bb87c7cfa0e41d196891818 Mon Sep 17 00:00:00 2001 From: morisgateno-appsflyer <121490279+morisgateno-appsflyer@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:58:47 +0300 Subject: [PATCH 10/10] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6e8674..ed20419 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ analytics.add(plugin: AppsFlyerDestination()) Your events will now begin to flow to AppsFlyer in device mode. +## Appsflyer SDK documentation +Please go here to see the Appsflyer Native iOS documentation [here](https://dev.appsflyer.com/hc/docs/ios-sdk) + ## Manual mode We support a manual mode to seperate the initialization of the AppsFlyer SDK and the start of the SDK. In this case, the AppsFlyer SDK won't start automatically, giving the developer more freedom when to start the AppsFlyer SDK. Please note that in manual mode, the developper is required to implement the API ``startAppsflyerSDK()`` in order to start the SDK.
If you are using CMP to collect consent data this feature is needed. See explanation [here](#dma_support). @@ -178,7 +181,7 @@ struct NewAnalyticsAppsflyerIntegrationApp: App { ``` ##
Send consent for DMA compliance -For a general introduction to DMA consent data, see [here](https://dev.appsflyer.com/hc/docs/send-consent-for-dma-compliance). +**important:** As of Appsflyer SDK 6.17.0 there are additions in the Appsflyer SDK API on how to use DMA, [see here](https://dev.appsflyer.com/hc/docs/ios-send-consent-for-dma-compliance).
The SDK offers two alternative methods for gathering consent data:
- **Through a Consent Management Platform (CMP)**: If the app uses a CMP that complies with the [Transparency and Consent Framework (TCF) v2.2 protocol](https://iabeurope.eu/tcf-supporting-resources/), the SDK can automatically retrieve the consent details.

OR