diff --git a/CHANGELOG.md b/CHANGELOG.md index 797b2237..944fa510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Updated iOS SDK from 6.17.9 to 6.18.0 - Updated iOS Purchase Connector from 6.17.9 to 6.18.0 - Fixed Android warm-app deep link consumption race so `DeepLinkListener` fires reliably when the app is resumed from a `VIEW` intent (forward new intents to `AppsFlyerLib` from the plugin's `onNewIntentListener` before the SDK's `onResume` auto-handler marks them `af_consumed`) +- Added Swift Package Manager (SPM) support for the Core iOS integration (`ios/appsflyer_sdk/Package.swift`), alongside continued full CocoaPods support — no behavior change for existing CocoaPods consumers. Purchase Connector remains CocoaPods-only for now, pending resolution of an upstream Flutter limitation ([flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182)) that blocks conditionally-compiled plugin features under SPM. ## 6.17.9 diff --git a/doc/Installation.md b/doc/Installation.md index fe8be440..100c7aa6 100644 --- a/doc/Installation.md +++ b/doc/Installation.md @@ -10,6 +10,13 @@ flutter pub add appsflyer_sdk This will download the AppsFlyer flutter plugin to your project, you may observe the changes in your `pubspec.yaml` file. +--- +## iOS: Swift Package Manager (SPM) support + +Starting with v6.18.0, the plugin's **Core** integration supports Swift Package Manager on iOS, alongside continued full CocoaPods support. If your app has SPM enabled (the default on Flutter 3.44+, or via `flutter config --enable-swift-package-manager` on Flutter 3.24+), no extra setup is needed — Flutter's tooling picks up the plugin's `Package.swift` automatically. + +**Purchase Connector is CocoaPods-only.** If your app uses the [Purchase Connector](PurchaseConnector.md), it must stay on CocoaPods for now — there is no SPM opt-in path for it yet, pending resolution of an upstream Flutter limitation ([flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182)). Apps that need both SPM (for Core) and Purchase Connector (via CocoaPods) can use both simultaneously; Flutter's tooling handles this automatically as long as your `Podfile` still exists. + --- ## Huawei Referrer Huawei Referrer is supported in SDK v6.14.0 and above. diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index 6c63c52f..adc72ab5 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -81,6 +81,8 @@ appsflyer.enable_purchase_connector=true ``` Once you set these properties, the Purchase Validation feature will be integrated into your project and you can utilize its functionality in your app. +> ⚠️ **iOS + Swift Package Manager**: Purchase Connector requires **CocoaPods** — there is no Swift Package Manager opt-in path for it. This is a temporary limitation pending an upstream Flutter fix ([flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182)). If your app has adopted SPM for the Core integration (see [Installation.md](Installation.md#ios-swift-package-manager-spm-support)), keep your `Podfile` in place and set `$AppsFlyerPurchaseConnector = true` there as above — Flutter's tooling will use SPM for Core and CocoaPods for Purchase Connector at the same time. If you skip the Podfile entirely (SPM-only), calling any Purchase Connector API will silently fail with a `MissingPluginException` — see the next section. + ### What Happens if You Use Dart Files Without Opting In? The Dart files for the Purchase Validation feature are always included in the plugin. If you try to use these Dart APIs without opting into the feature, the APIs will not have effect because the corresponding native code necessary for them to function will not be included in your project. diff --git a/internal-docs/features/F-001-sdk-initialization.md b/internal-docs/features/F-001-sdk-initialization.md index fe659c47..39a3a68a 100644 --- a/internal-docs/features/F-001-sdk-initialization.md +++ b/internal-docs/features/F-001-sdk-initialization.md @@ -30,7 +30,7 @@ AppsflyerSdk.initSdk({registerConversionDataCallback, registerOnAppOpenAttributi → Android: AppsflyerSdkPlugin.onMethodCall("initSdk") → initSdk(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().init(afDevKey, gcdListener, mContext) → instance.start(activity) [only if isManualStartMode == false] - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared].appsFlyerDevKey / .appleAppID / .isDebug = ... → [[AppsFlyerLib shared] start] [only if manualStart == NO] ``` @@ -45,8 +45,8 @@ AppsflyerSdk.initSdk({registerConversionDataCallback, registerOnAppOpenAttributi | `lib/src/appsflyer_constants.dart` | String keys shared across Dart/native (`AF_DEV_KEY`, `AF_APP_Id`, `AF_MANUAL_START`, `AF_GCD`, `AF_UDL`, `PLUGIN_VERSION`) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `initSdk(call, result)` — native Android init, conditional auto-start | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java` | Native Android mirror of the Dart string keys | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — native iOS init, conditional auto-start | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define` string keys (`afDevKey`, `afAppId`, `afManualStart`, …) and `kAppsFlyerPluginVersion` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — native iOS init, conditional auto-start | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define` string keys (`afDevKey`, `afAppId`, `afManualStart`, …) and `kAppsFlyerPluginVersion` | --- diff --git a/internal-docs/features/F-002-sdk-start.md b/internal-docs/features/F-002-sdk-start.md index 2cd1cf80..e4d4b554 100644 --- a/internal-docs/features/F-002-sdk-start.md +++ b/internal-docs/features/F-002-sdk-start.md @@ -30,7 +30,7 @@ AppsflyerSdk.startSDK({onSuccess, onError}) [li → Android: AppsflyerSdkPlugin.startSDKwithHandler(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().start(activity, null, AppsFlyerRequestListener) → onSuccess()/onError() → mMethodChannel.invokeMethod("onSuccess"|"onError") - → iOS: AppsflyerSdkPlugin.startSDKwithHandler:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.startSDKwithHandler:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] startWithCompletionHandler:^(...)] → [_methodChannel invokeMethod:@"onSuccess"|@"onError" ...] → else: @@ -46,7 +46,7 @@ AppsflyerSdk.startSDK({onSuccess, onError}) [li |------|------| | `lib/src/appsflyer_sdk.dart` | `startSDK()` — guards double-start via `_isSdkStarted`, chooses handler vs. plain path | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `startSDK`, `startSDKwithHandler` — native start, posts `onSuccess`/`onError` back on the UI thread | -| `ios/Classes/AppsflyerSdkPlugin.m` | `startSDK:result:`, `startSDKwithHandler:result:` — native start, dispatches completion handler results on main queue | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `startSDK:result:`, `startSDKwithHandler:result:` — native start, dispatches completion handler results on main queue | --- diff --git a/internal-docs/features/F-003-sdk-plugin-version-retrieval.md b/internal-docs/features/F-003-sdk-plugin-version-retrieval.md index 32a05db7..5b031235 100644 --- a/internal-docs/features/F-003-sdk-plugin-version-retrieval.md +++ b/internal-docs/features/F-003-sdk-plugin-version-retrieval.md @@ -26,7 +26,7 @@ AppsflyerSdk.getSDKVersion() [lib/src/a → _methodChannel.invokeMethod("getSDKVersion") → Android: AppsflyerSdkPlugin.onMethodCall("getSDKVersion") → getSdkVersion(result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().getSdkVersion() - → iOS: AppsflyerSdkPlugin.handleMethodCall("getSDKVersion") → getSDKVersion:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("getSDKVersion") → getSDKVersion:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] getSDKVersion] AppsflyerSdk.getVersionNumber() [lib/src/appsflyer_sdk.dart] @@ -41,7 +41,7 @@ AppsflyerSdk.getVersionNumber() [lib/src/a | `lib/src/appsflyer_sdk.dart` | `getSDKVersion()` (async, native round-trip), `getVersionNumber()` (sync, local constant) | | `lib/src/appsflyer_constants.dart` | `PLUGIN_VERSION` constant returned by `getVersionNumber()` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `getSdkVersion(result)` — proxies `AppsFlyerLib.getInstance().getSdkVersion()` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `getSDKVersion:result:` — proxies `[AppsFlyerLib shared] getSDKVersion]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `getSDKVersion:result:` — proxies `[AppsFlyerLib shared] getSDKVersion]` | --- diff --git a/internal-docs/features/F-004-in-app-event-logging.md b/internal-docs/features/F-004-in-app-event-logging.md index 0eafa4b7..8cf260b3 100644 --- a/internal-docs/features/F-004-in-app-event-logging.md +++ b/internal-docs/features/F-004-in-app-event-logging.md @@ -27,7 +27,7 @@ AppsflyerSdk.logEvent(eventName, eventValues) → Android: AppsflyerSdkPlugin.onMethodCall("logEvent") → logEvent(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().logEvent(mContext, eventName, eventValues) → result.success(true) - → iOS: AppsflyerSdkPlugin.handleMethodCall("logEvent") → logEventWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("logEvent") → logEventWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] logEvent:eventName withValues:eventValues] → result(@YES) ``` @@ -39,7 +39,7 @@ AppsflyerSdk.logEvent(eventName, eventValues) |------|------| | `lib/src/appsflyer_sdk.dart` | `logEvent(String eventName, Map? eventValues)` — Dart public API, returns `Future` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `logEvent(MethodCall, Result)` — reads `AF_EVENT_NAME`/`AF_EVENT_VALUES` args, forwards to `AppsFlyerLib.getInstance().logEvent(mContext, eventName, eventValues)`, always returns `result.success(true)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `logEventWithCall:result:` — reads `eventName`/`eventValues` (normalizes `NSNull` to `nil`), forwards to `[[AppsFlyerLib shared] logEvent:withValues:]`, always returns `result(@YES)`; comment `//TODO: Add callback handler` marks that no completion callback is wired | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `logEventWithCall:result:` — reads `eventName`/`eventValues` (normalizes `NSNull` to `nil`), forwards to `[[AppsFlyerLib shared] logEvent:withValues:]`, always returns `result(@YES)`; comment `//TODO: Add callback handler` marks that no completion callback is wired | | `doc/InAppEvents.md` | Public integration guide with usage example | --- diff --git a/internal-docs/features/F-005-ad-revenue-logging.md b/internal-docs/features/F-005-ad-revenue-logging.md index 919bc3d7..d3df7818 100644 --- a/internal-docs/features/F-005-ad-revenue-logging.md +++ b/internal-docs/features/F-005-ad-revenue-logging.md @@ -29,7 +29,7 @@ AppsflyerSdk.logAdRevenue(AdRevenueData) → new AFAdRevenueData(monetizationNetwork, mediationNetwork, currencyIso4217Code, revenue) → AppsFlyerLib.getInstance().logAdRevenue(adRevenueData, additionalParameters) → result.success(true) | result.error(...) on invalid/unexpected input - → iOS: AppsflyerSdkPlugin.handleMethodCall("logAdRevenue") → logAdRevenue:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("logAdRevenue") → logAdRevenue:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → getEnumValueFromString: maps the Dart enum's string value to AppsFlyerAdRevenueMediationNetworkType → [[AFAdRevenueData alloc] initWithMonetizationNetwork:mediationNetwork:currencyIso4217Code:eventRevenue:] → [[AppsFlyerLib shared] logAdRevenue:additionalParameters:] @@ -45,7 +45,7 @@ AppsflyerSdk.logAdRevenue(AdRevenueData) | `lib/src/appsflyer_ad_revenue_data.dart` | `AdRevenueData` model: `monetizationNetwork`, `mediationNetwork` (String), `currencyIso4217Code`, `revenue` (double), optional `additionalParameters` | | `lib/src/appsflyer_constants.dart` | `AFMediationNetwork` enum with a `.value` getter mapping each case (e.g. `applovinMax`) to the exact lowercase/snake_case string (`"applovin_max"`) both native sides expect | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `logAdRevenue(MethodCall, Result)` — validates required args via `requireNonNullArgument`, converts the mediation-network string to the native `MediationNetwork` enum via `.valueOf(...toUpperCase())`, builds `AFAdRevenueData`, calls `AppsFlyerLib.getInstance().logAdRevenue(...)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `logAdRevenue:result:` and `getEnumValueFromString:` — validates required args, maps the mediation-network string to `AppsFlyerAdRevenueMediationNetworkType` via an explicit `NSDictionary` lookup table, builds `AFAdRevenueData`, calls `[[AppsFlyerLib shared] logAdRevenue:additionalParameters:]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `logAdRevenue:result:` and `getEnumValueFromString:` — validates required args, maps the mediation-network string to `AppsFlyerAdRevenueMediationNetworkType` via an explicit `NSDictionary` lookup table, builds `AFAdRevenueData`, calls `[[AppsFlyerLib shared] logAdRevenue:additionalParameters:]` | | `doc/API.md` | `logAdRevenue` / `AdRevenueData` / `AFMediationNetwork` public documentation and usage example | --- diff --git a/internal-docs/features/F-006-custom-host-configuration.md b/internal-docs/features/F-006-custom-host-configuration.md index 03cd486c..80a1b86f 100644 --- a/internal-docs/features/F-006-custom-host-configuration.md +++ b/internal-docs/features/F-006-custom-host-configuration.md @@ -26,7 +26,7 @@ AppsflyerSdk.setHost(hostPrefix, hostName) [lib/src → _methodChannel.invokeMethod("setHost", {hostPrefix, hostName}) → Android: AppsflyerSdkPlugin.onMethodCall("setHost") → setHost(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setHost(hostPrefix, hostName) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setHost") → setHost:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setHost") → setHost:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setHost:hostName withHostPrefix:hostPrefix] AppsflyerSdk.getHostName() / getHostPrefix() @@ -43,7 +43,7 @@ AppsflyerSdk.getHostName() / getHostPrefix() | `lib/src/appsflyer_sdk.dart` | `setHost`, `getHostName`, `getHostPrefix` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setHost`, `getHostName`, `getHostPrefix` native handlers | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java` | `AF_HOST_PREFIX`, `AF_HOST_NAME` argument key constants | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setHost:result:`, `getHostName:result:`, `getHostPrefix:result:` native handlers | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setHost:result:`, `getHostName:result:`, `getHostPrefix:result:` native handlers | --- diff --git a/internal-docs/features/F-007-device-id-collection-optout.md b/internal-docs/features/F-007-device-id-collection-optout.md index ee368596..c09617c3 100644 --- a/internal-docs/features/F-007-device-id-collection-optout.md +++ b/internal-docs/features/F-007-device-id-collection-optout.md @@ -32,7 +32,7 @@ AppsflyerSdk.setCollectAndroidId(isCollect) [lib/src → Android: AppsflyerSdkPlugin.onMethodCall("setCollectAndroidId") → setCollectAndroidId(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setCollectAndroidID(isCollect) ``` -No iOS branch exists for either method name in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — on iOS these calls fall through to `result(FlutterMethodNotImplemented)`. +No iOS branch exists for either method name in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — on iOS these calls fall through to `result(FlutterMethodNotImplemented)`. --- diff --git a/internal-docs/features/F-008-manual-imei-android-id-override.md b/internal-docs/features/F-008-manual-imei-android-id-override.md index 3a504be5..35869e17 100644 --- a/internal-docs/features/F-008-manual-imei-android-id-override.md +++ b/internal-docs/features/F-008-manual-imei-android-id-override.md @@ -32,7 +32,7 @@ AppsflyerSdk.setAndroidIdData(androidId) [lib/src → Android: AppsflyerSdkPlugin.onMethodCall("setAndroidIdData") → setAndroidIdData(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setAndroidIdData(androidId) ``` -No iOS branch exists for either method name in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:`. +No iOS branch exists for either method name in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:`. --- diff --git a/internal-docs/features/F-009-min-time-between-sessions.md b/internal-docs/features/F-009-min-time-between-sessions.md index 4a60817d..3e3eb6b0 100644 --- a/internal-docs/features/F-009-min-time-between-sessions.md +++ b/internal-docs/features/F-009-min-time-between-sessions.md @@ -27,7 +27,7 @@ AppsflyerSdk.setMinTimeBetweenSessions(seconds) [lib/sr → _methodChannel.invokeMethod("setMinTimeBetweenSessions", {'seconds': seconds}) → Android: AppsflyerSdkPlugin.onMethodCall("setMinTimeBetweenSessions") → setMinTimeBetweenSessions(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setMinTimeBetweenSessions(seconds) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setMinTimeBetweenSessions") → setMinTimeBetweenSessions:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setMinTimeBetweenSessions") → setMinTimeBetweenSessions:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared].minTimeBetweenSessions = seconds ``` @@ -38,7 +38,7 @@ AppsflyerSdk.setMinTimeBetweenSessions(seconds) [lib/sr |------|------| | `lib/src/appsflyer_sdk.dart` | `setMinTimeBetweenSessions(int)` — asserts non-negative seconds, dispatches to channel | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setMinTimeBetweenSessions` native handler | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setMinTimeBetweenSessions:result:` native handler (direct property assignment) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setMinTimeBetweenSessions:result:` native handler (direct property assignment) | --- diff --git a/internal-docs/features/F-010-currency-code-setting.md b/internal-docs/features/F-010-currency-code-setting.md index eee0cb41..4550b6d7 100644 --- a/internal-docs/features/F-010-currency-code-setting.md +++ b/internal-docs/features/F-010-currency-code-setting.md @@ -27,7 +27,7 @@ AppsflyerSdk.setCurrencyCode(currencyCode) → Android: AppsflyerSdkPlugin.onMethodCall("setCurrencyCode") → setCurrencyCode(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setCurrencyCode(currencyCode) → result.success(null) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setCurrencyCode") → setCurrencyCode:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setCurrencyCode") → setCurrencyCode:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setCurrencyCode:currencyCode] → result(nil) ``` @@ -39,7 +39,7 @@ AppsflyerSdk.setCurrencyCode(currencyCode) |------|------| | `lib/src/appsflyer_sdk.dart` | `setCurrencyCode(String currencyCode)` — platform-agnostic Dart API, `void` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setCurrencyCode(MethodCall, Result)` — forwards to `AppsFlyerLib.getInstance().setCurrencyCode(currencyCode)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setCurrencyCode:result:` — forwards to `[[AppsFlyerLib shared] setCurrencyCode:]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setCurrencyCode:result:` — forwards to `[[AppsFlyerLib shared] setCurrencyCode:]` | | `doc/API.md` | Public documentation for `setCurrencyCode` | --- diff --git a/internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md b/internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md index 2a9a3431..7bf6f923 100644 --- a/internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md +++ b/internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md @@ -26,7 +26,7 @@ AppsflyerSdk.enableTCFDataCollection(shouldCollect) [lib/sr → _methodChannel.invokeListMethod("enableTCFDataCollection", {'shouldCollect': shouldCollect}) → Android: AppsflyerSdkPlugin.onMethodCall("enableTCFDataCollection") → enableTCFDataCollection(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().enableTCFDataCollection(shouldCollect) - → iOS: AppsflyerSdkPlugin.handleMethodCall("enableTCFDataCollection") → enableTCFDataCollection:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("enableTCFDataCollection") → enableTCFDataCollection:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] enableTCFDataCollection:shouldCollect] ``` @@ -37,7 +37,7 @@ AppsflyerSdk.enableTCFDataCollection(shouldCollect) [lib/sr |------|------| | `lib/src/appsflyer_sdk.dart` | `enableTCFDataCollection(bool)` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `enableTCFDataCollection` native handler | -| `ios/Classes/AppsflyerSdkPlugin.m` | `enableTCFDataCollection:result:` native handler | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `enableTCFDataCollection:result:` native handler | | `doc/DMA.md` | Integration guide documenting the required manual-start + CMP sequencing | --- diff --git a/internal-docs/features/F-012-manual-gdpr-dma-consent-api.md b/internal-docs/features/F-012-manual-gdpr-dma-consent-api.md index 4cb8c8c8..84c962bf 100644 --- a/internal-docs/features/F-012-manual-gdpr-dma-consent-api.md +++ b/internal-docs/features/F-012-manual-gdpr-dma-consent-api.md @@ -27,7 +27,7 @@ AppsflyerSdk.setConsentData(AppsFlyerConsent consentData) [DEPRECATED] → Android: AppsflyerSdkPlugin.onMethodCall("setConsentData") → setConsentData(call, result) [android/.../AppsflyerSdkPlugin.java] → new AppsFlyerConsent.forGDPRUser(...) | AppsFlyerConsent.forNonGDPRUser() → AppsFlyerLib.getInstance().setConsentData(consentData) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setConsentData") → setConsentData:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setConsentData") → setConsentData:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerConsent alloc] initForGDPRUserWith...] | initWithNonGDPRUser → [[AppsFlyerLib shared] setConsentData:consentData] @@ -36,7 +36,7 @@ AppsflyerSdk.setConsentDataV2({isUserSubjectToGDPR, consentForDataUsage, consent → Android: AppsflyerSdkPlugin.onMethodCall("setConsentDataV2") → setConsentDataV2(call, result) → getAppsFlyerConsentFromCall(call) [android/.../AppsflyerSdkPlugin.java] → new AppsFlyerConsent(isUserSubjectToGDPR, consentForDataUsage, consentForAdsPersonalization, hasConsentForAdStorage) → AppsFlyerLib.getInstance().setConsentData(consent) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setConsentDataV2") → setConsentDataV2:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setConsentDataV2") → setConsentDataV2:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerConsent alloc] initWithIsUserSubjectToGDPR:...hasConsentForAdStorage:...] → [[AppsFlyerLib shared] setConsentData:consentData] ``` @@ -49,7 +49,7 @@ AppsflyerSdk.setConsentDataV2({isUserSubjectToGDPR, consentForDataUsage, consent | `lib/src/appsflyer_consent.dart` | `AppsFlyerConsent` model — `forGDPRUser`/`nonGDPRUser` factories, `toMap()` (used by deprecated V1 API only) | | `lib/src/appsflyer_sdk.dart` | `setConsentData` (`@Deprecated`), `setConsentDataV2` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setConsentData` (deprecated), `setConsentDataV2`, `getAppsFlyerConsentFromCall` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setConsentData:result:` (deprecated), `setConsentDataV2:result:` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setConsentData:result:` (deprecated), `setConsentDataV2:result:` | | `doc/DMA.md` | Full integration guide for both the CMP-automatic (F-011) and manual (this feature) consent paths | --- diff --git a/internal-docs/features/F-013-user-anonymization.md b/internal-docs/features/F-013-user-anonymization.md index cb28b677..725b3ec6 100644 --- a/internal-docs/features/F-013-user-anonymization.md +++ b/internal-docs/features/F-013-user-anonymization.md @@ -26,7 +26,7 @@ AppsflyerSdk.anonymizeUser(shouldAnonymize) [lib/sr → _methodChannel.invokeMethod("anonymizeUser", {'shouldAnonymize': shouldAnonymize}) → Android: AppsflyerSdkPlugin.onMethodCall("anonymizeUser") → anonymizeUser(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().anonymizeUser(shouldAnonymize) - → iOS: AppsflyerSdkPlugin.handleMethodCall("anonymizeUser") → anonymizeUser:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("anonymizeUser") → anonymizeUser:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared].anonymizeUser = shouldAnonymize ``` @@ -37,7 +37,7 @@ AppsflyerSdk.anonymizeUser(shouldAnonymize) [lib/sr |------|------| | `lib/src/appsflyer_sdk.dart` | `anonymizeUser(bool)` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `anonymizeUser` native handler | -| `ios/Classes/AppsflyerSdkPlugin.m` | `anonymizeUser:result:` native handler (direct property assignment) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `anonymizeUser:result:` native handler (direct property assignment) | --- diff --git a/internal-docs/features/F-014-manual-deep-link-retrigger.md b/internal-docs/features/F-014-manual-deep-link-retrigger.md index fa4c0d5a..7d57405b 100644 --- a/internal-docs/features/F-014-manual-deep-link-retrigger.md +++ b/internal-docs/features/F-014-manual-deep-link-retrigger.md @@ -39,7 +39,7 @@ AppsflyerSdk.performOnDeepLinking() [li |------|------| | `lib/src/appsflyer_sdk.dart` | `performOnDeepLinking()` — platform-agnostic Dart API, no `Platform.isAndroid` guard | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `performOnDeepLinking(call, result)` — reads `activity.getIntent()` and forwards it to `AppsFlyerLib.getInstance().performOnDeepLinking(intent, mApplication)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | No corresponding case in `handleMethodCall:` — the method name is entirely absent | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | No corresponding case in `handleMethodCall:` — the method name is entirely absent | --- diff --git a/internal-docs/features/F-015-customer-user-id.md b/internal-docs/features/F-015-customer-user-id.md index 641a4f33..83a1fe0e 100644 --- a/internal-docs/features/F-015-customer-user-id.md +++ b/internal-docs/features/F-015-customer-user-id.md @@ -26,7 +26,7 @@ AppsflyerSdk.setCustomerUserId(id) [lib/sr → _methodChannel.invokeMethod("setCustomerUserId", {'id': id}) → Android: AppsflyerSdkPlugin.onMethodCall("setCustomerUserId") → setCustomerUserId(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setCustomerUserId(userId) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setCustomerUserId") → setCustomerUserId:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setCustomerUserId") → setCustomerUserId:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setCustomerUserID:userId] ``` Note: the related (but distinct) Dart API `setCustomerIdAndLogSession(id)` invokes the channel method `"setCustomerIdAndLogSession"`, which Android handles with its own `setCustomerIdAndLogSession(call, result)` (calling `AppsFlyerLib.getInstance().setCustomerIdAndLogSession(userId, mContext)`), while iOS routes `"setCustomerIdAndLogSession"` to the *same* `setCustomerUserId:result:` handler as plain `setCustomerUserId` — iOS has no distinct "and log session" native behavior. @@ -38,7 +38,7 @@ Note: the related (but distinct) Dart API `setCustomerIdAndLogSession(id)` invok |------|------| | `lib/src/appsflyer_sdk.dart` | `setCustomerUserId(String)` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setCustomerUserId` native handler | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setCustomerUserId:result:` native handler | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setCustomerUserId:result:` native handler | --- diff --git a/internal-docs/features/F-016-update-vs-fresh-install-flag.md b/internal-docs/features/F-016-update-vs-fresh-install-flag.md index 01c946b1..f8bdd8fc 100644 --- a/internal-docs/features/F-016-update-vs-fresh-install-flag.md +++ b/internal-docs/features/F-016-update-vs-fresh-install-flag.md @@ -26,7 +26,7 @@ AppsflyerSdk.setIsUpdate(isUpdate) [lib/sr → _methodChannel.invokeMethod("setIsUpdate", {'isUpdate': isUpdate}) → Android: AppsflyerSdkPlugin.onMethodCall("setIsUpdate") → setIsUpdate(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setIsUpdate(isUpdate) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setIsUpdate") → (no-op) [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setIsUpdate") → (no-op) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] ``` --- @@ -36,7 +36,7 @@ AppsflyerSdk.setIsUpdate(isUpdate) [lib/sr |------|------| | `lib/src/appsflyer_sdk.dart` | `setIsUpdate(bool)` — platform-agnostic Dart API (no `Platform.isAndroid` guard) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setIsUpdate` native handler — forwards to `AppsFlyerLib.getInstance().setIsUpdate(isUpdate)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `handleMethodCall:` contains an empty `else if([@"setIsUpdate" isEqualToString:call.method]){ }` branch — matched but intentionally does nothing | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `handleMethodCall:` contains an empty `else if([@"setIsUpdate" isEqualToString:call.method]){ }` branch — matched but intentionally does nothing | --- @@ -54,7 +54,7 @@ AppsflyerSdk.setIsUpdate(isUpdate) [lib/sr --- ## Known Limitations -- **iOS is a documented no-op**: in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:`, the `"setIsUpdate"` branch is matched (`if([@"setIsUpdate" isEqualToString:call.method]){ }`) but its body is empty — no native AppsFlyer API is called, and critically, `result(...)` is never invoked either. Since this branch matches inside an `if/else if` chain, control does not fall through to the trailing `result(FlutterMethodNotImplemented)` — the platform channel's pending reply for `setIsUpdate` on iOS is simply never resolved. Dart's `setIsUpdate()` is `void` and does not await the result, so this is silent to the caller today, but the update-vs-install distinction this API is meant to convey has **no effect whatsoever on iOS** — only Android attribution logic actually receives it. +- **iOS is a documented no-op**: in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:`, the `"setIsUpdate"` branch is matched (`if([@"setIsUpdate" isEqualToString:call.method]){ }`) but its body is empty — no native AppsFlyer API is called, and critically, `result(...)` is never invoked either. Since this branch matches inside an `if/else if` chain, control does not fall through to the trailing `result(FlutterMethodNotImplemented)` — the platform channel's pending reply for `setIsUpdate` on iOS is simply never resolved. Dart's `setIsUpdate()` is `void` and does not await the result, so this is silent to the caller today, but the update-vs-install distinction this API is meant to convey has **no effect whatsoever on iOS** — only Android attribution logic actually receives it. - The Dart API has no platform guard and gives no compile-time or runtime signal that calling `setIsUpdate` on iOS is a no-op; an integrator relying on it cross-platform would reasonably but incorrectly assume parity with Android. - No enforced ordering relative to `initSdk()` — the native SDK's own documentation-level expectation (call before init so the flag is available for the very first session) is not validated by either native handler. diff --git a/internal-docs/features/F-017-sdk-kill-switch.md b/internal-docs/features/F-017-sdk-kill-switch.md index 950dc22b..064a0829 100644 --- a/internal-docs/features/F-017-sdk-kill-switch.md +++ b/internal-docs/features/F-017-sdk-kill-switch.md @@ -26,7 +26,7 @@ AppsflyerSdk.stop(isStopped) [lib/src/ → _methodChannel.invokeMethod("stop", {'isStopped': isStopped}) → Android: AppsflyerSdkPlugin.onMethodCall("stop") → stop(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().stop(isStopped, mContext) - → iOS: AppsflyerSdkPlugin.handleMethodCall("stop") → stop:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("stop") → stop:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared].isStopped = stop ``` @@ -37,7 +37,7 @@ AppsflyerSdk.stop(isStopped) [lib/src/ |------|------| | `lib/src/appsflyer_sdk.dart` | `stop(bool)` — Dart API surface | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `stop(call, result)` native handler, line 1033 | -| `ios/Classes/AppsflyerSdkPlugin.m` | `stop:result:` native handler (direct property assignment), line 734 | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `stop:result:` native handler (direct property assignment), line 734 | --- diff --git a/internal-docs/features/F-018-uninstall-measurement.md b/internal-docs/features/F-018-uninstall-measurement.md index 3b6eff1a..f7d12595 100644 --- a/internal-docs/features/F-018-uninstall-measurement.md +++ b/internal-docs/features/F-018-uninstall-measurement.md @@ -26,7 +26,7 @@ AppsflyerSdk.updateServerUninstallToken(token) [lib/src/ → _methodChannel.invokeMethod("updateServerUninstallToken", {'token': token}) → Android: AppsflyerSdkPlugin.onMethodCall("updateServerUninstallToken") → updateServerUninstallToken(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().updateServerUninstallToken(mContext, token) - → iOS: AppsflyerSdkPlugin.handleMethodCall("updateServerUninstallToken") → updateServerUninstallToken:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("updateServerUninstallToken") → updateServerUninstallToken:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → hex-string token manually decoded into NSData → [AppsFlyerLib shared] registerUninstall:deviceTokenData] @@ -41,7 +41,7 @@ AppsflyerSdk.enableUninstallTracking(senderId) [DEPRECATED — no-op] [lib/s |------|------| | `lib/src/appsflyer_sdk.dart` | `updateServerUninstallToken(String)` (active), `enableUninstallTracking(String)` (`@Deprecated`, no-op) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `updateServerUninstallToken(call, result)`, line 1027 | -| `ios/Classes/AppsflyerSdkPlugin.m` | `updateServerUninstallToken:result:`, line 740 — converts hex-string token to `NSData` before calling `registerUninstall:` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `updateServerUninstallToken:result:`, line 740 — converts hex-string token to `NSData` before calling `registerUninstall:` | | `doc/AdvancedAPI.md` | "Measure App Uninstalls" section documents both the iOS-native (`registerUninstall:` in `AppDelegate.m`) and plugin-side paths, and the Firebase Messaging integration pattern | --- @@ -60,7 +60,7 @@ AppsflyerSdk.enableUninstallTracking(senderId) [DEPRECATED — no-op] [lib/s --- ## Known Limitations -- `enableUninstallTracking(senderId)` is `@Deprecated` and, unlike most other deprecated methods in this file, has been fully gutted — it only prints a message and does nothing else, even though the `ios/Classes/AppsflyerSdkPlugin.m` method-dispatch table still has a (no-op) `enableUninstallTracking` branch left over from the old implementation. +- `enableUninstallTracking(senderId)` is `@Deprecated` and, unlike most other deprecated methods in this file, has been fully gutted — it only prints a message and does nothing else, even though the `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` method-dispatch table still has a (no-op) `enableUninstallTracking` branch left over from the old implementation. - On iOS, `updateServerUninstallToken`'s hex-string parsing has no length/format validation — a malformed or odd-length hex string will silently produce truncated/incorrect `NSData` rather than raising an error back to Dart. - The app is responsible for obtaining and refreshing the push token itself (e.g. via `firebase_messaging`); this API only forwards whatever string it is given, so a stale or missing token upstream silently degrades uninstall measurement with no error surfaced to the caller. diff --git a/internal-docs/features/F-019-user-email-collection.md b/internal-docs/features/F-019-user-email-collection.md index e7e28c08..adb200f3 100644 --- a/internal-docs/features/F-019-user-email-collection.md +++ b/internal-docs/features/F-019-user-email-collection.md @@ -28,7 +28,7 @@ AppsflyerSdk.setUserEmails(emails, cryptType) [lib/src/ → Android: AppsflyerSdkPlugin.onMethodCall("setUserEmails") → setUserEmails(call, result) [android/.../AppsflyerSdkPlugin.java] → maps cryptTypeInt (0/1) to AppsFlyerProperties.EmailsCryptType.NONE / SHA256 (throws InvalidParameterException on any other value) → AppsFlyerLib.getInstance().setUserEmails(cryptType, emails.toArray(new String[0])) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setUserEmails") → setUserEmails:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setUserEmails") → setUserEmails:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → maps cryptTypeInt to native EmailCryptType (EmailCryptTypeNone / EmailCryptTypeSHA256) → [AppsFlyerLib shared] setUserEmails:cryptType:] ``` @@ -41,7 +41,7 @@ AppsflyerSdk.setUserEmails(emails, cryptType) [lib/src/ | `lib/src/appsflyer_sdk.dart` | `setUserEmails(List, [EmailCryptType?])` — converts the enum to its integer index before sending | | `lib/src/appsflyer_constants.dart` | `enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 }` — index 0/1 is the wire format sent to native | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setUserEmails(call, result)`, line 983 — maps int to `AppsFlyerProperties.EmailsCryptType`, throws on unrecognized value | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setUserEmails:result:`, line 761 — maps int to native `EmailCryptType` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setUserEmails:result:`, line 761 — maps int to native `EmailCryptType` | --- diff --git a/internal-docs/features/F-020-appsflyer-uid-retrieval.md b/internal-docs/features/F-020-appsflyer-uid-retrieval.md index 9cf10151..2fec51c9 100644 --- a/internal-docs/features/F-020-appsflyer-uid-retrieval.md +++ b/internal-docs/features/F-020-appsflyer-uid-retrieval.md @@ -26,7 +26,7 @@ AppsflyerSdk.getAppsFlyerUID() [lib/src/a → _methodChannel.invokeMethod("getAppsFlyerUID") → Android: AppsflyerSdkPlugin.onMethodCall("getAppsFlyerUID") → getAppsFlyerUID(result) [android/.../AppsflyerSdkPlugin.java] → result.success(AppsFlyerLib.getInstance().getAppsFlyerUID(mContext)) - → iOS: AppsflyerSdkPlugin.handleMethodCall("getAppsFlyerUID") → getAppsFlyerUID:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("getAppsFlyerUID") → getAppsFlyerUID:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → result([[AppsFlyerLib shared] getAppsFlyerUID]) ``` @@ -37,7 +37,7 @@ AppsflyerSdk.getAppsFlyerUID() [lib/src/a |------|------| | `lib/src/appsflyer_sdk.dart` | `getAppsFlyerUID()` — `Future` async round-trip | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `getAppsFlyerUID(result)`, line 797 | -| `ios/Classes/AppsflyerSdkPlugin.m` | `getAppsFlyerUID:result:`, line 602 | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `getAppsFlyerUID:result:`, line 602 | --- diff --git a/internal-docs/features/F-021-delayed-session-start-pending-cuid.md b/internal-docs/features/F-021-delayed-session-start-pending-cuid.md index 38a0dc86..44f71c32 100644 --- a/internal-docs/features/F-021-delayed-session-start-pending-cuid.md +++ b/internal-docs/features/F-021-delayed-session-start-pending-cuid.md @@ -26,14 +26,14 @@ AppsflyerSdk.waitForCustomerUserId(wait) [lib/src/ → _methodChannel.invokeMethod("waitForCustomerUserId", {'wait': wait}) → Android: AppsflyerSdkPlugin.onMethodCall("waitForCustomerUserId") → waitForCustomerUserId(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().waitForCustomerUserId(wait) - → iOS: AppsflyerSdkPlugin.handleMethodCall("waitForCustomerUserId") → waitForCustomerId:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("waitForCustomerUserId") → waitForCustomerId:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → NO-OP — the method body only calls result(nil); no native AppsFlyerLib API is invoked AppsflyerSdk.setCustomerIdAndLogSession(id) [lib/src/appsflyer_sdk.dart] → _methodChannel.invokeMethod("setCustomerIdAndLogSession", {'id': id}) → Android: AppsflyerSdkPlugin.onMethodCall("setCustomerIdAndLogSession") → setCustomerIdAndLogSession(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setCustomerIdAndLogSession(id, mContext) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setCustomerIdAndLogSession") → setCustomerUserId:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setCustomerIdAndLogSession") → setCustomerUserId:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → routed to the same handler as plain setCustomerUserId — [AppsFlyerLib shared] setCustomerUserID:id]; no "log session" semantics ``` @@ -44,7 +44,7 @@ AppsflyerSdk.setCustomerIdAndLogSession(id) [lib/src/ |------|------| | `lib/src/appsflyer_sdk.dart` | `waitForCustomerIdAndLogSession` split into `waitForCustomerUserId(bool)` and `setCustomerIdAndLogSession(String)` — no `Platform.isAndroid` guard on either | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `waitForCustomerUserId(call, result)` (line 971), `setCustomerIdAndLogSession(call, result)` (line 1009) — both proxy real native APIs | -| `ios/Classes/AppsflyerSdkPlugin.m` | `waitForCustomerId:result:` (line 757, no-op stub), `setCustomerIdAndLogSession` dispatch aliased to `setCustomerUserId:result:` (line 107/722) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `waitForCustomerId:result:` (line 757, no-op stub), `setCustomerIdAndLogSession` dispatch aliased to `setCustomerUserId:result:` (line 107/722) | | `doc/API.md` | Explicitly documents both APIs as **"Android only!"** (lines 440, 449) | --- diff --git a/internal-docs/features/F-022-push-notification-deep-link-path-config.md b/internal-docs/features/F-022-push-notification-deep-link-path-config.md index 33450004..1cd63e82 100644 --- a/internal-docs/features/F-022-push-notification-deep-link-path-config.md +++ b/internal-docs/features/F-022-push-notification-deep-link-path-config.md @@ -26,7 +26,7 @@ AppsflyerSdk.addPushNotificationDeepLinkPath(List deeplinkPath) → _methodChannel.invokeMethod("addPushNotificationDeepLinkPath", deeplinkPath) → Android: AppsflyerSdkPlugin.onMethodCall("addPushNotificationDeepLinkPath") → addPushNotificationDeepLinkPath(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().addPushNotificationDeepLinkPath(String[] path) - → iOS: AppsflyerSdkPlugin.handleMethodCall("addPushNotificationDeepLinkPath") → addPushNotificationDeepLinkPath:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("addPushNotificationDeepLinkPath") → addPushNotificationDeepLinkPath:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] addPushNotificationDeepLinkPath:deeplinkPath] ``` The configured path is later consulted when a push payload reaches the native SDK (Android: automatically, from the launch/new intent extras; iOS: when `sendPushNotificationData`/`handlePushNotification` is called — see F-031), and any OneLink URL found at that path is resolved and delivered through the UDL `onDeepLinking` callback (F-037). @@ -38,7 +38,7 @@ The configured path is later consulted when a push payload reaches the native SD |------|------| | `lib/src/appsflyer_sdk.dart` | `addPushNotificationDeepLinkPath(List)` — passes the path array directly as method-channel arguments (no wrapping map) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `addPushNotificationDeepLinkPath(call, result)` — casts arguments to `ArrayList`, converts to `String[]`, forwards to `AppsFlyerLib.getInstance().addPushNotificationDeepLinkPath` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `addPushNotificationDeepLinkPath:result:` — forwards the `NSArray` directly to `[AppsFlyerLib shared]` if non-nil | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `addPushNotificationDeepLinkPath:result:` — forwards the `NSArray` directly to `[AppsFlyerLib shared]` if non-nil | --- diff --git a/internal-docs/features/F-023-in-app-purchase-validation-v1.md b/internal-docs/features/F-023-in-app-purchase-validation-v1.md index 3ca1eb96..34a452e7 100644 --- a/internal-docs/features/F-023-in-app-purchase-validation-v1.md +++ b/internal-docs/features/F-023-in-app-purchase-validation-v1.md @@ -33,9 +33,9 @@ AppsflyerSdk.validateAndLogInAppAndroidPurchase(publicKey, signature, purchaseDa iOS: AppsflyerSdk.validateAndLogInAppIosPurchase(productIdentifier, price, currency, transactionId, additionalParameters) [lib/src/appsflyer_sdk.dart] → _methodChannel.invokeMethod("validateAndLogInAppIosPurchase", {productIdentifier, price, currency, transactionId, additionalParameters}) - → AppsflyerSdkPlugin.handleMethodCall case "validateAndLogInAppIosPurchase" → validateAndLogInAppPurchase:result: [ios/Classes/AppsflyerSdkPlugin.m] + → AppsflyerSdkPlugin.handleMethodCall case "validateAndLogInAppIosPurchase" → validateAndLogInAppPurchase:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared] validateAndLogInAppPurchase:productIdentifier price:currency:transactionId:additionalParameters:success:failure: - → success block → onValidateSuccess: → [_streamHandler sendResponseToFlutter:@"validatePurchase" status:@"success" data:response] [ios/Classes/AppsFlyerStreamHandler.m] + → success block → onValidateSuccess: → [_streamHandler sendResponseToFlutter:@"validatePurchase" status:@"success" data:response] [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m] → failure block → onValidateFail: → [_streamHandler sendResponseToFlutter:@"validatePurchase" status:@"failure" data:errorObject] → result(nil) // Future resolves immediately, same fire-and-forget pattern as Android ``` @@ -47,8 +47,8 @@ AppsflyerSdk.validateAndLogInAppIosPurchase(productIdentifier, price, currency, |------|------| | `lib/src/appsflyer_sdk.dart` | `validateAndLogInAppAndroidPurchase(...)` and `validateAndLogInAppIosPurchase(...)`, both annotated `@Deprecated` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `validateAndLogInAppPurchase(MethodCall, Result)` native handler; calls `registerValidatorListener()` and `AppsFlyerLib.getInstance().validateAndLogInAppPurchase(...)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `validateAndLogInAppPurchase:result:` native handler; calls `[AppsFlyerLib shared] validateAndLogInAppPurchase:...]` with success/failure blocks routed through `onValidateSuccess:`/`onValidateFail:` | -| `ios/Classes/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — forwards the async iOS validation result to Dart over the callback `MethodChannel` (`callbacks`) using `invokeMethod("callListener", ...)`, despite the class name suggesting an `EventChannel` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `validateAndLogInAppPurchase:result:` native handler; calls `[AppsFlyerLib shared] validateAndLogInAppPurchase:...]` with success/failure blocks routed through `onValidateSuccess:`/`onValidateFail:` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — forwards the async iOS validation result to Dart over the callback `MethodChannel` (`callbacks`) using `invokeMethod("callListener", ...)`, despite the class name suggesting an `EventChannel` | --- diff --git a/internal-docs/features/F-024-in-app-purchase-validation-v2.md b/internal-docs/features/F-024-in-app-purchase-validation-v2.md index 1297c104..bfc6ed29 100644 --- a/internal-docs/features/F-024-in-app-purchase-validation-v2.md +++ b/internal-docs/features/F-024-in-app-purchase-validation-v2.md @@ -33,7 +33,7 @@ AppsflyerSdk.validateAndLogInAppPurchaseV2(purchaseDetails, {additionalParameter → AppsFlyerLib.getInstance().validateAndLogInAppPurchase(purchaseDetails, additionalParameters, AppsFlyerInAppPurchaseValidationCallback) → onInAppPurchaseValidationFinished(...) → result.success(flutterResult) → onInAppPurchaseValidationError(...) → result.error("VALIDATION_ERROR", errorMessage, flutterErrorResult) - → iOS: AppsflyerSdkPlugin.handleMethodCall case "validateAndLogInAppPurchaseV2" → validateAndLogInAppPurchaseV2:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall case "validateAndLogInAppPurchaseV2" → validateAndLogInAppPurchaseV2:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → maps purchaseType string to AFSDKPurchaseType, purchaseToken → transactionId → new AFSDKPurchaseDetails(productId, transactionId, purchaseType) → [AppsFlyerLib shared] validateAndLogInAppPurchase:purchaseAdditionalDetails:completion: @@ -49,7 +49,7 @@ AppsflyerSdk.validateAndLogInAppPurchaseV2(purchaseDetails, {additionalParameter | `lib/src/appsflyer_sdk.dart` | `validateAndLogInAppPurchaseV2(AFPurchaseDetails, {additionalParameters})` | | `lib/src/af_purchase_details.dart` | `AFPurchaseDetails` model (`purchaseType`, `purchaseToken`, `productId`) and `AFPurchaseType` enum (`oneTimePurchase`, `subscription`); `toMap()` serializes `purchaseType` to `"one_time_purchase"` / `"subscription"` strings for the channel | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `validateAndLogInAppPurchaseV2(MethodCall, Result)` handler; `mapPurchaseType(String)` translates the Dart string enum to the native `AFPurchaseType` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `validateAndLogInAppPurchaseV2:result:` handler; inline string comparison maps to `AFSDKPurchaseType` (note: `purchaseToken` from Dart is passed as iOS `transactionId`) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `validateAndLogInAppPurchaseV2:result:` handler; inline string comparison maps to `AFSDKPurchaseType` (note: `purchaseToken` from Dart is passed as iOS `transactionId`) | --- diff --git a/internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md b/internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md index cca546f1..a5f9c0ec 100644 --- a/internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md +++ b/internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md @@ -25,7 +25,7 @@ Called by the host app during setup/configuration (typically before or alongside AppsflyerSdk.useReceiptValidationSandbox(bool isSandboxEnabled) [lib/src/appsflyer_sdk.dart] → _methodChannel.invokeMethod("useReceiptValidationSandbox", isSandboxEnabled) → AppsflyerSdkPlugin.handleMethodCall case "useReceiptValidationSandbox" - → useReceiptValidationSandbox:result: [ios/Classes/AppsflyerSdkPlugin.m] + → useReceiptValidationSandbox:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → _isSandboxEnabled = isSandboxEnabled.boolValue → [AppsFlyerLib shared].useReceiptValidationSandbox = _isSandboxEnabled → result(nil) @@ -38,7 +38,7 @@ There is no Android implementation: the method channel argument is only handled | File | Role | |------|------| | `lib/src/appsflyer_sdk.dart` | `useReceiptValidationSandbox(bool isSandboxEnabled)` — sends the raw bool as the method-call argument (not wrapped in a map) | -| `ios/Classes/AppsflyerSdkPlugin.m` | `useReceiptValidationSandbox:result:` (line ~410) — guards with `isKindOfClass:[NSNumber class]`, stores into static `_isSandboxEnabled`, and forwards to `[AppsFlyerLib shared].useReceiptValidationSandbox` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `useReceiptValidationSandbox:result:` (line ~410) — guards with `isKindOfClass:[NSNumber class]`, stores into static `_isSandboxEnabled`, and forwards to `[AppsFlyerLib shared].useReceiptValidationSandbox` | --- diff --git a/internal-docs/features/F-026-additional-custom-data.md b/internal-docs/features/F-026-additional-custom-data.md index 58b90049..6a3be71a 100644 --- a/internal-docs/features/F-026-additional-custom-data.md +++ b/internal-docs/features/F-026-additional-custom-data.md @@ -27,7 +27,7 @@ AppsflyerSdk.setAdditionalData(customData) → Android: AppsflyerSdkPlugin.onMethodCall("setAdditionalData") → setAdditionalData(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setAdditionalData((HashMap) customData) → result.success(null) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setAdditionalData") → setAdditionalData:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setAdditionalData") → setAdditionalData:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setAdditionalData:data] → result(nil) ``` @@ -39,7 +39,7 @@ AppsflyerSdk.setAdditionalData(customData) |------|------| | `lib/src/appsflyer_sdk.dart` | `setAdditionalData(Map? customData)` — platform-agnostic Dart API, `void` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setAdditionalData(MethodCall, Result)` — casts the `customData` argument directly to `HashMap` and forwards to `AppsFlyerLib.getInstance().setAdditionalData(...)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setAdditionalData:result:` — reads `customData` as an `NSDictionary` and forwards to `[[AppsFlyerLib shared] setAdditionalData:]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setAdditionalData:result:` — reads `customData` as an `NSDictionary` and forwards to `[[AppsFlyerLib shared] setAdditionalData:]` | | `doc/API.md` | Public documentation for `setAdditionalData` | --- diff --git a/internal-docs/features/F-027-user-invite-link-generation-onelink.md b/internal-docs/features/F-027-user-invite-link-generation-onelink.md index 93a6fab1..7c3a1d75 100644 --- a/internal-docs/features/F-027-user-invite-link-generation-onelink.md +++ b/internal-docs/features/F-027-user-invite-link-generation-onelink.md @@ -30,9 +30,9 @@ AppsflyerSdk.generateInviteLink(params, success, error) → Android: AppsflyerSdkPlugin.onMethodCall("generateInviteLink") → generateInviteLink(call, result) [android/.../AppsflyerSdkPlugin.java] → ShareInviteHelper.generateInviteUrl(mContext) → LinkGenerator.generateLink(mContext, listener) (native AppsFlyer Android SDK) → listener.onResponse(url) / onResponseError(error) → runOnUIThread(...) → mCallbackChannel.invokeMethod("callListener", ...) - → iOS: AppsflyerSdkPlugin.handleMethodCall("generateInviteLink") → generateInviteLink:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("generateInviteLink") → generateInviteLink:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → AppsFlyerShareInviteHelper generateInviteUrlWithLinkGenerator:completionHandler: (native AppsFlyer iOS SDK) - → _streamHandler sendResponseToFlutter:responseID:status:data: [ios/Classes/AppsFlyerStreamHandler.m] + → _streamHandler sendResponseToFlutter:responseID:status:data: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m] → Dart: callbacks.dart _methodCallHandler("callListener") → _callbacksById["generateInviteLinkSuccess"/"generateInviteLinkFailure"](data) [lib/src/callbacks.dart] ``` @@ -45,8 +45,8 @@ AppsflyerSdk.generateInviteLink(params, success, error) | `lib/src/appsflyer_sdk.dart` | `generateInviteLink()` (public API) and `_translateInviteLinkParamsToMap()` — builds the method-channel payload and registers the two callbacks | | `lib/src/callbacks.dart` | `startListening()` registers the success/failure callback IDs; `_methodCallHandler` dispatches `"callListener"` invocations back to the registered Dart callback | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `generateInviteLink(call, result)` — maps arguments onto `LinkGenerator`, invokes the native `ShareInviteHelper`, and forwards the async result via `runOnUIThread` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `generateInviteLink:result:` — same mapping onto `AppsFlyerLinkGenerator`, using `AppsFlyerShareInviteHelper` | -| `ios/Classes/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — JSON-encodes the callback payload and invokes `"callListener"` on the callback channel | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `generateInviteLink:result:` — same mapping onto `AppsFlyerLinkGenerator`, using `AppsFlyerShareInviteHelper` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — JSON-encodes the callback payload and invokes `"callListener"` on the callback channel | --- diff --git a/internal-docs/features/F-028-app-invite-onelink-id-configuration.md b/internal-docs/features/F-028-app-invite-onelink-id-configuration.md index 051d4ca3..64312d70 100644 --- a/internal-docs/features/F-028-app-invite-onelink-id-configuration.md +++ b/internal-docs/features/F-028-app-invite-onelink-id-configuration.md @@ -27,7 +27,7 @@ AppsflyerSdk.setAppInviteOneLinkID(oneLinkID, callback) → _methodChannel.invokeMethod("setAppInviteOneLinkID", {'oneLinkID': oneLinkID}) → Android: AppsflyerSdkPlugin.onMethodCall("setAppInviteOneLinkID") → setAppInivteOneLinkID(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setAppInviteOneLink(oneLinkId) → runOnUIThread(..., "setAppInviteOneLinkIDCallback", AF_SUCCESS) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setAppInviteOneLinkID") → setAppInviteOneLinkID:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setAppInviteOneLinkID") → setAppInviteOneLinkID:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared].appInviteOneLinkID = oneLinkID → _streamHandler sendResponseToFlutter:... → Dart: callbacks.dart _methodCallHandler("callListener") → _callbacksById["setAppInviteOneLinkIDCallback"](data) [lib/src/callbacks.dart] ``` @@ -40,7 +40,7 @@ AppsflyerSdk.setAppInviteOneLinkID(oneLinkID, callback) | `lib/src/appsflyer_sdk.dart` | `setAppInviteOneLinkID(String, Function)` — public API; registers the callback and invokes the method channel | | `lib/src/callbacks.dart` | `startListening()` / `_methodCallHandler` — generic callback-channel plumbing shared with other async APIs | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setAppInivteOneLinkID(call, result)` (note the native method's typo — "Inivte") — forwards to `AppsFlyerLib.getInstance().setAppInviteOneLink(oneLinkId)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setAppInviteOneLinkID:result:` — sets `[AppsFlyerLib shared].appInviteOneLinkID` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setAppInviteOneLinkID:result:` — sets `[AppsFlyerLib shared].appInviteOneLinkID` | --- diff --git a/internal-docs/features/F-029-cross-promotion-impression-click-tracking.md b/internal-docs/features/F-029-cross-promotion-impression-click-tracking.md index 1259af17..990bcfb8 100644 --- a/internal-docs/features/F-029-cross-promotion-impression-click-tracking.md +++ b/internal-docs/features/F-029-cross-promotion-impression-click-tracking.md @@ -27,14 +27,14 @@ AppsflyerSdk.logCrossPromotionImpression(appId, campaign, data) → _methodChannel.invokeMethod("logCrossPromotionImpression", {...}) → Android: AppsflyerSdkPlugin.onMethodCall("logCrossPromotionImpression") → logCrossPromotionImpression(call, result) [android/.../AppsflyerSdkPlugin.java] → CrossPromotionHelper.logCrossPromoteImpression(mContext, appId, campaign, data) → result.success(null) (native AppsFlyer Android SDK) - → iOS: AppsflyerSdkPlugin.handleMethodCall("logCrossPromotionImpression") → logCrossPromotionImpression:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("logCrossPromotionImpression") → logCrossPromotionImpression:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerCrossPromotionHelper logCrossPromoteImpression:appId campaign:campaign parameters:parameters] (native AppsFlyer iOS SDK) AppsflyerSdk.logCrossPromotionAndOpenStore(appId, campaign, params) [lib/src/appsflyer_sdk.dart] → _methodChannel.invokeMethod("logCrossPromotionAndOpenStore", {...}) → Android: AppsflyerSdkPlugin.onMethodCall("logCrossPromotionAndOpenStore") → logCrossPromotionAndOpenStore(call, result) [android/.../AppsflyerSdkPlugin.java] → CrossPromotionHelper.logAndOpenStore(mContext, appId, campaign, data) → result.success(null) (native AppsFlyer Android SDK) - → iOS: AppsflyerSdkPlugin.handleMethodCall("logCrossPromotionAndOpenStore") → logCrossPromotionAndOpenStore:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("logCrossPromotionAndOpenStore") → logCrossPromotionAndOpenStore:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → AppsFlyerShareInviteHelper generateInviteUrlWithLinkGenerator:completionHandler: → [[UIApplication sharedApplication] openURL:...] (see Known Limitations) ``` @@ -45,7 +45,7 @@ AppsflyerSdk.logCrossPromotionAndOpenStore(appId, campaign, params) |------|------| | `lib/src/appsflyer_sdk.dart` | `logCrossPromotionImpression()` and `logCrossPromotionAndOpenStore()` — public API, both `void`/fire-and-forget | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `logCrossPromotionImpression(call, result)` and `logCrossPromotionAndOpenStore(call, result)` — forward to native `CrossPromotionHelper`, guarded by a non-empty `appId` check, always call `result.success(null)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `logCrossPromotionImpression:result:` and `logCrossPromotionAndOpenStore:result:` — see Known Limitations for behavioral divergence from Android | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `logCrossPromotionImpression:result:` and `logCrossPromotionAndOpenStore:result:` — see Known Limitations for behavioral divergence from Android | --- @@ -63,7 +63,7 @@ AppsflyerSdk.logCrossPromotionAndOpenStore(appId, campaign, params) --- ## Known Limitations -- **iOS `logCrossPromotionImpression:result:` and `logCrossPromotionAndOpenStore:result:` never call `result(...)`**: unlike every other handler in `ios/Classes/AppsflyerSdkPlugin.m`, these two methods have no `result(nil)` (or any `result` call) at the end. The Dart-side `Future` returned by `_methodChannel.invokeMethod` for these calls is therefore never resolved on iOS — callers awaiting it (if any were added later) would hang indefinitely; today both Dart methods are `void` and don't await, so this is currently silent but latent. +- **iOS `logCrossPromotionImpression:result:` and `logCrossPromotionAndOpenStore:result:` never call `result(...)`**: unlike every other handler in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`, these two methods have no `result(nil)` (or any `result` call) at the end. The Dart-side `Future` returned by `_methodChannel.invokeMethod` for these calls is therefore never resolved on iOS — callers awaiting it (if any were added later) would hang indefinitely; today both Dart methods are `void` and don't await, so this is currently silent but latent. - **iOS `logCrossPromotionAndOpenStore:result:` does not use the native cross-promotion "open store" API at all**: instead of calling an equivalent to Android's `CrossPromotionHelper.logAndOpenStore`, it generates a plain invite link via `AppsFlyerShareInviteHelper generateInviteUrlWithLinkGenerator:` (setting only `campaign` and custom params — `appId` is read from `call.arguments` on Android but is **never read** on iOS) and then opens that URL with `UIApplication openURL:options:completionHandler:`. This means the promoted app's ID is not passed to the underlying attribution call on iOS, unlike Android. - Android's `logCrossPromotionImpression`/`logCrossPromotionAndOpenStore` silently skip the native call entirely (but still return success) if `appId` is `null` or `""`. diff --git a/internal-docs/features/F-030-custom-branded-onelink-domains.md b/internal-docs/features/F-030-custom-branded-onelink-domains.md index a7dec9ac..6723dd5b 100644 --- a/internal-docs/features/F-030-custom-branded-onelink-domains.md +++ b/internal-docs/features/F-030-custom-branded-onelink-domains.md @@ -26,7 +26,7 @@ AppsflyerSdk.setOneLinkCustomDomain(brandDomains) → _methodChannel.invokeMethod("setOneLinkCustomDomain", brandDomains) → Android: AppsflyerSdkPlugin.onMethodCall("setOneLinkCustomDomain") → setOneLinkCustomDomain(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setOneLinkCustomDomain(brandDomainsArray) → result.success(null) - → iOS: AppsflyerSdkPlugin.handleMethodCall("setOneLinkCustomDomain") → setOneLinkCustomDomain:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setOneLinkCustomDomain") → setOneLinkCustomDomain:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setOneLinkCustomDomains:brandDomains] → result(nil) ``` @@ -37,7 +37,7 @@ AppsflyerSdk.setOneLinkCustomDomain(brandDomains) |------|------| | `lib/src/appsflyer_sdk.dart` | `setOneLinkCustomDomain(List)` — public API, passes the list directly as the method-channel arguments (no wrapping map) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setOneLinkCustomDomain(call, result)` — casts `call.arguments` to `ArrayList`, converts to `String[]`, forwards to `AppsFlyerLib.getInstance().setOneLinkCustomDomain(...)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setOneLinkCustomDomain:result:` — forwards `call.arguments` directly to `[AppsFlyerLib shared] setOneLinkCustomDomains:]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setOneLinkCustomDomain:result:` — forwards `call.arguments` directly to `[AppsFlyerLib shared] setOneLinkCustomDomains:]` | --- diff --git a/internal-docs/features/F-031-push-notification-data-handling.md b/internal-docs/features/F-031-push-notification-data-handling.md index ab918c87..01d10958 100644 --- a/internal-docs/features/F-031-push-notification-data-handling.md +++ b/internal-docs/features/F-031-push-notification-data-handling.md @@ -28,7 +28,7 @@ AppsflyerSdk.sendPushNotificationData(Map? userInfo) → jsonToBundle(pushPayload) → Bundle → activity.getIntent().putExtras(bundle); activity.setIntent(intent) → AppsFlyerLib.getInstance().sendPushNotificationData(activity) - → iOS: AppsflyerSdkPlugin.handleMethodCall("sendPushNotificationData") → sendPushNotificationData:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("sendPushNotificationData") → sendPushNotificationData:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] handlePushNotification:userInfo] AppsflyerSdk.setPushNotification(bool isEnabled) [DEPRECATED, use sendPushNotificationData instead] @@ -44,7 +44,7 @@ AppsflyerSdk.setPushNotification(bool isEnabled) [DEPRECATED, use sendPushNoti |------|------| | `lib/src/appsflyer_sdk.dart` | `sendPushNotificationData(Map?)` (active) and `setPushNotification(bool)` (`@Deprecated`) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `sendPushNotificationData` — converts the JSON payload to a `Bundle` via `jsonToBundle`, stuffs it into the current activity's intent extras, then calls `AppsFlyerLib.getInstance().sendPushNotificationData(activity)`; `setPushNotification` — ignores its boolean argument and just re-invokes `sendPushNotificationData(activity)` with whatever extras are already on the intent | -| `ios/Classes/AppsflyerSdkPlugin.m` | `sendPushNotificationData:result:` — passes `userInfo` straight to `[AppsFlyerLib shared] handlePushNotification:]`; `setPushNotification:result:` — stores an unused static flag | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `sendPushNotificationData:result:` — passes `userInfo` straight to `[AppsFlyerLib shared] handlePushNotification:]`; `setPushNotification:result:` — stores an unused static flag | --- diff --git a/internal-docs/features/F-032-facebook-deferred-app-links.md b/internal-docs/features/F-032-facebook-deferred-app-links.md index 811ab6ee..cbee6a4f 100644 --- a/internal-docs/features/F-032-facebook-deferred-app-links.md +++ b/internal-docs/features/F-032-facebook-deferred-app-links.md @@ -26,7 +26,7 @@ AppsflyerSdk.enableFacebookDeferredApplinks(bool isEnabled) → _methodChannel.invokeMethod("enableFacebookDeferredApplinks", {'isFacebookDeferredApplinksEnabled': isEnabled}) → Android: AppsflyerSdkPlugin.onMethodCall("enableFacebookDeferredApplinks") → enableFacebookDeferredApplinks(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().enableFacebookDeferredApplinks(true|false) - → iOS: AppsflyerSdkPlugin.handleMethodCall("enableFacebookDeferredApplinks") → enableFacebookDeferredApplinks:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("enableFacebookDeferredApplinks") → enableFacebookDeferredApplinks:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → only if isEnabled == true: [[AppsFlyerLib shared] enableFacebookDeferredApplinksWithClass:NSClassFromString(@"FBSDKAppLinkUtility")] ``` @@ -37,7 +37,7 @@ AppsflyerSdk.enableFacebookDeferredApplinks(bool isEnabled) |------|------| | `lib/src/appsflyer_sdk.dart` | `enableFacebookDeferredApplinks(bool)` — wraps the flag in `{'isFacebookDeferredApplinksEnabled': isEnabled}` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `enableFacebookDeferredApplinks(call, result)` — explicitly calls the native API with either `true` or `false` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `enableFacebookDeferredApplinks:result:` — only calls the native enabling API when `isEnabled == true`; a `false` value is a no-op | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `enableFacebookDeferredApplinks:result:` — only calls the native enabling API when `isEnabled == true`; a `false` value is a no-op | --- diff --git a/internal-docs/features/F-033-skadnetwork-opt-out.md b/internal-docs/features/F-033-skadnetwork-opt-out.md index e718edab..afd797d7 100644 --- a/internal-docs/features/F-033-skadnetwork-opt-out.md +++ b/internal-docs/features/F-033-skadnetwork-opt-out.md @@ -24,8 +24,8 @@ Called by the host app during startup configuration, before `AppsFlyerLib` start ``` AppsflyerSdk.disableSKAdNetwork(isEnabled) [lib/src/appsflyer_sdk.dart:566] → _methodChannel.invokeMethod("disableSKAdNetwork", isEnabled) - → iOS: AppsflyerSdkPlugin handleMethodCall: case "disableSKAdNetwork" → disableSKAdNetwork:result: [ios/Classes/AppsflyerSdkPlugin.m:153] - → [AppsFlyerLib shared].disableSKAdNetwork = _isSKADEnabled [ios/Classes/AppsflyerSdkPlugin.m:401] + → iOS: AppsflyerSdkPlugin handleMethodCall: case "disableSKAdNetwork" → disableSKAdNetwork:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:153] + → [AppsFlyerLib shared].disableSKAdNetwork = _isSKADEnabled [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:401] ``` No `case "disableSKAdNetwork"` exists in `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java`'s method-call switch — on Android the call falls through to the default branch and returns `MethodNotImplemented`. @@ -35,7 +35,7 @@ No `case "disableSKAdNetwork"` exists in `android/src/main/java/com/appsflyer/ap | File | Role | |------|------| | `lib/src/appsflyer_sdk.dart` | `disableSKAdNetwork(bool)` — platform-agnostic Dart API surface (no `Platform.isIOS` guard) | -| `ios/Classes/AppsflyerSdkPlugin.m` | `disableSKAdNetwork:result:` native handler, sets `[AppsFlyerLib shared].disableSKAdNetwork` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `disableSKAdNetwork:result:` native handler, sets `[AppsFlyerLib shared].disableSKAdNetwork` | --- diff --git a/internal-docs/features/F-034-advertising-identifier-collection-disable.md b/internal-docs/features/F-034-advertising-identifier-collection-disable.md index 91554862..53e80cfd 100644 --- a/internal-docs/features/F-034-advertising-identifier-collection-disable.md +++ b/internal-docs/features/F-034-advertising-identifier-collection-disable.md @@ -28,7 +28,7 @@ AppsflyerSdk._validateAFOptions / _validateMapOptions [lib/src/a → _methodChannel.invokeMethod("initSdk", validatedOptions) → Android: AppsflyerSdkPlugin.initSdk(call, result) [android/.../AppsflyerSdkPlugin.java] → if (advertiserIdDisabled) instance.setDisableAdvertisingIdentifiers(true) [only applies `true`; never explicitly re-enables] - → iOS: AppsflyerSdkPlugin.initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → resolves selector `setDisableAdvertisingIdentifier:` via objc_msgSend runtime dispatch, only if disableAdvertisingIdentifier == true # Runtime path @@ -36,7 +36,7 @@ AppsflyerSdk.setDisableAdvertisingIdentifiers(isEnabled) [lib/src/ → _methodChannel.invokeMethod("setDisableAdvertisingIdentifiers", isEnabled) → Android: AppsflyerSdkPlugin.onMethodCall("setDisableAdvertisingIdentifiers") → setDisableAdvertisingIdentifiers(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setDisableAdvertisingIdentifiers(isEnabled) [handles both true and false explicitly] - → iOS: AppsflyerSdkPlugin.handleMethodCall("setDisableAdvertisingIdentifiers") → setDisableAdvertisingIdentifiers:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("setDisableAdvertisingIdentifiers") → setDisableAdvertisingIdentifiers:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [AppsFlyerLib shared] setDisableAdvertisingIdentifier:_isAdvertiserIdEnabled] ``` @@ -49,7 +49,7 @@ AppsflyerSdk.setDisableAdvertisingIdentifiers(isEnabled) [lib/src/ | `lib/src/appsflyer_options.dart` | `disableAdvertisingIdentifier` field on `AppsFlyerOptions` | | `lib/src/appsflyer_constants.dart` | `DISABLE_ADVERTISING_IDENTIFIER` string key | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `initSdk` (init-time, line 1072), `setDisableAdvertisingIdentifiers(call, result)` (runtime, line 564) | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` (init-time, uses `objc_msgSend` runtime dispatch to `setDisableAdvertisingIdentifier:`, line ~841-855), `setDisableAdvertisingIdentifiers:result:` (runtime, line 380) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` (init-time, uses `objc_msgSend` runtime dispatch to `setDisableAdvertisingIdentifier:`, line ~841-855), `setDisableAdvertisingIdentifiers:result:` (runtime, line 380) | | `doc/BasicIntegration.md` | Documents the field as "Opt-out of the collection of Advertising Identifiers, which include OAID, AAID, GAID and IDFA." | --- diff --git a/internal-docs/features/F-035-conversion-data-callback.md b/internal-docs/features/F-035-conversion-data-callback.md index f5372ae3..91d849aa 100644 --- a/internal-docs/features/F-035-conversion-data-callback.md +++ b/internal-docs/features/F-035-conversion-data-callback.md @@ -26,13 +26,13 @@ AppsflyerSdk.initSdk(registerConversionDataCallback: true, ...) → validatedOptions[AF_GCD] = registerConversionDataCallback || registerOnAppOpenAttributionCallback → _methodChannel.invokeMethod("initSdk", validatedOptions) → Android: initSdk(call, result) → if (getGCD) gcdListener = afConversionListener; instance.init(afDevKey, gcdListener, mContext) [android/.../AppsflyerSdkPlugin.java] - → iOS: initSdkWithCall:result: → if (isConversionData) [[AppsFlyerLib shared] setDelegate:_streamHandler] [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: initSdkWithCall:result: → if (isConversionData) [[AppsFlyerLib shared] setDelegate:_streamHandler] [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] AppsflyerSdk.onInstallConversionData(Function callback) [lib/src/appsflyer_sdk.dart] → startListening(callback, "onInstallConversionData") [lib/src/callbacks.dart] → _channel(AF_CALLBACK_CHANNEL).invokeMethod("startListening", "onInstallConversionData") → Android: startListening(...) → gcdCallback = true (when callbackName == AF_GCD_CALLBACK == "onInstallConversionData") [android/.../AppsflyerSdkPlugin.java] - → iOS: startListening:result: → _gcdCallback = true (when callbackId == afGCDCallback == "onInstallConversionData") [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: startListening:result: → _gcdCallback = true (when callbackId == afGCDCallback == "onInstallConversionData") [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] Native SDK conversion data arrives: Android: afConversionListener.onConversionDataSuccess(map) / onConversionDataFail(s) @@ -50,8 +50,8 @@ Native SDK conversion data arrives: | `lib/src/appsflyer_sdk.dart` | `onInstallConversionData(Function)` — registers the Dart callback via `startListening` | | `lib/src/callbacks.dart` | `_methodCallHandler` — decodes the `callListener` JSON envelope and dispatches `{"status", "payload"}` to the registered `"onInstallConversionData"` callback | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `afConversionListener.onConversionDataSuccess/onConversionDataFail` — native `AppsFlyerConversionListener` implementation; `initSdk` registers it with `AppsFlyerLib.getInstance().init(...)` only when `AF_GCD` is true; also caches results (`cachedOnConversionDataSuccess`/`cachedOnConversionDataFail`) across activity detach/reattach (`RD-65582`) | -| `ios/Classes/AppsFlyerStreamHandler.m` | `onConversionDataSuccess:`/`onConversionDataFail:` — `AppsFlyerLibDelegate` implementation, gated by `[AppsflyerSdkPlugin gcdCallback]` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `_streamHandler` as the `AppsFlyerLib` delegate only if the `GCD` flag is true | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `onConversionDataSuccess:`/`onConversionDataFail:` — `AppsFlyerLibDelegate` implementation, gated by `[AppsflyerSdkPlugin gcdCallback]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `_streamHandler` as the `AppsFlyerLib` delegate only if the `GCD` flag is true | --- diff --git a/internal-docs/features/F-036-app-open-attribution-callback.md b/internal-docs/features/F-036-app-open-attribution-callback.md index a7589714..37590cf6 100644 --- a/internal-docs/features/F-036-app-open-attribution-callback.md +++ b/internal-docs/features/F-036-app-open-attribution-callback.md @@ -26,13 +26,13 @@ AppsflyerSdk.initSdk(registerOnAppOpenAttributionCallback: true, ...) → validatedOptions[AF_GCD] = registerConversionDataCallback || registerOnAppOpenAttributionCallback → _methodChannel.invokeMethod("initSdk", validatedOptions) → Android: initSdk(call, result) → if (getGCD) gcdListener = afConversionListener; instance.init(afDevKey, gcdListener, mContext) [android/.../AppsflyerSdkPlugin.java] - → iOS: initSdkWithCall:result: → if (isConversionData) [[AppsFlyerLib shared] setDelegate:_streamHandler] [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: initSdkWithCall:result: → if (isConversionData) [[AppsFlyerLib shared] setDelegate:_streamHandler] [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] AppsflyerSdk.onAppOpenAttribution(Function callback) [lib/src/appsflyer_sdk.dart] → startListening(callback, "onAppOpenAttribution") [lib/src/callbacks.dart] → _channel(AF_CALLBACK_CHANNEL).invokeMethod("startListening", "onAppOpenAttribution") → Android: startListening(...) → oaoaCallback = true (when callbackName == AF_OAOA_CALLBACK == "onAppOpenAttribution") [android/.../AppsflyerSdkPlugin.java] - → iOS: startListening:result: → _oaoaCallback = true (when callbackId == afOAOACallback == "onAppOpenAttribution") [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: startListening:result: → _oaoaCallback = true (when callbackId == afOAOACallback == "onAppOpenAttribution") [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] Native SDK app-open attribution arrives: Android: afConversionListener.onAppOpenAttribution(map) / onAttributionFailure(errorMessage) @@ -50,8 +50,8 @@ Native SDK app-open attribution arrives: | `lib/src/appsflyer_sdk.dart` | `onAppOpenAttribution(Function)` — registers the Dart callback via `startListening` | | `lib/src/callbacks.dart` | `_methodCallHandler` — decodes the `callListener` JSON envelope and dispatches `{"status", "payload"}` to the registered `"onAppOpenAttribution"` callback | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `afConversionListener.onAppOpenAttribution/onAttributionFailure` — native `AppsFlyerConversionListener` methods, gated by `oaoaCallback`; also cached across activity detach/reattach (`cachedOnAppOpenAttribution`/`cachedOnAttributionFailure`, `RD-65582`) | -| `ios/Classes/AppsFlyerStreamHandler.m` | `onAppOpenAttribution:`/`onAppOpenAttributionFailure:` — `AppsFlyerLibDelegate` methods, gated by `[AppsflyerSdkPlugin oaoaCallback]` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `_streamHandler` as the `AppsFlyerLib` delegate only if the `GCD` flag is true (shared with F-035) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `onAppOpenAttribution:`/`onAppOpenAttributionFailure:` — `AppsFlyerLibDelegate` methods, gated by `[AppsflyerSdkPlugin oaoaCallback]` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `_streamHandler` as the `AppsFlyerLib` delegate only if the `GCD` flag is true (shared with F-035) | --- diff --git a/internal-docs/features/F-037-unified-deep-linking-callback-and-models.md b/internal-docs/features/F-037-unified-deep-linking-callback-and-models.md index 3ee6e5bf..25c94177 100644 --- a/internal-docs/features/F-037-unified-deep-linking-callback-and-models.md +++ b/internal-docs/features/F-037-unified-deep-linking-callback-and-models.md @@ -26,19 +26,19 @@ AppsflyerSdk.initSdk(registerOnDeepLinkingCallback: true, ...) → validatedOptions[AF_UDL] = registerOnDeepLinkingCallback → _methodChannel.invokeMethod("initSdk", validatedOptions) → Android: initSdk(call, result) → if (getUdl) instance.subscribeForDeepLink(afDeepLinkListener) [android/.../AppsflyerSdkPlugin.java] - → iOS: initSdkWithCall:result: → if (isUDP) [AppsFlyerLib shared].deepLinkDelegate = _streamHandler [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: initSdkWithCall:result: → if (isUDP) [AppsFlyerLib shared].deepLinkDelegate = _streamHandler [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] AppsflyerSdk.onDeepLinking(Function(DeepLinkResult) callback) [lib/src/appsflyer_sdk.dart] → startListeningToUDL(callback, "onDeepLinking") [lib/src/callbacks.dart] → _channel(AF_CALLBACK_CHANNEL).invokeMethod("startListening", "onDeepLinking") → Android: startListening(...) → udlCallback = true (when callbackName == AF_UDL_CALLBACK == "onDeepLinking") [android/.../AppsflyerSdkPlugin.java] - → iOS: startListening:result: → _udpCallback = true (when callbackId == afUDPCallback == "onDeepLinking") [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: startListening:result: → _udpCallback = true (when callbackId == afUDPCallback == "onDeepLinking") [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] Native deep link resolved (via F-039 iOS entry points / F-040 Android onNewIntent, or SDK-internal resume/link-resolution): Android: afDeepLinkListener.onDeepLinking(DeepLinkResult) [com.appsflyer.deeplink.DeepLinkResult, native SDK type] → if (udlCallback) runOnUIThread(deepLinkResult, AF_UDL_CALLBACK, AF_SUCCESS) → args {"id", "deepLinkStatus", "deepLinkError"?, "deepLinkObj"? } → mCallbackChannel.invokeMethod("callListener", jsonArgs) - iOS: AppsFlyerStreamHandler.didResolveDeepLink: (AppsFlyerDeepLinkDelegate) [ios/Classes/AppsFlyerStreamHandler.m] + iOS: AppsFlyerStreamHandler.didResolveDeepLink: (AppsFlyerDeepLinkDelegate) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m] → if ([AppsflyerSdkPlugin udpCallback]) build {"id", "deepLinkStatus", "deepLinkError"?, "deepLinkObj"?} → AppsflyerSdkPlugin.callbackChannel invokeMethod:"callListener" Dart: _methodCallHandler(call) [lib/src/callbacks.dart] → callMap["id"] == "onDeepLinking" → error = callMap["deepLinkError"]?.errorFromString() @@ -57,9 +57,9 @@ Native deep link resolved (via F-039 iOS entry points / F-040 Android onNewInten | `lib/src/udl/deeplink.dart` | `DeepLink` — typed accessors (`deepLinkValue`, `matchType`, `mediaSource`, `campaign`, `afSub1..5`, `isDeferred`, etc.) over the raw click-event map | | `lib/src/udl/deep_link_result.dart` | `DeepLinkResult`, `Status` (`FOUND`/`NOT_FOUND`/`ERROR`/`PARSE_ERROR`), `Error` (`TIMEOUT`/`NETWORK`/`HTTP_STATUS_CODE`/`UNEXPECTED`/`DEVELOPER_ERROR`) enums and string-conversion extensions used to decode the wire payload | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `afDeepLinkListener` (`com.appsflyer.deeplink.DeepLinkListener`) — registered via `AppsFlyerLib.getInstance().subscribeForDeepLink(...)` only when `AF_UDL` is true; `runOnUIThread` serializes `DeepLinkResult` into the `deepLinkStatus`/`deepLinkError`/`deepLinkObj` JSON shape; caches `cachedDeepLinkResult` across activity detach/reattach (`RD-65582`) | -| `ios/Classes/AppsFlyerStreamHandler.m` | `didResolveDeepLink:` (`AppsFlyerDeepLinkDelegate`) — gated by `[AppsflyerSdkPlugin udpCallback]`; builds the same JSON shape as Android | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` sets `[AppsFlyerLib shared].deepLinkDelegate = _streamHandler` only if the `UDL` flag is true; `startListening:` flips the internal `_udpCallback` flag when `callbackId == afUDPCallback` | -| `ios/Classes/AppsflyerSdkPlugin.h` | Defines `afUDL` (`"UDL"`), `afUDPCallback` (`"onDeepLinking"`) — note the `udpCallback`/`_udpCallback` naming (likely a "UDL"→"UDP" typo) used throughout the iOS plugin for this feature | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `didResolveDeepLink:` (`AppsFlyerDeepLinkDelegate`) — gated by `[AppsflyerSdkPlugin udpCallback]`; builds the same JSON shape as Android | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` sets `[AppsFlyerLib shared].deepLinkDelegate = _streamHandler` only if the `UDL` flag is true; `startListening:` flips the internal `_udpCallback` flag when `callbackId == afUDPCallback` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | Defines `afUDL` (`"UDL"`), `afUDPCallback` (`"onDeepLinking"`) — note the `udpCallback`/`_udpCallback` naming (likely a "UDL"→"UDP" typo) used throughout the iOS plugin for this feature | --- diff --git a/internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md b/internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md index 16e4b255..222f6d9c 100644 --- a/internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md +++ b/internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md @@ -42,7 +42,7 @@ AppsFlyerInAppPurchaseValidatorListener (registered by registerValidatorListener → the app's registered callback runs Delivery (iOS): -[AppsFlyerLib shared] validateAndLogInAppPurchase:...success:/failure: (F-023's V1 flow) [ios/Classes/AppsflyerSdkPlugin.m] +[AppsFlyerLib shared] validateAndLogInAppPurchase:...success:/failure: (F-023's V1 flow) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → onValidateSuccess:/onValidateFail: → [_streamHandler sendResponseToFlutter:afValidatePurchase(@"validatePurchase") status:... data:...] [AppsFlyerStreamHandler.m] → Dart: same _methodCallHandler case 'callListener' → case "validatePurchase" path as Android @@ -58,9 +58,9 @@ Delivery (iOS): | `lib/src/appsflyer_constants.dart` | `AF_VALIDATE_PURCHASE = "validatePurchase"` — the shared event id constant | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `startListening(Object, Result)` sets `validatePurchaseCallback = true`; `registerValidatorListener()` builds the `AppsFlyerInAppPurchaseValidatorListener` whose `onValidateInApp()`/`onValidateInAppFailure(String)` gate on that flag and call `runOnUIThread(...)` to push the event to Dart over the `"callbacks"` (`mCallbackChannel`) `MethodChannel` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java` | `AF_VALIDATE_PURCHASE = "validatePurchase"` — native-side mirror of the Dart constant | -| `ios/Classes/AppsflyerSdkPlugin.m` | `onValidateSuccess:`/`onValidateFail:` (fed by F-023's `validateAndLogInAppPurchase:result:`) call `[_streamHandler sendResponseToFlutter:afValidatePurchase ...]` to forward the result | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define afValidatePurchase @"validatePurchase"` — iOS-side mirror of the same event id | -| `ios/Classes/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — forwards the result to Dart via `invokeMethod("callListener", ...)` on the callback channel (same channel/protocol Android uses) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `onValidateSuccess:`/`onValidateFail:` (fed by F-023's `validateAndLogInAppPurchase:result:`) call `[_streamHandler sendResponseToFlutter:afValidatePurchase ...]` to forward the result | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define afValidatePurchase @"validatePurchase"` — iOS-side mirror of the same event id | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m` | `sendResponseToFlutter:status:data:` — forwards the result to Dart via `invokeMethod("callListener", ...)` on the callback channel (same channel/protocol Android uses) | --- diff --git a/internal-docs/features/F-039-native-ios-deep-link-entry-points.md b/internal-docs/features/F-039-native-ios-deep-link-entry-points.md index 8f0cd2d5..5904492a 100644 --- a/internal-docs/features/F-039-native-ios-deep-link-entry-points.md +++ b/internal-docs/features/F-039-native-ios-deep-link-entry-points.md @@ -23,8 +23,8 @@ Fires whenever iOS launches or resumes the app via a deep link: URI-scheme opens ## Call Chain ``` iOS OS-level deep-link delivery (app already running or resuming): - application:openURL:options: (iOS 9+) [ios/Classes/AppsflyerSdkPlugin.m] - → [[AppsFlyerAttribution shared] handleOpenUrl:url options:options] [ios/Classes/AppsFlyerAttribution.m] + application:openURL:options: (iOS 9+) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] + → [[AppsFlyerAttribution shared] handleOpenUrl:url options:options] [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m] application:openURL:sourceApplication:annotation: (iOS 8 and below) → [[AppsFlyerAttribution shared] handleOpenUrl:url sourceApplication:annotation:] application:continueUserActivity:restorationHandler: (Universal Links) @@ -37,12 +37,12 @@ iOS UIScene-based delivery (Flutter 3.41+ UIScene migration, iOS 13+, only compi → for each userActivity of type NSUserActivityTypeBrowsingWeb → continueUserActivity:restorationHandler:nil scene:continueUserActivity: → [[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:nil] -AppsFlyerAttribution (buffering singleton, isBridgeReady initially NO) [ios/Classes/AppsFlyerAttribution.m] +AppsFlyerAttribution (buffering singleton, isBridgeReady initially NO) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m] handleOpenUrl:.../continueUserActivity:... → if isBridgeReady == YES: forward immediately to [AppsFlyerLib shared] handleOpenUrl:/continueUserActivity: → else: buffer url/options/sourceApplication/annotation/userActivity/restorationHandler on self -AppsflyerSdkPlugin initSdkWithCall:result: (Dart called initSdk → method channel → native init) [ios/Classes/AppsflyerSdkPlugin.m] +AppsflyerSdkPlugin initSdkWithCall:result: (Dart called initSdk → method channel → native init) [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → ... [AppsFlyerLib shared] init/start ... → [AppsFlyerAttribution shared].isBridgeReady = YES → [[NSNotificationCenter defaultCenter] postNotificationName:AF_BRIDGE_SET object:self] @@ -56,10 +56,10 @@ AppsflyerSdkPlugin initSdkWithCall:result: (Dart called initSdk → method chann ## Files | File | Role | |------|------| -| `ios/Classes/AppsflyerSdkPlugin.m` | `application:openURL:options:`, `application:openURL:sourceApplication:annotation:`, `application:continueUserActivity:restorationHandler:`, and (behind `FlutterSceneLifeCycle.h` availability) `scene:openURLContexts:`, `scene:willConnectToSession:options:`, `scene:continueUserActivity:` — all OS/Scene entry points, each forwarding into `AppsFlyerAttribution`; `initSdkWithCall:result:` sets `isBridgeReady = YES` and posts `AF_BRIDGE_SET` once Dart's `initSdk` call reaches native code | -| `ios/Classes/AppsFlyerAttribution.h` | Declares the `AppsFlyerAttribution` singleton interface: buffering properties (`userActivity`, `restorationHandler`, `url`, `options`, `sourceApplication`, `annotation`), `isBridgeReady` flag, and the `AF_BRIDGE_SET` notification name constant | -| `ios/Classes/AppsFlyerAttribution.m` | Singleton implementation — `handleOpenUrl:...`/`continueUserActivity:...` either forward immediately to `AppsFlyerLib` or buffer until `isBridgeReady`; `receiveBridgeReadyNotification:` flushes exactly one buffered event (checked in priority order: sourceApplication+annotation form, then options form, then userActivity form) when notified | -| `ios/Classes/AppsflyerSdkPlugin.h` | `AppsflyerSdkPlugin` class declaration; conditionally conforms to `FlutterSceneLifeCycleDelegate` when available | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `application:openURL:options:`, `application:openURL:sourceApplication:annotation:`, `application:continueUserActivity:restorationHandler:`, and (behind `FlutterSceneLifeCycle.h` availability) `scene:openURLContexts:`, `scene:willConnectToSession:options:`, `scene:continueUserActivity:` — all OS/Scene entry points, each forwarding into `AppsFlyerAttribution`; `initSdkWithCall:result:` sets `isBridgeReady = YES` and posts `AF_BRIDGE_SET` once Dart's `initSdk` call reaches native code | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h` | Declares the `AppsFlyerAttribution` singleton interface: buffering properties (`userActivity`, `restorationHandler`, `url`, `options`, `sourceApplication`, `annotation`), `isBridgeReady` flag, and the `AF_BRIDGE_SET` notification name constant | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m` | Singleton implementation — `handleOpenUrl:...`/`continueUserActivity:...` either forward immediately to `AppsFlyerLib` or buffer until `isBridgeReady`; `receiveBridgeReadyNotification:` flushes exactly one buffered event (checked in priority order: sourceApplication+annotation form, then options form, then userActivity form) when notified | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `AppsflyerSdkPlugin` class declaration; conditionally conforms to `FlutterSceneLifeCycleDelegate` when available | --- diff --git a/internal-docs/features/F-041-current-device-language-override.md b/internal-docs/features/F-041-current-device-language-override.md index 674426bf..a6ad6617 100644 --- a/internal-docs/features/F-041-current-device-language-override.md +++ b/internal-docs/features/F-041-current-device-language-override.md @@ -24,8 +24,8 @@ Called by the host app whenever it needs to explicitly declare (or correct) the ``` AppsflyerSdk.setCurrentDeviceLanguage(language) [lib/src/appsflyer_sdk.dart:597] → _methodChannel.invokeMethod("setCurrentDeviceLanguage", language) - → iOS: AppsflyerSdkPlugin handleMethodCall: case "setCurrentDeviceLanguage" → setCurrentDeviceLanguage:result: [ios/Classes/AppsflyerSdkPlugin.m:155] - → [AppsFlyerLib shared] setCurrentDeviceLanguage: language [ios/Classes/AppsflyerSdkPlugin.m:395] + → iOS: AppsflyerSdkPlugin handleMethodCall: case "setCurrentDeviceLanguage" → setCurrentDeviceLanguage:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:155] + → [AppsFlyerLib shared] setCurrentDeviceLanguage: language [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:395] ``` No `case "setCurrentDeviceLanguage"` exists in `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java`'s method-call switch — on Android the call falls through to the default branch and returns `MethodNotImplemented`. @@ -35,7 +35,7 @@ No `case "setCurrentDeviceLanguage"` exists in `android/src/main/java/com/appsfl | File | Role | |------|------| | `lib/src/appsflyer_sdk.dart` | `setCurrentDeviceLanguage(String)` — platform-agnostic Dart API surface (no `Platform.isIOS` guard) | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setCurrentDeviceLanguage:result:` native handler, forwards to `AppsFlyerLib.shared` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setCurrentDeviceLanguage:result:` native handler, forwards to `AppsFlyerLib.shared` | --- diff --git a/internal-docs/features/F-042-partner-postback-sharing-filter.md b/internal-docs/features/F-042-partner-postback-sharing-filter.md index 0961c8ad..f2330a49 100644 --- a/internal-docs/features/F-042-partner-postback-sharing-filter.md +++ b/internal-docs/features/F-042-partner-postback-sharing-filter.md @@ -26,7 +26,7 @@ AppsflyerSdk.setSharingFilterForPartners(partners) [lib/sr → _methodChannel.invokeMethod("setSharingFilterForPartners", partners) → Android: AppsflyerSdkPlugin.onMethodCall("setSharingFilterForPartners") → setSharingFilterForPartners(call, result) [android/.../AppsflyerSdkPlugin.java:349,555] → AppsFlyerLib.getInstance().setSharingFilterForPartners(partners) (only if call.arguments != null) - → iOS: AppsflyerSdkPlugin handleMethodCall: case "setSharingFilterForPartners" → setSharingFilterForPartners:result: [ios/Classes/AppsflyerSdkPlugin.m:157,389] + → iOS: AppsflyerSdkPlugin handleMethodCall: case "setSharingFilterForPartners" → setSharingFilterForPartners:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:157,389] → [AppsFlyerLib shared] setSharingFilterForPartners: partners AppsflyerSdk.setSharingFilter(partners) [DEPRECATED] [lib/src/appsflyer_sdk.dart:603] @@ -43,7 +43,7 @@ AppsflyerSdk.setSharingFilterForAllPartners() [DEPRECATED] [lib/s |------|------| | `lib/src/appsflyer_sdk.dart` | `setSharingFilterForPartners(List)` (active); `setSharingFilter(List)` and `setSharingFilterForAllPartners()` (`@Deprecated`, both re-route to `setSharingFilterForPartners`) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setSharingFilterForPartners` (active, dispatched via channel), plus dead `setSharingFilter`/`setSharingFilterForAllPartners` channel handlers no longer reachable from the current Dart API | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setSharingFilterForPartners:result:` (active, dispatched via channel), plus dead `setSharingFilter:result:`/`setSharingFilterForAllPartners:` channel handlers no longer reachable from the current Dart API | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setSharingFilterForPartners:result:` (active, dispatched via channel), plus dead `setSharingFilter:result:`/`setSharingFilterForAllPartners:` channel handlers no longer reachable from the current Dart API | --- diff --git a/internal-docs/features/F-043-out-of-store-install-source.md b/internal-docs/features/F-043-out-of-store-install-source.md index cefd00b2..ab259544 100644 --- a/internal-docs/features/F-043-out-of-store-install-source.md +++ b/internal-docs/features/F-043-out-of-store-install-source.md @@ -32,7 +32,7 @@ AppsflyerSdk.getOutOfStore() [lib/sr → Android: AppsflyerSdkPlugin.onMethodCall("getOutOfStore") → getOutOfStore(result) [android/.../AppsflyerSdkPlugin.java:352,526] → result.success(AppsFlyerLib.getInstance().getOutOfStore(this.mContext)) ``` -Neither `"setOutOfStore"` nor `"getOutOfStore"` has a case in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — on iOS both calls fall through to `result(FlutterMethodNotImplemented)`. +Neither `"setOutOfStore"` nor `"getOutOfStore"` has a case in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — on iOS both calls fall through to `result(FlutterMethodNotImplemented)`. --- diff --git a/internal-docs/features/F-044-partner-specific-data.md b/internal-docs/features/F-044-partner-specific-data.md index 6688b9c8..263ba57c 100644 --- a/internal-docs/features/F-044-partner-specific-data.md +++ b/internal-docs/features/F-044-partner-specific-data.md @@ -26,7 +26,7 @@ AppsflyerSdk.setPartnerData(partnerId, partnerData) [lib/sr → _methodChannel.invokeMethod("setPartnerData", {'partnerId': partnerId, 'partnersData': partnerData}) → Android: AppsflyerSdkPlugin.onMethodCall("setPartnerData") → setPartnerData(call, result) [android/.../AppsflyerSdkPlugin.java:358,546] → AppsFlyerLib.getInstance().setPartnerData(partnerId, partnerData) (only if partnerData != null) - → iOS: AppsflyerSdkPlugin handleMethodCall: case "setPartnerData" → setPartnerData:result: [ios/Classes/AppsflyerSdkPlugin.m:161,370] + → iOS: AppsflyerSdkPlugin handleMethodCall: case "setPartnerData" → setPartnerData:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m:161,370] → [AppsFlyerLib shared] setPartnerDataWithPartnerId:partnerId partnerInfo:partnersData ``` @@ -37,7 +37,7 @@ AppsflyerSdk.setPartnerData(partnerId, partnerData) [lib/sr |------|------| | `lib/src/appsflyer_sdk.dart` | `setPartnerData(String partnerId, Map partnerData)` — platform-agnostic Dart API surface | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `setPartnerData` native handler | -| `ios/Classes/AppsflyerSdkPlugin.m` | `setPartnerData:result:` native handler | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `setPartnerData:result:` native handler | --- diff --git a/internal-docs/features/F-045-deep-link-url-resolution-allow-list.md b/internal-docs/features/F-045-deep-link-url-resolution-allow-list.md index e40dcb16..54af80a2 100644 --- a/internal-docs/features/F-045-deep-link-url-resolution-allow-list.md +++ b/internal-docs/features/F-045-deep-link-url-resolution-allow-list.md @@ -28,7 +28,7 @@ AppsflyerSdk.setResolveDeepLinkURLs(List urls) → urls = (ArrayList) call.arguments → urlsArr = urls.toArray(new String[0]) → AppsFlyerLib.getInstance().setResolveDeepLinkURLs(urlsArr) → result.success(null) - → iOS: handleMethodCall: → case "setResolveDeepLinkURLs" → setResolveDeepLinkURLs:call result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: handleMethodCall: → case "setResolveDeepLinkURLs" → setResolveDeepLinkURLs:call result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → urlsArr = call.arguments (NSArray) → if urlsArr != nil: [[AppsFlyerLib shared] setResolveDeepLinkURLs:urlsArr] → result(nil) ``` @@ -40,7 +40,7 @@ AppsflyerSdk.setResolveDeepLinkURLs(List urls) |------|------| | `lib/src/appsflyer_sdk.dart` | `setResolveDeepLinkURLs(List urls)` — thin passthrough invoking the `setResolveDeepLinkURLs` method channel call with the raw URL list | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `onMethodCall` dispatch `case "setResolveDeepLinkURLs"`; `setResolveDeepLinkURLs(MethodCall, Result)` — casts arguments to `ArrayList`, converts to `String[]`, calls `AppsFlyerLib.getInstance().setResolveDeepLinkURLs(urlsArr)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | Method-channel dispatch `case @"setResolveDeepLinkURLs"`; `setResolveDeepLinkURLs:result:` — passes `call.arguments` (an `NSArray`) directly to `[AppsFlyerLib shared] setResolveDeepLinkURLs:]`, guarded only by a nil check | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | Method-channel dispatch `case @"setResolveDeepLinkURLs"`; `setResolveDeepLinkURLs:result:` — passes `call.arguments` (an `NSArray`) directly to `[AppsFlyerLib shared] setResolveDeepLinkURLs:]`, guarded only by a nil check | | `doc/API.md` | Documents the API (`setResolveDeepLinkURLs`) with the wrapped-OneLink rationale and a usage example; does not restrict it to a single platform | --- diff --git a/internal-docs/features/F-046-disable-network-data.md b/internal-docs/features/F-046-disable-network-data.md index 659c435b..c01c26f8 100644 --- a/internal-docs/features/F-046-disable-network-data.md +++ b/internal-docs/features/F-046-disable-network-data.md @@ -27,7 +27,7 @@ AppsflyerSdk.setDisableNetworkData(disable) [lib/src/ → Android: AppsflyerSdkPlugin.onMethodCall("setDisableNetworkData") → setDisableNetworkData(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().setDisableNetworkData(disable) ``` -No iOS branch exists for `"setDisableNetworkData"` in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — the call falls through to `result(FlutterMethodNotImplemented)`. +No iOS branch exists for `"setDisableNetworkData"` in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — the call falls through to `result(FlutterMethodNotImplemented)`. --- diff --git a/internal-docs/features/F-047-appset-id-collection-optout.md b/internal-docs/features/F-047-appset-id-collection-optout.md index 192787e0..a42a6a2b 100644 --- a/internal-docs/features/F-047-appset-id-collection-optout.md +++ b/internal-docs/features/F-047-appset-id-collection-optout.md @@ -27,7 +27,7 @@ AppsflyerSdk.disableAppSetId() [lib/src/ → Android: AppsflyerSdkPlugin.onMethodCall("disableAppSetId") → disableAppSetId(call, result) [android/.../AppsflyerSdkPlugin.java] → AppsFlyerLib.getInstance().disableAppSetId() ``` -No iOS branch exists for `"disableAppSetId"` in `ios/Classes/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — the call falls through to `result(FlutterMethodNotImplemented)`. This is expected: AppSet ID is a Google Play Services / Android-only concept. +No iOS branch exists for `"disableAppSetId"` in `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m`'s `handleMethodCall:` — the call falls through to `result(FlutterMethodNotImplemented)`. This is expected: AppSet ID is a Google Play Services / Android-only concept. --- diff --git a/internal-docs/features/F-048-plugin-metadata-reporting.md b/internal-docs/features/F-048-plugin-metadata-reporting.md index 58b0223b..65cfcf93 100644 --- a/internal-docs/features/F-048-plugin-metadata-reporting.md +++ b/internal-docs/features/F-048-plugin-metadata-reporting.md @@ -28,7 +28,7 @@ AppsflyerSdk.initSdk(...) [lib/src/ → new PluginInfo(Plugin.FLUTTER, AppsFlyerConstants.PLUGIN_VERSION) (line 1095) → AppsFlyerLib.getInstance().setPluginInfo(pluginInfo) (line 1096) → AppsFlyerLib.getInstance().init(afDevKey, gcdListener, mContext) (called right after) - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → [[AppsFlyerLib shared] setPluginInfoWith:AFSDKPluginFlutter pluginVersion:kAppsFlyerPluginVersion additionalParams:nil] (line 857) @@ -42,8 +42,8 @@ AppsflyerSdk.initSdk(...) [lib/src/ |------|------| | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `initSdk(call, result)` — builds `PluginInfo(Plugin.FLUTTER, AppsFlyerConstants.PLUGIN_VERSION)` and calls `setPluginInfo` (lines 1095–1096), immediately before `instance.init(...)` | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java` | `PLUGIN_VERSION = "6.18.0"` — the version string reported to the native SDK | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — calls `setPluginInfoWith:AFSDKPluginFlutter pluginVersion:kAppsFlyerPluginVersion additionalParams:nil` (line 857) | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define kAppsFlyerPluginVersion @"6.18.0"` — the version string reported on iOS | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — calls `setPluginInfoWith:AFSDKPluginFlutter pluginVersion:kAppsFlyerPluginVersion additionalParams:nil` (line 857) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define kAppsFlyerPluginVersion @"6.18.0"` — the version string reported on iOS | `Plugin`, `PluginInfo` (Android, package `com.appsflyer.internal.platform_extension`) and `AFSDKPluginFlutter` (iOS, an enum/constant defined inside the native `AppsFlyerLib` framework) are external types supplied by the native AppsFlyer SDK dependency, not defined in this repo. diff --git a/internal-docs/features/F-054-purchase-connector-build-time-opt-in.md b/internal-docs/features/F-054-purchase-connector-build-time-opt-in.md index a6b7aced..6badbc0e 100644 --- a/internal-docs/features/F-054-purchase-connector-build-time-opt-in.md +++ b/internal-docs/features/F-054-purchase-connector-build-time-opt-in.md @@ -4,8 +4,8 @@ name: "Purchase Connector: Build-Time Opt-in (Android include/exclude variants)" type: purchaseValidation platform: both status: active -last_verified: 2026-07-15 -depends_on: [] +last_verified: 2026-07-19 +depends_on: [F-060] --- ## Business Purpose @@ -16,9 +16,10 @@ The Purchase Connector depends on the Google Play Billing Library (Android) and --- ## Trigger -Not a runtime trigger — this is a build-time decision made once per app project when it configures its Gradle/CocoaPods build: +Not a runtime trigger — this is a build-time decision made once per app project when it configures its Gradle/CocoaPods/SPM build: - **Android**: `android/build.gradle` reads `project.findProperty('appsflyer.enable_purchase_connector')?.toBoolean() ?: false`. The app sets `appsflyer.enable_purchase_connector=true` in its own `gradle.properties`. -- **iOS**: `ios/appsflyer_sdk.podspec` checks `if defined?($AppsFlyerPurchaseConnector)`. The app sets `$AppsFlyerPurchaseConnector = true` in its own `Podfile` before running `pod install`. +- **iOS, CocoaPods**: `ios/appsflyer_sdk.podspec` checks `if defined?($AppsFlyerPurchaseConnector)`. The app sets `$AppsFlyerPurchaseConnector = true` in its own `Podfile` before running `pod install`. +- **iOS, SPM (as of F-060 — Swift Package Manager Support)**: there is no opt-in mechanism at all. `ios/appsflyer_sdk/Package.swift` only ever declares the Core target; it has no knowledge of `PurchaseConnector` and no equivalent of the podspec's `pod_target_xcconfig` macro injection. An SPM-only integration always behaves as "not opted in" — see Known Limitations. --- @@ -42,7 +43,7 @@ iOS (CocoaPods, evaluated at `pod install` time): else s.default_subspecs = 'Core' (PurchaseConnector subspec/pod not included at all) - ios/Classes/AppsflyerSdkPlugin.m (compiled per the xcconfig macro above): + ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m (compiled per the xcconfig macro above): #ifdef ENABLE_PURCHASE_CONNECTOR #import "appsflyer_sdk/appsflyer_sdk-Swift.h" #endif @@ -51,6 +52,12 @@ iOS (CocoaPods, evaluated at `pod install` time): #ifdef ENABLE_PURCHASE_CONNECTOR [PurchaseConnectorPlugin registerWithRegistrar:registrar]; #endif + +iOS (SPM, resolved at `swift build`/`flutter build` time — third gate, added by F-060): + ios/appsflyer_sdk/Package.swift + targets: [.target(name: "appsflyer_sdk", ...)] — Core only, no PurchaseConnector target/product exists + → ENABLE_PURCHASE_CONNECTOR is never defined for this target (SPM has no equivalent of CocoaPods' pod_target_xcconfig) + → the same AppsflyerSdkPlugin.m above compiles with the #ifdef guard resolving false, identically to the CocoaPods not-opted-in path ``` --- @@ -63,7 +70,8 @@ iOS (CocoaPods, evaluated at `pod install` time): | `android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt` | Wraps `PurchaseClient` (Play Billing Library) — only compiled in the include-connector variant | | `android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt` | No-op stub: implements `FlutterPlugin` but registers no `MethodChannel` at all | | `ios/appsflyer_sdk.podspec` | Defines the `PurchaseConnector` CocoaPods subspec conditionally on `$AppsFlyerPurchaseConnector`, and sets the `ENABLE_PURCHASE_CONNECTOR=1` preprocessor macro for that subspec only | -| `ios/Classes/AppsflyerSdkPlugin.m` | `#ifdef ENABLE_PURCHASE_CONNECTOR` guards both the Swift-bridging header import and the `[PurchaseConnectorPlugin registerWithRegistrar:registrar]` call | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `#ifdef ENABLE_PURCHASE_CONNECTOR` guards both the Swift-bridging header import and the `[PurchaseConnectorPlugin registerWithRegistrar:registrar]` call | +| `ios/appsflyer_sdk/Package.swift` (added by F-060) | Declares only the Core target — has no PurchaseConnector target/product and no mechanism to define `ENABLE_PURCHASE_CONNECTOR`, so this gate is permanently "not opted in" for any SPM-only integration | | `doc/PurchaseConnector.md` | App-facing opt-in instructions (`$AppsFlyerPurchaseConnector = true` in Podfile; `appsflyer.enable_purchase_connector=true` in gradle.properties) and an explicit "What Happens if You Use Dart Files Without Opting In?" section | --- @@ -86,6 +94,7 @@ No dedicated test found — this is a Gradle/CocoaPods build-configuration conce - iOS has the same silent-gap behavior by omission rather than an explicit stub: if `$AppsFlyerPurchaseConnector` is undefined, the `PurchaseConnector` subspec/macro/registration are all compiled out, so `PurchaseConnectorPlugin` never registers a handler for `af-purchase-connector` either — same `MissingPluginException` outcome as Android, but reached via a completely different mechanism (absent Ruby global vs. an explicit empty Kotlin object), which is easy for engineers modifying one platform to forget applies to the other. - **F-049 (Purchase Connector: Configuration & Lifecycle) and every other Purchase Connector Dart API are entirely meaningless without this feature being correctly opted into on both platforms** — the Dart-side classes (`PurchaseConnector`, `PurchaseConnectorConfiguration`, etc.) are always compiled into the plugin regardless of opt-in status, so an app can write code against them, pass static analysis, and still get runtime `MissingPluginException`s in production if it forgot the Podfile/gradle.properties step on either platform (`doc/PurchaseConnector.md` calls this out explicitly). - The two opt-in mechanisms are asymmetric in strictness: Android checks a boolean value (`.toBoolean() ?: false`), so `appsflyer.enable_purchase_connector=false` or an unset/malformed property both cleanly resolve to "excluded." iOS checks mere *definedness* of `$AppsFlyerPurchaseConnector` (`defined?(...)`), so setting it to `false` in a Podfile still counts as "opted in" (`if defined?($AppsFlyerPurchaseConnector)` is true regardless of the assigned value) — a plausible copy-paste mistake (`$AppsFlyerPurchaseConnector = false` intending to disable it) silently enables the feature. +- **As of F-060 (Swift Package Manager Support), this gate has a third path with no opt-in mechanism at all**: an app integrated via SPM cannot enable Purchase Connector under any configuration this release — `ios/appsflyer_sdk/Package.swift` never defines `ENABLE_PURCHASE_CONNECTOR`, so the `#ifdef` guard always resolves false. Calling any Purchase Connector Dart API from an SPM-only integration fails with the same generic `MissingPluginException` described above for the CocoaPods not-opted-in case — this is not a new failure mode, but it is a third, permanent path to the same confusing outcome, not a temporary misconfiguration a developer can fix by setting a flag. Apps that need Purchase Connector must stay on CocoaPods until flutter/flutter#161182 (Flutter's own plugin tooling lacking conditional-compilation support) is resolved — see F-060 and `docs/researches/R-001-spm-support.md` for why SPM Package Traits do not currently offer a workaround. --- @@ -94,6 +103,9 @@ No dedicated test found — this is a Gradle/CocoaPods build-configuration conce flowchart LR F054["F-054 · Purchase Connector: Build-Time Opt-in"]:::purchaseValidation F049["F-049 · Purchase Connector: Configuration & Lifecycle"]:::purchaseValidation + F060["F-060 · Swift Package Manager Support"]:::sdkCore F054 -->|"gates compilation/registration of"| F049 + F060 -->|"adds a third, permanently-excluded iOS path to"| F054 classDef purchaseValidation fill:#F59F00,color:#fff + classDef sdkCore fill:#4C6EF5,color:#fff ``` diff --git a/internal-docs/features/F-056-app-invite-link-onelink-id-init-time.md b/internal-docs/features/F-056-app-invite-link-onelink-id-init-time.md index cb9950cd..854faa07 100644 --- a/internal-docs/features/F-056-app-invite-link-onelink-id-init-time.md +++ b/internal-docs/features/F-056-app-invite-link-onelink-id-init-time.md @@ -29,7 +29,7 @@ AppsFlyerOptions(appInviteOneLink: "...") → _methodChannel.invokeMethod("initSdk", validatedOptions) → Android: AppsflyerSdkPlugin.onMethodCall("initSdk") → initSdk(call, result) [android/.../AppsflyerSdkPlugin.java] → call.argument(AppsFlyerConstants.AF_APP_INVITE_ONE_LINK) → AppsFlyerLib.getInstance().setAppInviteOneLink(appInviteOneLink) (only if non-null) - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → call.arguments[afInviteOneLink] → [AppsFlyerLib shared].appInviteOneLinkID = appInviteOneLink (only if non-nil and not NSNull) ``` @@ -41,7 +41,7 @@ AppsFlyerOptions(appInviteOneLink: "...") | `lib/src/appsflyer_options.dart` | `AppsFlyerOptions.appInviteOneLink` — optional `String?` init-time field | | `lib/src/appsflyer_sdk.dart` | `_validateAFOptions()` (lines ~56-61) and `_validateMapOptions()` (lines ~111-123) — copy `appInviteOneLink` into `validatedOptions[AppsflyerConstants.APP_INVITE_ONE_LINK]` under the wire key `"appInviteOneLink"`; `initSdk()` sends it as part of the `"initSdk"` method-channel call | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `initSdk(call, result)` (~line 1100) reads `AppsFlyerConstants.AF_APP_INVITE_ONE_LINK` and calls `AppsFlyerLib.getInstance().setAppInviteOneLink(appInviteOneLink)` if non-null, **after** `instance.init(...)` but before `instance.start(activity)` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` (~line 831) reads `afInviteOneLink` (`"appInviteOneLink"`) and sets `[AppsFlyerLib shared].appInviteOneLinkID` if non-nil and not `NSNull` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` (~line 831) reads `afInviteOneLink` (`"appInviteOneLink"`) and sets `[AppsFlyerLib shared].appInviteOneLinkID` if non-nil and not `NSNull` | --- diff --git a/internal-docs/features/F-057-asa-collection-optout.md b/internal-docs/features/F-057-asa-collection-optout.md index ff675b36..d7d526d3 100644 --- a/internal-docs/features/F-057-asa-collection-optout.md +++ b/internal-docs/features/F-057-asa-collection-optout.md @@ -28,7 +28,7 @@ AppsFlyerOptions(disableCollectASA: true) [lib/src/ → if Platform.isIOS is NOT required here — value is copied unconditionally on both platforms: validatedOptions[AppsflyerConstants.DISABLE_COLLECT_ASA] = options.disableCollectASA (line 63-66 / 125-128) → _methodChannel.invokeMethod("initSdk", validatedOptions) - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → disableCollectASA = call.arguments[afDisableCollectASA] (as NSNumber → BOOL) (line 836-840) → [AppsFlyerLib shared].disableCollectASA = disableCollectASA (line 848) → Android: AppsflyerSdkPlugin.initSdk(call, result) — value is never read; no `DISABLE_COLLECT_ASA` @@ -43,8 +43,8 @@ AppsFlyerOptions(disableCollectASA: true) [lib/src/ | `lib/src/appsflyer_options.dart` | `AppsFlyerOptions.disableCollectASA` (`bool?`, optional named constructor param) | | `lib/src/appsflyer_sdk.dart` | `_validateAFOptions` / `_validateMapOptions` — copies `disableCollectASA` into the validated options map unconditionally (no `Platform.isIOS` guard on the Dart validation side) if non-null | | `lib/src/appsflyer_constants.dart` | `DISABLE_COLLECT_ASA = "disableCollectASA"` — shared Dart↔native key | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define afDisableCollectASA @"disableCollectASA"` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — parses the flag and sets `[AppsFlyerLib shared].disableCollectASA` (lines 836–848) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define afDisableCollectASA @"disableCollectASA"` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — parses the flag and sets `[AppsFlyerLib shared].disableCollectASA` (lines 836–848) | | `doc/BasicIntegration.md`, `doc/API.md` | Document `disableCollectASA` as "Opt-out of the Apple Search Ads attributions" | --- diff --git a/internal-docs/features/F-058-att-authorization-wait-timeout.md b/internal-docs/features/F-058-att-authorization-wait-timeout.md index 78276563..a467a2c0 100644 --- a/internal-docs/features/F-058-att-authorization-wait-timeout.md +++ b/internal-docs/features/F-058-att-authorization-wait-timeout.md @@ -28,7 +28,7 @@ AppsFlyerOptions(timeToWaitForATTUserAuthorization: 50.0) [lib/src → if (Platform.isIOS) { assert(value is double); validatedOptions[AF_TIME_TO_WAIT_FOR_ATT_USER_AUTHORIZATION] = value } (lines 76-85 / 137-148) → _methodChannel.invokeMethod("initSdk", validatedOptions) - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → timeToWaitForATTUserAuthorization = call.arguments[afTimeToWaitForATTUserAuthorization] doubleValue (line 796) → if (timeToWaitForATTUserAuthorization != 0) { [[AppsFlyerLib shared] waitForATTUserAuthorizationWithTimeoutInterval:timeToWaitForATTUserAuthorization] @@ -45,8 +45,8 @@ AppsFlyerOptions(timeToWaitForATTUserAuthorization: 50.0) [lib/src | `lib/src/appsflyer_options.dart` | `AppsFlyerOptions.timeToWaitForATTUserAuthorization` (`double?`, optional named constructor param) | | `lib/src/appsflyer_sdk.dart` | `_validateAFOptions` / `_validateMapOptions` — reads the value **only** when `Platform.isIOS`, asserts it is a `double`, copies into the validated options map | | `lib/src/appsflyer_constants.dart` | `AF_TIME_TO_WAIT_FOR_ATT_USER_AUTHORIZATION = "timeToWaitForATTUserAuthorization"` — shared Dart↔native key | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define afTimeToWaitForATTUserAuthorization @"timeToWaitForATTUserAuthorization"` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — parses the interval and calls `waitForATTUserAuthorizationWithTimeoutInterval:` before `start` (lines 796, 860-869) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define afTimeToWaitForATTUserAuthorization @"timeToWaitForATTUserAuthorization"` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — parses the interval and calls `waitForATTUserAuthorizationWithTimeoutInterval:` before `start` (lines 796, 860-869) | | `doc/BasicIntegration.md`, `doc/AdvancedAPI.md`, `doc/Guides.md`, `doc/API.md` | Document the option as delaying SDK start "for x seconds until the user either accepts the consent dialog, declines it, or the timer runs out" | --- diff --git a/internal-docs/features/F-059-debug-logging-toggle.md b/internal-docs/features/F-059-debug-logging-toggle.md index 04a3bec6..453bccb0 100644 --- a/internal-docs/features/F-059-debug-logging-toggle.md +++ b/internal-docs/features/F-059-debug-logging-toggle.md @@ -32,7 +32,7 @@ AppsFlyerOptions(showDebug: true) [lib/src → if (isDebug) { instance.setLogLevel(AFLogger.LogLevel.DEBUG); instance.setDebugLog(true); } else { instance.setDebugLog(false); } (lines 1088-1093) - → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/Classes/AppsflyerSdkPlugin.m] + → iOS: AppsflyerSdkPlugin.handleMethodCall("initSdk") → initSdkWithCall:result: [ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m] → isDebugValue = call.arguments[afIsDebug] (line 805) → [AppsFlyerLib shared].isDebug = isDebug (line 813) ``` @@ -47,8 +47,8 @@ AppsFlyerOptions(showDebug: true) [lib/src | `lib/src/appsflyer_constants.dart` | `AF_IS_DEBUG = "isDebug"` — shared Dart↔native key | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java` | `initSdk(call, result)` — toggles `AppsFlyerLib.getInstance().setLogLevel(...)` and `.setDebugLog(...)` (lines 1087-1093) | | `android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java` | `AF_IS_DEBUG = "isDebug"` — native Android mirror of the Dart key | -| `ios/Classes/AppsflyerSdkPlugin.h` | `#define afIsDebug @"isDebug"` | -| `ios/Classes/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `[AppsFlyerLib shared].isDebug` directly (lines 805, 813) | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h` | `#define afIsDebug @"isDebug"` | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m` | `initSdkWithCall:result:` — sets `[AppsFlyerLib shared].isDebug` directly (lines 805, 813) | | `doc/BasicIntegration.md`, `doc/API.md`, `doc/Testing.md` | Document `showDebug` and warn "do not release to production with this parameter set to `true`" | --- diff --git a/internal-docs/features/F-060-swift-package-manager-support.md b/internal-docs/features/F-060-swift-package-manager-support.md new file mode 100644 index 00000000..5ba4dddf --- /dev/null +++ b/internal-docs/features/F-060-swift-package-manager-support.md @@ -0,0 +1,104 @@ +--- +id: F-060 +name: "Swift Package Manager (SPM) Support (Core, iOS)" +type: sdkCore +platform: ios +status: active +last_verified: 2026-07-19 +depends_on: [] +--- + +## Business Purpose +Flutter 3.44+ makes Swift Package Manager the default iOS integration mechanism, and CocoaPods trunk goes read-only on December 2, 2026 — after that date, this plugin could no longer publish new CocoaPods releases at all, and any app on Flutter 3.44+ that hadn't migrated would hit a hard build error instead of today's build warning. Without this feature, every consumer of the plugin would eventually be forced onto an unsupported distribution path, and competing attribution SDKs (Adjust, Singular) that already support SPM would have a real integration advantage. This feature adds a `Package.swift` manifest for the Core integration so apps can adopt SPM today, while leaving CocoaPods fully intact for apps that aren't ready to migrate or that need Purchase Connector (see Known Limitations). + +Ticket: DELIVERY-125462. + +--- + +## Trigger +Not a runtime trigger — this is a build-time/distribution-mechanism choice made once per consuming app project: +- **SPM path**: the app either runs on Flutter 3.44+ (SPM is the default) or explicitly opts in on earlier 3.24+ versions via `flutter config --enable-swift-package-manager`. Flutter's own tooling then discovers `ios/appsflyer_sdk/Package.swift` at its conventional path — no marker or flag is required in the podspec to signal SPM availability. +- **CocoaPods path**: unchanged — apps that run `pod install` continue to resolve via `ios/appsflyer_sdk.podspec` exactly as before. + +--- + +## Call Chain +This feature has no runtime call chain — it is a build-time source-tree and manifest change: + +``` +Shared source tree (used by both paths, single copy — no duplication): + ios/appsflyer_sdk/Sources/appsflyer_sdk/ + AppsflyerSdkPlugin.m (moved from ios/Classes/, content unmodified) + AppsFlyerAttribution.m (moved, unmodified) + AppsFlyerStreamHandler.m (moved, unmodified) + include/appsflyer_sdk/ + AppsflyerSdkPlugin.h (moved, unmodified — public header, pluginClass entry point) + AppsFlyerAttribution.h + AppsFlyerStreamHandler.h + FlutterAppDelegate+AppsFlyerStreamHandler.h + +SPM path (resolved by `flutter build`/`swift build` at build configuration time): + ios/appsflyer_sdk/Package.swift + → target "appsflyer_sdk" depends on product "AppsFlyerLib" from AppsFlyerFramework, pinned exactly to 6.18.0 + → compiles the shared Sources/ tree above as a ClangTarget, iOS 12.0 minimum + → does NOT reference ios/PurchaseConnector/ at all — no PurchaseConnector target/product exists in this manifest + +CocoaPods path (resolved by `pod install` at install time, unchanged behavior): + ios/appsflyer_sdk.podspec + subspec 'Core' → source_files/public_header_files repointed at the same shared Sources/ tree above + subspec 'PurchaseConnector' → untouched, still points at ios/PurchaseConnector/ (unmoved) +``` + +--- + +## Files +| File | Role | +|------|------| +| `ios/appsflyer_sdk/Package.swift` | New SPM manifest. `swift-tools-version:5.9` (Xcode 15.0+), `platforms: [.iOS("12.0")]` (matches the podspec's existing deployment target). Declares one product/target depending on `AppsFlyerFramework`'s `AppsFlyerLib` product, pinned `.exact("6.18.0")`, matching the podspec's exact CocoaPods pin. | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/*.m` | Core implementation files, moved verbatim from `ios/Classes/` via `git mv` (confirmed zero content diff) — now the single shared source tree for both CocoaPods and SPM. | +| `ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/*.h` | Public headers, moved verbatim from `ios/Classes/` — `AppsflyerSdkPlugin.h` is where `pluginClass: AppsflyerSdkPlugin` (declared in `pubspec.yaml`, unchanged) resolves from in both integration paths. | +| `ios/appsflyer_sdk.podspec` | `Core` subspec's `source_files`/`public_header_files` repointed to the new shared path; `PurchaseConnector` subspec is untouched. No marker added to declare SPM availability — Flutter's tooling detects it purely by the presence of `Package.swift` at the conventional path. | +| `ios/.gitignore` | Added `.build/` and `.swiftpm/` — local SPM resolution/build artifacts that must not be committed. | +| `CHANGELOG.md` | Documents SPM support added under the 6.18.0 entry, Purchase Connector's continued CocoaPods-only status, and a link to flutter/flutter#161182. | + +--- + +## Input / Output +| | | +|--|--| +| **Input** | Which iOS integration mechanism the consuming app's Flutter tooling selects: SPM (default on Flutter 3.44+, opt-in via `flutter config --enable-swift-package-manager` on 3.24–3.43) or CocoaPods (`pod install`, unchanged). Nothing in `pubspec.yaml` changes to select this — it's entirely driven by the app's own Flutter/Xcode configuration. | +| **Output** | Which build system compiles the Core native code and links `AppsFlyerFramework` into the app: Swift Package Manager resolving `AppsFlyerLib` directly from GitHub, or CocoaPods resolving the `AppsFlyerFramework` pod as before. Either path produces the same compiled Core behavior — same source files, same public API surface. | + +--- + +## Tests +No dedicated automated test — this is a build-configuration/distribution-mechanism concern with no Dart or native runtime logic change, the same category as F-054 (Purchase Connector: Build-Time Opt-in), which sets the precedent that this class of change is verified via full builds rather than unit tests. Verification performed for this change: +- `swift package describe` — genuine dependency resolution against the live `AppsFlyerFramework` GitHub repository, confirming the manifest resolves product `AppsFlyerLib` at `Exact: 6.18.0` (corrected from an earlier `from:` range pin during review — see Known Limitations) and picks up all 3 Core `.m` sources correctly. +- `pod spec lint --quick --allow-warnings` — passed, confirming the podspec's repointed `source_files`/`public_header_files` globs resolve correctly against the moved tree. +- `flutter test test` — all 38 existing Dart tests pass unaffected (this change touches only iOS native file locations and build manifests, not Dart code). +- **Real-device iOS E2E, dispatched via GitHub Actions with real credentials, 3 of the tech design's 4 combinations — all 6 scenario phases PASS in each:** + - SPM, Core only, `.exact("6.18.0")` pin — [run 30191649705](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/actions/runs/30191649705). `getSDKVersion` confirmed resolving `6.18.0`, not a drifted patch release (an earlier run against the pre-fix `from:` pin had resolved `6.18.1` — see Known Limitations). + - Hybrid: SPM Core + CocoaPods PurchaseConnector simultaneously (realistic config for an app that wants both) — [run 29848672331](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/actions/runs/29848672331). + - Pure CocoaPods, Core + PurchaseConnector, SPM explicitly disabled — [run 29901950273](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/actions/runs/29901950273). + +> **Remaining gap**: the 4th combination — actively calling a Purchase Connector API from an SPM-only integration and confirming it raises `MissingPluginException` rather than crashing or hanging — has not been exercised by a real test, only reasoned through statically (see the tech design's corrected failure-mode analysis). This is a low-risk, non-blocking gap: the mechanism (`ENABLE_PURCHASE_CONNECTOR` never defined under SPM) is the same one already exercised today by the CocoaPods not-opted-in path, just reached a third way. + +--- + +## Known Limitations +- **Purchase Connector is not available via SPM this release, with no opt-in mechanism at all.** `Package.swift` never references `ios/PurchaseConnector/` and has no equivalent of the podspec's `pod_target_xcconfig` macro injection, so `ENABLE_PURCHASE_CONNECTOR` is never defined for an SPM build under any configuration. Calling a Purchase Connector Dart API from an SPM-only integration fails with the same generic Flutter `MissingPluginException` that F-054 already documents for the CocoaPods not-opted-in case — this is not a new or worse failure mode, but it is a third, permanent path to it (not something a developer can fix by setting a flag, unlike the other two paths). Apps that need Purchase Connector must stay on CocoaPods until flutter/flutter#161182 is resolved. +- **flutter/flutter#161182 (Flutter's own plugin tooling lacking conditional-compilation support under SPM) is the real blocker**, not a SwiftPM limitation — investigated during research (`internal-docs/researches/R-001-spm-support.md`), including whether SwiftPM Package Traits (Swift tools 6.1+) could work around it. They cannot: the issue's own text states Flutter would need to add trait support to its plugin tooling first, which it has not. +- **Three architectural alternatives to bring Purchase Connector onto SPM were evaluated and rejected for this release** (see `internal-docs/researches/R-001-spm-support.md` addendum): a second product in the same `Package.swift` (not viable — Flutter's tooling only links one product per plugin, no documented support for a second), an environment-variable-gated compile flag (technically usable but fragile — requires every consuming app to set an env var on every build/CI run with silent failure if forgotten), and splitting Purchase Connector into its own federated pub.dev package (architecturally sound, no hidden blocker, but a separate, larger initiative with its own versioning/release pipeline — a candidate future initiative, not part of this ticket). +- **Real-device build verification is outstanding** — see Tests section above. Static/network verification (Swift manifest resolution, podspec lint, Dart test suite) passed, but the tech design's full 4-path device build has not yet run. + +--- + +## Dependencies +```mermaid +flowchart LR + F060["F-060 · Swift Package Manager Support"]:::sdkCore + F054["F-054 · Purchase Connector: Build-Time Opt-in"]:::purchaseValidation + F060 -->|"adds a third, permanently-excluded iOS path to"| F054 + classDef sdkCore fill:#4C6EF5,color:#fff + classDef purchaseValidation fill:#F59F00,color:#fff +``` diff --git a/internal-docs/features/INDEX.md b/internal-docs/features/INDEX.md index ca40fb1f..7ffa9557 100644 --- a/internal-docs/features/INDEX.md +++ b/internal-docs/features/INDEX.md @@ -1,6 +1,6 @@ # AppsFlyer Flutter Plugin — Feature Catalog Index -59 features across 6 categories. See `DIAGRAM.md` for runtime/init dependency diagrams and the full dependency table. +60 features across 6 categories. See `DIAGRAM.md` for runtime/init dependency diagrams and the full dependency table. --- @@ -34,6 +34,7 @@ SDK lifecycle, identity, privacy/consent, and low-level configuration. | F-057 | ASA (Apple Search Ads) Collection Opt-out | active | ios | | F-058 | ATT Authorization Wait Timeout (iOS) | active | ios | | F-059 | Debug Logging Toggle | active | both | +| F-060 | Swift Package Manager (SPM) Support (Core, iOS) | active | ios | ## eventsAndRevenue diff --git a/internal-docs/prds/spm-support.md b/internal-docs/prds/spm-support.md new file mode 100644 index 00000000..3cd10f0d --- /dev/null +++ b/internal-docs/prds/spm-support.md @@ -0,0 +1,71 @@ +--- +ticket: DELIVERY-125462 +priority: P1 +target: v6.18.0, end of July 2026 +--- + +# PRD: Swift Package Manager (SPM) Support + +## Problem + +The plugin's iOS integration ships only via CocoaPods (`ios/appsflyer_sdk.podspec`). Two industry shifts make this untenable on the current timeline: + +1. Flutter 3.44+ makes Swift Package Manager the default iOS integration mechanism. Plugins without an SPM manifest already surface a build warning in consuming apps today. +2. CocoaPods trunk (the `pod repo push` publishing path) goes **read-only on December 2, 2026**. Once that happens, the plugin cannot ship *new* CocoaPods releases at all — the build warning becomes a hard build error for any app that hasn't migrated, and we lose the ability to patch the CocoaPods distribution. + +Competing attribution SDKs (Adjust, Singular) already support SPM, so apps that need SPM today are choosing those SDKs over ours. The community has raised this twice (tracking issue #364, draft PR #370) and both attempts stalled on the same blocker: the `PurchaseConnector` subspec has no clean SPM path because it depends on an upstream Flutter engine limitation (flutter/flutter#161182) that is outside this plugin's control. + +## Goal + +Ship a `Package.swift` manifest so apps can integrate the plugin's Core (default) functionality via SPM, while `PurchaseConnector` remains CocoaPods-only until upstream Flutter resolves flutter/flutter#161182. Existing CocoaPods consumers must see zero behavior change. + +Success: an app can add the plugin via SPM and get full attribution/deep-linking functionality (everything except Purchase Connector) with no CocoaPods dependency, by end of July 2026, in v6.18.0. + +## Non-goals + +- Making `PurchaseConnector` available via SPM — explicitly blocked on flutter/flutter#161182; out of scope until that upstream issue is resolved. +- Dropping or deprecating CocoaPods support — CocoaPods remains fully supported in this release. +- Migrating the Android side of the plugin (SPM is iOS/Apple-platform-only; no Android equivalent exists). +- Evaluating Swift Package Manager Traits (Swift tools 6.1+) as a mechanism to ship `PurchaseConnector` conditionally via SPM — flagged as a candidate for Bob to research, but committing to it is out of scope for this PRD until Bob confirms it's viable and doesn't just relocate the same upstream Flutter blocker. + +## User/customer impact + +- **Apps not using Purchase Connector**: can adopt SPM immediately, removing their CocoaPods dependency and the build warning; avoids a hard build break after Dec 2, 2026. +- **Apps using Purchase Connector**: must stay on CocoaPods (full install) until the upstream blocker resolves. They are not broken by this change, but they don't get the SPM option yet — this is a real, currently-unavoidable gap that needs to be communicated clearly in docs/release notes so these teams aren't surprised post-Dec-2026. +- **Existing CocoaPods consumers (any config)**: no behavior change — this PRD requires full backward compatibility as an explicit requirement, not an assumption. + +## Requirements + +1. Add a `Package.swift` manifest exposing the Core integration as an SPM product, building on the approach already prototyped in draft PRs #455 and #454. +2. `PurchaseConnector` is NOT exposed via SPM in this release; it remains a CocoaPods-only subspec, gated the same way `appsflyer.enable_purchase_connector` / `$AppsFlyerPurchaseConnector` already gate it today (see F-054). +3. `ios/appsflyer_sdk.podspec` continues to work unmodified in behavior for existing CocoaPods consumers — both the Core-only and Core+PurchaseConnector configurations. +4. Both integration paths must be verified before release: + - SPM-only (Core, no PurchaseConnector) + - CocoaPods, Core only + - CocoaPods, Core + PurchaseConnector + - (Explicitly NOT required: SPM + PurchaseConnector — not supported this release) +5. `CHANGELOG.md` and plugin release notes document: SPM support added, PurchaseConnector's CocoaPods-only status and why, and a pointer to flutter/flutter#161182 for apps tracking when Purchase Connector SPM support might land. +6. Ship as part of the current SDK 6 line, v6.18.0. + +## Acceptance criteria + +- [ ] A fresh Flutter app added via SPM (no `Podfile`) builds successfully on iOS and can call Core attribution APIs (init, start, event logging) end to end. +- [ ] A fresh Flutter app using CocoaPods with `PurchaseConnector` disabled builds and behaves identically to pre-change behavior. +- [ ] A fresh Flutter app using CocoaPods with `PurchaseConnector` enabled builds and behaves identically to pre-change behavior. +- [ ] Attempting to reference Purchase Connector APIs from an SPM-only integration fails at build/compile time with a clear signal (not a silent runtime no-op) — exact mechanism to be defined by Dave in tech design. +- [ ] `CHANGELOG.md` entry and release notes are published alongside v6.18.0 describing the SPM addition and the PurchaseConnector CocoaPods-only limitation. +- [ ] No existing `example/` app (CocoaPods-based) requires any change to keep building. + +## Risks + +- **Release risk**: this touches the iOS distribution mechanism for every consumer of the plugin, including all existing CocoaPods apps. A `Package.swift` misconfiguration or podspec regression could break builds plugin-wide. Requires explicit verification of all three supported build paths (Requirement 4) before shipping, not just the new SPM path. +- **Confusing failure mode risk**: if referencing Purchase Connector from an SPM-only integration fails silently or with an unclear Swift compiler error, it repeats the exact "confusing MissingPluginException" failure pattern already documented as a known limitation of the existing CocoaPods opt-in gate (F-054). Dave's tech design must address this explicitly. +- **Deadline risk**: CocoaPods trunk goes read-only Dec 2, 2026, well after this July 2026 ship date — no schedule risk from that deadline itself, but it does mean this is the last comfortable window to ship before urgency increases. +- **Scope creep risk**: SPM Package Traits (surfaced during research) could look like a tempting way to "solve" the PurchaseConnector gap now. Bob must confirm whether it actually changes anything about the flutter/flutter#161182 blocker before any decision to expand scope — the default assumption per ticket is that PurchaseConnector stays CocoaPods-only this release regardless of what traits offer. + +## Open questions + +- ~~Does flutter/flutter#161182 block *any* SPM path for PurchaseConnector, or does SPM Package Traits (Swift tools 6.1+) offer a way around it?~~ **Resolved (R-001):** flutter/flutter#161182 is still open and is about Flutter's own plugin build tooling lacking conditional-compilation support — not something SwiftPM Traits can fix from our side, since Flutter doesn't route plugin builds through traits today. PurchaseConnector stays CocoaPods-only this release, as originally scoped; traits are not a viable shortcut. +- ~~What is the minimum Xcode / Swift tools version the target Package.swift manifest requires, and is it compatible with the Flutter versions this plugin currently supports?~~ **Resolved (R-001):** `AppsFlyerFramework`'s own SPM package (dependency) requires Swift tools 5.3; draft PRs use tools-version 5.9 (Xcode 15.0+ minimum) for this plugin's own manifest. No conflict with `pubspec.yaml`'s Dart SDK/Flutter constraints — SPM eligibility is gated by the consuming app's Flutter tool version, not this package's declared environment. +- What exact compile-time signal should apps get if they reference Purchase Connector APIs without CocoaPods? (compiler error vs. missing symbol vs. something else) — **Dave to resolve in tech design.** (R-001 notes this should surface as a build/link error, not a silent runtime no-op — an improvement over F-054's existing CocoaPods failure mode — but Dave must confirm this holds for the SPM path specifically.) +- Do draft PRs #455/#454 already answer the Package.swift structure question, or do they need re-validation against the current plugin structure? — **Partially resolved (R-001):** #454 is the recommended starting point (move-based layout, already isolates PurchaseConnector correctly) but its dependency declaration is wrong (`AppsFlyerLib`, not `AppsFlyerLib-Static`) and neither draft PR completed real CI/device-build verification — **Dave to re-validate and correct in tech design**, and confirm the exact required `Package.swift` path convention against Flutter's official plugin-author SPM guide. diff --git a/internal-docs/researches/R-001-spm-support.md b/internal-docs/researches/R-001-spm-support.md new file mode 100644 index 00000000..4b14358f --- /dev/null +++ b/internal-docs/researches/R-001-spm-support.md @@ -0,0 +1,91 @@ +--- +id: R-001 +title: Swift Package Manager (SPM) support — feasibility, PurchaseConnector blocker, and prior art +versions: "Flutter 3.24 (experimental) – 3.44+ (default); Swift tools 5.3 – 5.9; Xcode 12+ (Package.swift baseline), Xcode 15+ (this plugin's actual manifest)" +status: complete +date: 2026-07-19 +affects-features: [F-054] +related-issue-cases: [] +--- + +## Summary + +Researched for DELIVERY-125462 / PRD `docs/prds/spm-support.md`. No prior research or issue-case docs existed on this topic (`docs/researches/` and `docs/issue-cases/` are both empty in this repo). Checked GitHub directly (issues/PRs on this plugin's repo, flutter/flutter, and AppsFlyerSDK/AppsFlyerFramework) rather than relying on secondhand summaries. + +Key finding: **Swift Package Manager Traits do NOT unblock PurchaseConnector.** flutter/flutter#161182 — the exact issue the ticket cites — is literally titled "[SwiftPM] Support conditional compilation in plugins" and is still **OPEN**, unassigned, P3. It states plainly: "Swift Package Manager does not support conditional compilation," and lists two possible fixes, neither shipped: (1) a documented hacky workaround, or (2) "Update Flutter to support Swift package traits **if/when that lands**." Traits are a SwiftPM-language feature (SE-0450, Swift tools 6.1+) — the blocker is that **Flutter's own plugin build tooling** has no support for conditional compilation of plugin code, with or without traits underneath. Until Flutter's tooling adds that support, PurchaseConnector cannot be conditionally included via SPM regardless of what SwiftPM itself offers. This confirms the PRD's non-goal was correctly scoped: don't chase traits for this release. + +Second finding: two real, unmerged draft PRs already exist on this exact repo with working (self-reported) Core-only SPM implementations — #454 and #455 — and they diverge in approach. #454 is the more directly relevant prior art (explicitly designed to avoid the PurchaseConnector blocker); its dependency pin has a naming inaccuracy that Dave should not copy verbatim (see below). + +## API / Platform Details + +**flutter/flutter#161182** — "[SwiftPM] Support conditional compilation in plugins," opened by `loic-sharma` (Flutter/iOS team), state: **OPEN**, labels `c: new feature, P3, platform-ios, platform-macos, team-ios, triaged-ios`, no assignee. +- Root cause: CocoaPods plugins can gate optional features behind compile flags set in an app's Podfile/gradle-equivalent (this plugin already does exactly that for PurchaseConnector via `$AppsFlyerPurchaseConnector`, see F-054). SwiftPM has no equivalent mechanism *as surfaced through Flutter's plugin system* today. +- Named affected packages besides us: `just_audio` (optional microphone feature), `permission_handler`. +- Proposed fixes, both unshipped: a documented hacky workaround (external gist), or adopting Swift Package Traits (SE-0450) once Flutter's tooling supports them. + +**flutter/flutter tracking issue #364** on our own repo (`AppsFlyerSDK/appsflyer-flutter-plugin`) — filed by the Flutter team's automated outreach (`loic-sharma`), **state: CLOSED** (labels: `enhancement, stale`), assigned to an AppsFlyer employee (Dani Koza). This is not really "community-proposed" in the grassroots sense — it's Flutter's own campaign to get plugin authors to migrate, sent directly to us, that went stale. + +**Draft PR #370** — `AppsFlyerSDK/appsflyer-flutter-plugin`, state DRAFT, external contributor, touches the podspec only ("Updated podspec"), closes #364. No Package.swift authored. Confirms the ticket's claim that this attempt stalled without a working manifest. + +**Draft PR #454** — "feat(ios): add Swift Package Manager support for Core," state OPEN, external contributor (`nurlangarash`), 52 changed lines across 8 files. This is the most directly relevant prior art: +- Moves `ios/Classes/*.m` → `ios/appsflyer_sdk/Sources/appsflyer_sdk/` (impl) and `.../include/appsflyer_sdk/` (public headers) via `git mv` — required because SPM expects a specific source-tree layout, unlike CocoaPods' `source_files` glob. +- Adds `ios/appsflyer_sdk/Package.swift`, `swift-tools-version: 5.9`, `platforms: [.iOS(.v12)]`. +- PR description claims it depends on "AppsFlyerFramework-Static SPM package (product `AppsFlyerLib-Static`, pinned `6.18.0`)" — **this is inaccurate**. I fetched `AppsFlyerFramework`'s actual `Package.swift` at tag `6.18.0` directly: the product name is `AppsFlyerLib` (not `AppsFlyerLib-Static`), `swift-tools-version:5.3`, and it resolves to a binary xcframework at `https://github.com/AppsFlyerSDK/AppsFlyerFramework/releases/download/6.18.0/AppsFlyerLib-Static-SPM.xcframework.zip` (the *file* is named `-Static-SPM`, the *product* is not). Dave should depend on product `AppsFlyerLib`, not `AppsFlyerLib-Static`, and pin `from: "6.18.0"` to match the podspec's existing `ss.ios.dependency 'AppsFlyerFramework','6.18.0'` — do not blindly copy the PR's dependency declaration. +- Leaves `PurchaseConnector/` untouched on CocoaPods; the existing `#ifdef ENABLE_PURCHASE_CONNECTOR` guard (see F-054) already compiles it out of any target that doesn't define the macro, which is exactly the mechanism the SPM Core target relies on implicitly (SPM target simply never defines the macro or includes PurchaseConnector sources). +- Explicitly flagged by its own author as unverified: "⚠️ Please run CI / a device build before merging... I could not run a full iOS build here." + +**Draft PR #455** — "feat(ios): add Swift Package Manager support," state OPEN, different external contributor (`TeddyYeung`), 1432 changed lines. Broader/older attempt: keeps `ios/Classes/` in place and adds a separate `ios/appsflyer_sdk/Sources/appsflyer_sdk/` tree (mirrors rather than moves), declares `AppsFlyerFramework` as a binary dependency directly rather than depending on its published SPM package. Author reports both SPM and CocoaPods builds succeeded locally with Flutter 3.35.7. No device-level test evidence beyond that. Larger diff, more duplication between CocoaPods and SPM source trees than #454's move-based approach. + +**AppsFlyerFramework SPM package** (`AppsFlyerSDK/AppsFlyerFramework`): confirmed via GitHub API that both the `6.18.0` and `7.0.0` tags carry a working `Package.swift` (binary xcframework target, tools-version 5.3). The plugin's current podspec already pins native SDK `6.18.0` — that tag's SPM package is confirmed present and resolvable, so no native-SDK version bump is required to add SPM support in this release. + +## Behavior by Version + +| Version | Behavior | Notes | +|---------|----------|-------| +| Flutter < 3.24 | No SPM awareness; CocoaPods only | No change needed — these apps are unaffected either way | +| Flutter 3.24 – 3.43 | SPM available behind `flutter config --enable-swift-package-manager` (experimental, opt-in) | Apps must explicitly opt in to hit our new SPM path | +| Flutter 3.44+ | SPM is the **default** iOS integration; plugins without a manifest emit the "does not support Swift Package Manager" build warning | This is the driver for the ticket's urgency | +| Swift tools 5.3 | Minimum declared by `AppsFlyerFramework`'s own Package.swift (both 6.18.0 and 7.0.0 tags) | Not a constraint we control but must stay compatible with | +| Swift tools 5.9 (used by draft PR #454) | Requires Xcode 15.0+ to resolve/build | Xcode 15 shipped Sept 2023 — not a meaningful constraint for apps building in July 2026 | +| CocoaPods trunk | Read-only from **Dec 2, 2026** | After this date we lose the ability to publish *new* CocoaPods releases — not a factor for this July 2026 ship date, but the reason this can't slip past that window | + +## SDK/Service Impact + +- **F-054 (Purchase Connector: Build-Time Opt-in)** is directly relevant and should be updated once implementation lands: its "Files" and "Call Chain" sections describe the CocoaPods-only `#ifdef ENABLE_PURCHASE_CONNECTOR` gate. That gate is the same mechanism the SPM Core target relies on (by omission — the SPM target never defines the macro or references PurchaseConnector sources at all). Recommend Dave add a note there once the SPM manifest exists, since it becomes a *third* code path relying on the same guard, not just Android Gradle + iOS CocoaPods. +- Dave's tech design should decide, and document, what happens if an app tries to reference Purchase Connector Dart APIs while integrated via SPM. Given the ObjC `#ifdef` guard is compiled out entirely, the native symbol won't exist — this should surface as a build/link error in the consuming app's Xcode build (undefined symbol / missing plugin registration), not a silent runtime no-op. This is an *improvement* over the existing CocoaPods failure mode (F-054's Known Limitations documents that CocoaPods opt-out currently fails silently at runtime with a generic Flutter `MissingPluginException`). Dave should confirm this build-time-vs-runtime distinction holds for the SPM path specifically before claiming it in the tech design. +- No changes are needed to `pubspec.yaml`'s Dart SDK/Flutter environment constraints (`>=2.17.0 <4.0.0` / `>=1.10.0`) — SPM eligibility is gated by the Flutter *tool* version an app builds with, not by this package's declared Dart SDK constraint. Existing CocoaPods consumers on old Flutter versions are entirely unaffected. +- Recommend Dave use PR #454's move-based file layout (not #455's mirror-based layout) as the starting structure — less duplication, and its author already anticipated the PurchaseConnector guard correctly — but correct the dependency declaration to product `AppsFlyerLib` (not `AppsFlyerLib-Static`) pinned `from: "6.18.0"`, and complete the CI/device build verification neither draft PR finished. +- **Compliance/privacy — no impact, verified directly.** Downloaded and inspected both native-SDK distribution artifacts for tag `6.18.0` rather than assuming: the CocoaPods pod (`AppsFlyerFramework.podspec`) sources from `AppsFlyerLib-Binaries.zip` and declares the Apple privacy manifest via `resource_bundles = {'AppsFlyerLib_Privacy' => [...PrivacyInfo.xcprivacy]}`. The SPM binary target sources from a *different* zip (`AppsFlyerLib-Static-SPM.xcframework.zip`) — I downloaded it and confirmed `PrivacyInfo.xcprivacy` is embedded directly inside each per-platform slice of the xcframework itself (`AppsFlyerLib.xcframework/ios-arm64/AppsFlyerLib.framework/PrivacyInfo.xcprivacy`, and five other platform slices). Same privacy manifest content, different packaging convention (CocoaPods resource bundle vs. SPM's expected in-framework embedding) — this is the standard, Apple-documented way privacy manifests differ by distribution mechanism, not a gap. No new data collection, consent, or tracking-disclosure surface is introduced by adding the SPM path; it ships the exact same native binary's declared privacy behavior, just packaged per SPM's own convention. Out of scope for further compliance review. +- **Platform/integration risk — no App Store precedent found.** I did not find any documented Apple App Store review policy that distinguishes between CocoaPods-distributed and SPM-distributed dependencies — Apple's review process operates on the built app binary and its declared entitlements/privacy manifests, not on which dependency manager assembled it. No rejection precedent tied to distribution mechanism itself is known. Stating this explicitly rather than leaving it silent: this is not a risk vector for this change. + +## Open Questions + +1. Neither draft PR ran a full CI pipeline or device build (both explicitly flag simulator/local-only or "please verify before merging") — Dave's tech design must include real verification of all four build-path combinations from the PRD's acceptance criteria, not reuse the drafts' informal testing claims. +2. Should the plugin's `Package.swift` live at `ios/appsflyer_sdk/Package.swift` (both drafts' choice, required by SPM's convention of the manifest sitting at the package root alongside `Sources/`) — confirm this is compatible with how `flutter pub` locates iOS plugin folders; the ticket and both PRs assume yes but I did not find an authoritative Flutter doc confirming the exact required path for a **plugin's nested** SPM package (vs. a repo that is only an SPM package). Dave should verify against the official Flutter SPM plugin-author guide linked in PR #455 before finalizing the path. +3. Whether the CocoaPods podspec needs any accompanying change to declare compatibility/coexistence with the new SPM manifest (some Flutter plugin migrations add a marker so `flutter` tooling detects SPM availability) — not established by either draft PR; Dave to confirm against the Flutter plugin-author migration guide. + +## Addendum — can PurchaseConnector be included in SPM at all, via a different architecture? + +Follow-up investigation: R-001's original conclusion (PurchaseConnector stays CocoaPods-only) was specifically about the "single target, opt-in compile flag" pattern (matching `just_audio`'s approach, the pattern flutter/flutter#161182 is literally about). Two structurally different architectures were checked concretely rather than assumed away: + +**1. Multi-product single package — not viable via supported Flutter tooling.** A `Package.swift` *can* technically declare two separate library products (Core + PurchaseConnector as distinct targets) — that's plain SPM, no traits needed. But Flutter's own plugin-authoring model, per the official guide, links exactly **one** product per plugin, matching the plugin's registered name (`plugin_name` → library `plugin-name`) — there is no documented mechanism for a second, app-opt-in product, and the guide does not describe `FlutterGeneratedPluginSwiftPackage` (the tool-managed aggregator package Flutter generates from `pubspec.yaml`) as supporting manual edits or additional per-plugin products. Any hand-added Xcode-level dependency edge to a non-default product would be at risk of being wiped by Flutter's own regeneration on `flutter pub get`/`flutter build` — this is exactly the class of problem flutter/flutter#161182 is asking Flutter to solve, and it isn't solved yet. **Not recommended**: relies on undocumented, unsupported tool behavior. + +**2. Documented hacky workaround (env-var-gated compile flag) — technically usable, not recommended for a published package.** Flutter's own issue links to https://github.com/loic-sharma/swiftpm_conditional_compilation, which works by reading `ProcessInfo.processInfo.environment` **inside `Package.swift`'s manifest evaluation** and conditionally adding a `SwiftSetting.define(...)` flag if an environment variable is set to `"1"` at the time the consuming app invokes `flutter run`/`flutter build`. This is something a plugin author *can* write into a public package's `Package.swift` — nothing blocks it technically. But the consuming app must (a) set that env var on every single build/run invocation (local dev *and* CI/release pipelines) and (b) run `flutter clean` every time the value changes, since SPM does not auto-invalidate the build when the env var flips — the workaround's own README documents this as a required manual step, not automatic. A missed env var in a release CI pipeline would silently disable Purchase Connector with zero build warning. This is a materially worse and more fragile experience than today's one-time `$AppsFlyerPurchaseConnector = true` Podfile flag or Android's `gradle.properties` flag (set once, persists across builds). **Not recommended for a published pub.dev plugin**: pushes a fragile, easy-to-silently-break requirement onto every consumer's build pipeline. + +**3. Federated package split — architecturally sound, no blocker found, but out of scope for this ticket.** Splitting `PurchaseConnector` into its own independent Flutter package (e.g. `appsflyer_purchase_connector`) with its own `pubspec.yaml`, podspec, `Package.swift`, and Android `build.gradle` sidesteps flutter/flutter#161182 entirely — "is this package a `pubspec.yaml` dependency or not" is not conditional compilation, it's the normal dependency-resolution mechanism Flutter has always fully supported for both CocoaPods and SPM. This is the same pattern Firebase (`firebase_core` + `cloud_firestore`, etc.) and federated plugins (platform-interface splits) already use in production at scale — no hidden blocker found. On the Dart side, `package:appsflyer_sdk/purchase_connector.dart` could remain a working import path via an `export 'package:appsflyer_purchase_connector/purchase_connector.dart';` re-export shim, so existing Dart-level imports would not need to change. **However**, this does NOT make it a small change: it requires the new package to carry its own `pluginClass`/native plugin registration (currently `PurchaseConnector` is a CocoaPods *subspec* of the same plugin, not an independently-registered Flutter plugin at all), its own independent versioning and release process through the six-stage RC pipeline, a migration/deprecation path for existing consumers' native build files (`Podfile`/`gradle.properties` flags would change meaning or need replacing), and coordination with whatever timeline is acceptable for a breaking-ish native architecture change. This is a real, viable option — but it is a separate, larger initiative, not something that fits inside DELIVERY-125462's July 2026 / v6.18.0 window alongside Core SPM support. + +**Recommendation**: none of the three options make "PurchaseConnector via SPM, this release" viable without either relying on unsupported Flutter tooling behavior (option 1), pushing real production fragility onto every consumer (option 2), or taking on a materially larger, independently-scoped migration (option 3). The current PRD's non-goal (PurchaseConnector stays CocoaPods-only, revisit when flutter/flutter#161182 resolves) remains the soundest call for this ticket. Option 3 is worth raising as a candidate follow-up initiative if PurchaseConnector-via-SPM becomes a hard requirement before flutter/flutter#161182 resolves — but that is a scope/roadmap decision, not a technical necessity for DELIVERY-125462. + +## References + +- flutter/flutter#161182 — https://github.com/flutter/flutter/issues/161182 (open, unresolved, primary blocker) +- AppsFlyerSDK/appsflyer-flutter-plugin#364 — https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/issues/364 (closed, stale) +- AppsFlyerSDK/appsflyer-flutter-plugin#370 — https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/370 (draft, podspec-only, no manifest) +- AppsFlyerSDK/appsflyer-flutter-plugin#454 — https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/454 (draft, Core-only, move-based, recommended starting point) +- AppsFlyerSDK/appsflyer-flutter-plugin#455 — https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/455 (draft, broader/older, mirror-based) +- AppsFlyerSDK/AppsFlyerFramework `Package.swift` at tags `6.18.0` and `7.0.0` (fetched directly via GitHub API) +- loic-sharma/swiftpm_conditional_compilation — https://github.com/loic-sharma/swiftpm_conditional_compilation (documented hacky workaround, env-var + manual `flutter clean` gated, not recommended for a published plugin) +- Flutter SPM plugin-author guide — https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-plugin-authors (confirms one product per plugin is the only documented/supported pattern) +- Flutter SPM guide for plugin authors — https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-plugin-authors +- Swift Evolution SE-0450 (Package Manager Traits) — https://github.com/swiftlang/swift-evolution/blob/main/proposals/0450-swiftpm-package-traits.md (referenced from within flutter/flutter#161182 itself) +- Official Swift docs on Package Traits — https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/packagetraits/ (used to independently verify traits syntax; confirmed accurate but confirmed **not currently applicable** because Flutter's tooling, not SwiftPM, is the blocker) diff --git a/internal-docs/tech-designs/spm-support.md b/internal-docs/tech-designs/spm-support.md new file mode 100644 index 00000000..5766fa06 --- /dev/null +++ b/internal-docs/tech-designs/spm-support.md @@ -0,0 +1,138 @@ +--- +ticket: DELIVERY-125462 +prd: internal-docs/prds/spm-support.md +research: internal-docs/researches/R-001-spm-support.md +planned_feature_doc: F-060 — doc to be written after development is complete +--- + +# Tech Design: Swift Package Manager (SPM) Support + +## Context table + +| Type | ID | Name | +|------|----|------| +| Issue case | none | `docs/issue-cases/` does not exist in this repo yet — no hot-zone history to check | +| Feature doc | F-054 | Purchase Connector: Build-Time Opt-in — directly extended by this design | + +## Approach + +Move (not mirror) `ios/Classes/` into an SPM-compatible tree shared by both CocoaPods and SPM, following the official Flutter plugin-author SPM migration guide exactly (verified directly at https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-plugin-authors — not just copied from the draft PRs): + +``` +ios/ +├── appsflyer_sdk/ # NEW — SPM package root +│ ├── Package.swift # NEW — SPM manifest +│ └── Sources/appsflyer_sdk/ +│ ├── AppsflyerSdkPlugin.m # moved from ios/Classes/ +│ ├── AppsFlyerAttribution.m # moved +│ ├── AppsFlyerStreamHandler.m # moved +│ └── include/appsflyer_sdk/ +│ ├── AppsflyerSdkPlugin.h # moved (public header) +│ ├── AppsFlyerAttribution.h +│ ├── AppsFlyerStreamHandler.h +│ └── FlutterAppDelegate+AppsFlyerStreamHandler.h +├── appsflyer_sdk.podspec # UPDATED — source_files/public_header_files repointed +├── .gitignore # UPDATED — add .build/ and .swiftpm/ +└── PurchaseConnector/ # UNCHANGED — stays CocoaPods-only, untouched +``` + +`ios/.gitignore` must add `.build/` and `.swiftpm/` per the official migration guide's checklist (step 10) — these are local SPM resolution/build artifacts that must not be committed, same rationale as `.dart_tool/`/`build/` already being ignored at the Dart level. + +This matches draft PR #454's structure (not #455's mirror-based duplication), which the official guide independently confirms is the correct approach: the guide's own migration checklist deletes `ios/Classes/` entirely after moving — there is exactly one copy of Core's source, referenced by both the podspec (CocoaPods path) and `Package.swift` (SPM path). `pubspec.yaml` requires **no changes** — `pluginClass: AppsflyerSdkPlugin` continues to resolve via `` in the new location, per the guide. + +### `ios/appsflyer_sdk/Package.swift` + +```swift +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "appsflyer_sdk", + platforms: [.iOS("12.0")], + products: [ + .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) + ], + dependencies: [ + .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", .exact("6.18.0")) + ], + targets: [ + .target( + name: "appsflyer_sdk", + dependencies: [ + .product(name: "AppsFlyerLib", package: "AppsFlyerFramework") + ], + cSettings: [ + .headerSearchPath("include/appsflyer_sdk") + ] + ) + ] +) +``` + +**Correction to draft PR #454**: its PR description names the dependency product `AppsFlyerLib-Static`. I fetched `AppsFlyerFramework`'s actual `Package.swift` at tag `6.18.0` directly via GitHub API — the declared product name is `AppsFlyerLib`, not `AppsFlyerLib-Static` (that string only appears in the *binary artifact's zip filename*, not the SPM product). Using the wrong product name would fail dependency resolution outright. Pin `.exact("6.18.0")` to match the podspec's existing `ss.ios.dependency 'AppsFlyerFramework','6.18.0'` exactly — no native SDK version bump required (R-001 confirmed the 6.18.0 tag's own Package.swift resolves and is valid). + +**Correction (post-review)**: the original design used `from: "6.18.0"`, a semver-range requirement (`6.18.0..<7.0.0`) rather than an exact pin. This was caught during PR review — the CI E2E run cited in the PR's test plan actually resolved and ran against `AppsFlyerFramework` **6.18.1**, not 6.18.0, exposing a real asymmetry: CocoaPods consumers get exactly 6.18.0, SPM consumers could silently float onto any untested patch/minor release below 7.0.0. Changed to `.exact("6.18.0")` so both distribution paths pin identically. Re-verified via `swift package describe`: `Requirement: Exact: 6.18.0`. + +### `ios/appsflyer_sdk.podspec` — path updates only, no marker needed + +Per the official guide, **no special marker or flag is needed in the podspec to declare SPM availability** — the Flutter tool detects SPM support purely by the presence of `ios/appsflyer_sdk/Package.swift` at the conventional path. The podspec only needs its `Core` subspec's paths repointed to the moved files: + +```ruby +s.subspec 'Core' do |ss| + ss.source_files = 'appsflyer_sdk/Sources/appsflyer_sdk/**/*.m' + ss.public_header_files = 'appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/*.h' + ss.dependency 'Flutter' + ss.ios.dependency 'AppsFlyerFramework','6.18.0' +end +``` + +`PurchaseConnector` subspec is untouched — its `source_files = 'PurchaseConnector/**/*'` still points at the existing, unmoved directory. + +## PurchaseConnector isolation — corrected failure-mode analysis + +R-001 hypothesized that referencing Purchase Connector APIs from an SPM-only integration would fail as a **build/link error** (an improvement over F-054's documented silent-runtime `MissingPluginException`). Tracing the actual mechanism, **this hypothesis is wrong** — the real behavior is identical to today's CocoaPods opt-out path, not better: + +- `ios/PurchaseConnector/` is never added to the SPM target's `Sources/` tree — it's a completely separate directory the `Package.swift` above never references. +- The existing `#ifdef ENABLE_PURCHASE_CONNECTOR` guard in `AppsflyerSdkPlugin.m` (moved, unmodified) depends on the `ENABLE_PURCHASE_CONNECTOR=1` preprocessor macro, which today is set only via the podspec's `pod_target_xcconfig` on the `PurchaseConnector` subspec (a CocoaPods-only mechanism — SPM has no equivalent `xcconfig` macro injection path in this design). +- Therefore in an SPM-only build, that macro is simply never defined — the guard resolves to false exactly as it does today for a CocoaPods app that didn't opt in. +- Net effect: an app integrated via SPM that calls a Purchase Connector Dart API gets the **same outcome as today's undocumented CocoaPods opt-out** — the `af-purchase-connector` MethodChannel has no registered handler, and Flutter raises its own `MissingPluginException` at runtime, not at build time. This is not an improvement; it is the same known limitation F-054 already documents, now reachable via a third path. + +**Decision**: accept this as the same known-limitation behavior, not attempt to introduce a build-time guard for this release. Rationale: making PurchaseConnector fail differently (e.g., a Swift `#error` directive) would require adding conditional logic that reads consuming-app config *inside* the Package.swift/SPM target — which is precisely what flutter/flutter#161182 says SPM cannot yet do for Flutter plugins. Manufacturing a compile-time signal is out of scope until that's resolved; this PRD's non-goal (no SPM Purchase Connector this release) already excludes it. Flag for Phase 3: F-054's Known Limitations section needs a new bullet noting this is now reachable via SPM too, not just the two existing CocoaPods/Gradle paths — and R-001's speculative "improvement" claim should not be repeated in the final feature doc. + +## Migration & rollout risk + +- **No opt-in required, no behavior change for existing users.** CocoaPods apps continue to resolve via the podspec exactly as before — same subspecs, same dependency versions, only the on-disk source location changed (transparent to consumers, who never reference `ios/Classes/` paths directly). +- **Public API surface**: unchanged. No new Dart methods, no MethodChannel changes. This is purely an iOS build/distribution-mechanism addition. +- **Rollback plan**: if a regression surfaces post-release, revert the file move + podspec path change + delete `Package.swift`; CocoaPods consumers are unaffected either way since the podspec keeps working throughout development (verified per-build-path below, not assumed). +- **Big-bang vs gradual**: this ships in v6.18.0 as a single release; SPM adoption itself is gradual and consumer-controlled (Flutter's own `--enable-swift-package-manager` flag / 3.44+ default) — we're not forcing anyone onto SPM, only making it available. + +## Concurrency & Thread Safety + +**N/A for this change.** No runtime or concurrent code path is touched — the `.m`/`.h` files are relocated verbatim (`git mv`, no content changes to the moved implementation), and the only new artifacts (`Package.swift`, podspec path updates, `.gitignore`) are build-time manifests with no executable logic, threading, or callback/completion-handler code of their own. + +## Test Coverage + +**No automated unit test is added.** This falls in the same category as F-054 (Purchase Connector: Build-Time Opt-in), which is explicitly documented as untested at the unit level because "this is a Gradle/CocoaPods build-configuration concern with no Dart or native unit test coverage; verifying it requires two full builds (opted-in vs. opted-out) rather than a unit test." The same reasoning applies here: there is no Dart or native runtime logic change to unit-test — only source-tree layout and build manifests. The Verification plan below (4 real build-path checks) is the equivalent verification for this category of change, not a substitute being skipped. + +## Verification plan (mandatory — neither draft PR completed this) + +Both #454 and #455 self-report only local/simulator builds and explicitly ask reviewers to verify before merging. This design requires actually running all three supported build paths from the PRD's acceptance criteria before shipping, using `example/`: + +1. **SPM, Core only** — `flutter config --enable-swift-package-manager && cd example && flutter clean && flutter build ios --no-codesign`. Confirm init/start/event-logging Dart APIs reach the native layer (existing `example/` app coverage). +2. **CocoaPods, Core only** (`$AppsFlyerPurchaseConnector` unset) — `flutter config --no-enable-swift-package-manager && cd example && flutter clean && pod install && flutter build ios --no-codesign`. Confirm behavior is bit-for-bit identical to pre-change (regression check). +3. **CocoaPods, Core + PurchaseConnector** (`$AppsFlyerPurchaseConnector = true` in `example/ios/Podfile`) — same as above with the flag set. Confirm Purchase Connector channel still registers and responds. +4. **Explicitly not required this release**: SPM + PurchaseConnector — confirm it's genuinely absent/inert per the corrected failure-mode analysis above (attempt calling a Purchase Connector API from an SPM-only build and confirm it raises `MissingPluginException`, matching the documented limitation rather than crashing or hanging). + +All four must be run on a real device build, not just `--no-codesign`, before Alice's implementation review is requested — `--no-codesign` only proves compilation succeeds, not that the native SDK initializes and channels respond. + +## Documentation impact (flag only — action in Phase 3) + +- **F-054** (`docs/features/F-054-purchase-connector-build-time-opt-in.md`): add SPM as a third gating path in its Call Chain/Files sections, and add the corrected failure-mode bullet to Known Limitations (see above) once implementation lands. +- **F-060** (new): this feature's own catalog entry, written in Phase 3 from the real implemented code — supersedes the placeholder discussion from earlier in this session; do not reuse any earlier draft. +- `CHANGELOG.md` and release notes (PRD requirement 5): document SPM support added for Core, PurchaseConnector's continued CocoaPods-only status, and link flutter/flutter#161182 for apps tracking when that might change. + +## Open questions resolved by this design + +- Package.swift path: confirmed `ios/appsflyer_sdk/Package.swift` against the official Flutter guide (not just the drafts) — correct. +- podspec marker: none needed — presence of `Package.swift` at the conventional path is the only signal Flutter tooling requires. +- Compile-time signal for Purchase Connector-without-CocoaPods: corrected from R-001's hypothesis — it's the same runtime `MissingPluginException` as today's CocoaPods opt-out, not a build-time error. Accepted as an existing known limitation, not a regression. diff --git a/ios/.gitignore b/ios/.gitignore index 710ec6cf..62364b2a 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -34,3 +34,6 @@ Icon? .tags* /Flutter/Generated.xcconfig + +.build/ +.swiftpm/ diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 34981194..8a4df2ac 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -18,8 +18,8 @@ Pod::Spec.new do |s| end s.subspec 'Core' do |ss| - ss.source_files = 'Classes/**/*' - ss.public_header_files = 'Classes/**/*.h' + ss.source_files = 'appsflyer_sdk/Sources/appsflyer_sdk/**/*.{h,m}' + ss.public_header_files = 'appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/*.h' ss.dependency 'Flutter' ss.ios.dependency 'AppsFlyerFramework','6.18.0' end diff --git a/ios/appsflyer_sdk/Package.swift b/ios/appsflyer_sdk/Package.swift new file mode 100644 index 00000000..69faf72a --- /dev/null +++ b/ios/appsflyer_sdk/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "appsflyer_sdk", + platforms: [.iOS("12.0")], + products: [ + .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) + ], + dependencies: [ + .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", .exact("6.18.0")) + ], + targets: [ + .target( + name: "appsflyer_sdk", + dependencies: [ + .product(name: "AppsFlyerLib", package: "AppsFlyerFramework") + ], + cSettings: [ + .headerSearchPath("include/appsflyer_sdk") + ] + ) + ] +) diff --git a/ios/Classes/AppsFlyerAttribution.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m similarity index 100% rename from ios/Classes/AppsFlyerAttribution.m rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m diff --git a/ios/Classes/AppsFlyerStreamHandler.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m similarity index 100% rename from ios/Classes/AppsFlyerStreamHandler.m rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m similarity index 100% rename from ios/Classes/AppsflyerSdkPlugin.m rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m diff --git a/ios/Classes/AppsFlyerAttribution.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h similarity index 100% rename from ios/Classes/AppsFlyerAttribution.h rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h diff --git a/ios/Classes/AppsFlyerStreamHandler.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h similarity index 100% rename from ios/Classes/AppsFlyerStreamHandler.h rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h similarity index 100% rename from ios/Classes/AppsflyerSdkPlugin.h rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h diff --git a/ios/Classes/FlutterAppDelegate+AppsFlyerStreamHandler.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h similarity index 100% rename from ios/Classes/FlutterAppDelegate+AppsFlyerStreamHandler.h rename to ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h diff --git a/templates/af-tech-quiz-template.html b/templates/af-tech-quiz-template.html deleted file mode 100644 index cce62f36..00000000 --- a/templates/af-tech-quiz-template.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - -Tech Quiz · {{QUIZ_TITLE}} - - - -
- - -