From 56db9d14ba2b80231c4455789e784874031c9e6d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 20 Jul 2026 10:40:19 +0300 Subject: [PATCH 1/6] Add Swift Package Manager (SPM) support for Core (DELIVERY-125462) Adds ios/appsflyer_sdk/Package.swift for the Core iOS integration, moving ios/Classes/ into an SPM-compatible shared source tree that both CocoaPods and SPM compile from. Purchase Connector stays CocoaPods-only, blocked upstream by flutter/flutter#161182. Updates F-054 and adds F-060 in the feature catalog, plus PRD, research, and tech design docs for the delivery workflow. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + docs/features/F-001-sdk-initialization.md | 6 +- docs/features/F-002-sdk-start.md | 4 +- .../F-003-sdk-plugin-version-retrieval.md | 4 +- docs/features/F-004-in-app-event-logging.md | 4 +- docs/features/F-005-ad-revenue-logging.md | 4 +- .../F-006-custom-host-configuration.md | 4 +- .../F-007-device-id-collection-optout.md | 2 +- .../F-008-manual-imei-android-id-override.md | 2 +- .../F-009-min-time-between-sessions.md | 4 +- docs/features/F-010-currency-code-setting.md | 4 +- ...11-tcf-dma-automatic-consent-collection.md | 4 +- .../F-012-manual-gdpr-dma-consent-api.md | 6 +- docs/features/F-013-user-anonymization.md | 4 +- .../F-014-manual-deep-link-retrigger.md | 2 +- docs/features/F-015-customer-user-id.md | 4 +- .../F-016-update-vs-fresh-install-flag.md | 6 +- docs/features/F-017-sdk-kill-switch.md | 4 +- docs/features/F-018-uninstall-measurement.md | 6 +- docs/features/F-019-user-email-collection.md | 4 +- .../features/F-020-appsflyer-uid-retrieval.md | 4 +- ...-021-delayed-session-start-pending-cuid.md | 6 +- ...push-notification-deep-link-path-config.md | 4 +- .../F-023-in-app-purchase-validation-v1.md | 8 +- .../F-024-in-app-purchase-validation-v2.md | 4 +- ...5-ios-receipt-validation-sandbox-toggle.md | 4 +- docs/features/F-026-additional-custom-data.md | 4 +- ...027-user-invite-link-generation-onelink.md | 8 +- ...028-app-invite-onelink-id-configuration.md | 4 +- ...oss-promotion-impression-click-tracking.md | 8 +- .../F-030-custom-branded-onelink-domains.md | 4 +- .../F-031-push-notification-data-handling.md | 4 +- .../F-032-facebook-deferred-app-links.md | 4 +- docs/features/F-033-skadnetwork-opt-out.md | 6 +- ...vertising-identifier-collection-disable.md | 6 +- .../F-035-conversion-data-callback.md | 8 +- .../F-036-app-open-attribution-callback.md | 8 +- ...nified-deep-linking-callback-and-models.md | 12 +- ...rchase-validation-notification-callback.md | 8 +- ...F-039-native-ios-deep-link-entry-points.md | 16 +- .../F-041-current-device-language-override.md | 6 +- .../F-042-partner-postback-sharing-filter.md | 4 +- .../F-043-out-of-store-install-source.md | 2 +- docs/features/F-044-partner-specific-data.md | 4 +- ...045-deep-link-url-resolution-allow-list.md | 4 +- docs/features/F-046-disable-network-data.md | 2 +- .../F-047-appset-id-collection-optout.md | 2 +- .../F-048-plugin-metadata-reporting.md | 6 +- ...54-purchase-connector-build-time-opt-in.md | 24 ++- ...56-app-invite-link-onelink-id-init-time.md | 4 +- docs/features/F-057-asa-collection-optout.md | 6 +- .../F-058-att-authorization-wait-timeout.md | 6 +- docs/features/F-059-debug-logging-toggle.md | 6 +- .../F-060-swift-package-manager-support.md | 100 +++++++++ docs/features/INDEX.md | 3 +- docs/prds/spm-support.md | 71 ++++++ docs/researches/R-001-spm-support.md | 91 ++++++++ docs/tech-designs/spm-support.md | 136 ++++++++++++ ios/.gitignore | 3 + ios/appsflyer_sdk.podspec | 4 +- ios/appsflyer_sdk/Package.swift | 24 +++ .../appsflyer_sdk}/AppsFlyerAttribution.m | 0 .../appsflyer_sdk}/AppsFlyerStreamHandler.m | 0 .../appsflyer_sdk}/AppsflyerSdkPlugin.m | 0 .../appsflyer_sdk}/AppsFlyerAttribution.h | 0 .../appsflyer_sdk}/AppsFlyerStreamHandler.h | 0 .../appsflyer_sdk}/AppsflyerSdkPlugin.h | 0 ...lutterAppDelegate+AppsFlyerStreamHandler.h | 0 plans/01-ios-spm-core-support.md | 202 ++++++++++++++++++ 69 files changed, 780 insertions(+), 139 deletions(-) create mode 100644 docs/features/F-060-swift-package-manager-support.md create mode 100644 docs/prds/spm-support.md create mode 100644 docs/researches/R-001-spm-support.md create mode 100644 docs/tech-designs/spm-support.md create mode 100644 ios/appsflyer_sdk/Package.swift rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk}/AppsFlyerAttribution.m (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk}/AppsFlyerStreamHandler.m (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk}/AppsflyerSdkPlugin.m (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk}/AppsFlyerAttribution.h (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk}/AppsFlyerStreamHandler.h (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk}/AppsflyerSdkPlugin.h (100%) rename ios/{Classes => appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk}/FlutterAppDelegate+AppsFlyerStreamHandler.h (100%) create mode 100644 plans/01-ios-spm-core-support.md 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/docs/features/F-001-sdk-initialization.md b/docs/features/F-001-sdk-initialization.md index fe659c47..39a3a68a 100644 --- a/docs/features/F-001-sdk-initialization.md +++ b/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/docs/features/F-002-sdk-start.md b/docs/features/F-002-sdk-start.md index 2cd1cf80..e4d4b554 100644 --- a/docs/features/F-002-sdk-start.md +++ b/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/docs/features/F-003-sdk-plugin-version-retrieval.md b/docs/features/F-003-sdk-plugin-version-retrieval.md index 32a05db7..5b031235 100644 --- a/docs/features/F-003-sdk-plugin-version-retrieval.md +++ b/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/docs/features/F-004-in-app-event-logging.md b/docs/features/F-004-in-app-event-logging.md index 0eafa4b7..8cf260b3 100644 --- a/docs/features/F-004-in-app-event-logging.md +++ b/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/docs/features/F-005-ad-revenue-logging.md b/docs/features/F-005-ad-revenue-logging.md index 919bc3d7..d3df7818 100644 --- a/docs/features/F-005-ad-revenue-logging.md +++ b/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/docs/features/F-006-custom-host-configuration.md b/docs/features/F-006-custom-host-configuration.md index 03cd486c..80a1b86f 100644 --- a/docs/features/F-006-custom-host-configuration.md +++ b/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/docs/features/F-007-device-id-collection-optout.md b/docs/features/F-007-device-id-collection-optout.md index ee368596..c09617c3 100644 --- a/docs/features/F-007-device-id-collection-optout.md +++ b/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/docs/features/F-008-manual-imei-android-id-override.md b/docs/features/F-008-manual-imei-android-id-override.md index 3a504be5..35869e17 100644 --- a/docs/features/F-008-manual-imei-android-id-override.md +++ b/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/docs/features/F-009-min-time-between-sessions.md b/docs/features/F-009-min-time-between-sessions.md index 4a60817d..3e3eb6b0 100644 --- a/docs/features/F-009-min-time-between-sessions.md +++ b/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/docs/features/F-010-currency-code-setting.md b/docs/features/F-010-currency-code-setting.md index eee0cb41..4550b6d7 100644 --- a/docs/features/F-010-currency-code-setting.md +++ b/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/docs/features/F-011-tcf-dma-automatic-consent-collection.md b/docs/features/F-011-tcf-dma-automatic-consent-collection.md index 2a9a3431..7bf6f923 100644 --- a/docs/features/F-011-tcf-dma-automatic-consent-collection.md +++ b/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/docs/features/F-012-manual-gdpr-dma-consent-api.md b/docs/features/F-012-manual-gdpr-dma-consent-api.md index 4cb8c8c8..84c962bf 100644 --- a/docs/features/F-012-manual-gdpr-dma-consent-api.md +++ b/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/docs/features/F-013-user-anonymization.md b/docs/features/F-013-user-anonymization.md index cb28b677..725b3ec6 100644 --- a/docs/features/F-013-user-anonymization.md +++ b/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/docs/features/F-014-manual-deep-link-retrigger.md b/docs/features/F-014-manual-deep-link-retrigger.md index fa4c0d5a..7d57405b 100644 --- a/docs/features/F-014-manual-deep-link-retrigger.md +++ b/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/docs/features/F-015-customer-user-id.md b/docs/features/F-015-customer-user-id.md index 641a4f33..83a1fe0e 100644 --- a/docs/features/F-015-customer-user-id.md +++ b/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/docs/features/F-016-update-vs-fresh-install-flag.md b/docs/features/F-016-update-vs-fresh-install-flag.md index 01c946b1..f8bdd8fc 100644 --- a/docs/features/F-016-update-vs-fresh-install-flag.md +++ b/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/docs/features/F-017-sdk-kill-switch.md b/docs/features/F-017-sdk-kill-switch.md index 950dc22b..064a0829 100644 --- a/docs/features/F-017-sdk-kill-switch.md +++ b/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/docs/features/F-018-uninstall-measurement.md b/docs/features/F-018-uninstall-measurement.md index 3b6eff1a..f7d12595 100644 --- a/docs/features/F-018-uninstall-measurement.md +++ b/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/docs/features/F-019-user-email-collection.md b/docs/features/F-019-user-email-collection.md index e7e28c08..adb200f3 100644 --- a/docs/features/F-019-user-email-collection.md +++ b/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/docs/features/F-020-appsflyer-uid-retrieval.md b/docs/features/F-020-appsflyer-uid-retrieval.md index 9cf10151..2fec51c9 100644 --- a/docs/features/F-020-appsflyer-uid-retrieval.md +++ b/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/docs/features/F-021-delayed-session-start-pending-cuid.md b/docs/features/F-021-delayed-session-start-pending-cuid.md index 38a0dc86..44f71c32 100644 --- a/docs/features/F-021-delayed-session-start-pending-cuid.md +++ b/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/docs/features/F-022-push-notification-deep-link-path-config.md b/docs/features/F-022-push-notification-deep-link-path-config.md index 33450004..1cd63e82 100644 --- a/docs/features/F-022-push-notification-deep-link-path-config.md +++ b/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/docs/features/F-023-in-app-purchase-validation-v1.md b/docs/features/F-023-in-app-purchase-validation-v1.md index 3ca1eb96..34a452e7 100644 --- a/docs/features/F-023-in-app-purchase-validation-v1.md +++ b/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/docs/features/F-024-in-app-purchase-validation-v2.md b/docs/features/F-024-in-app-purchase-validation-v2.md index 1297c104..bfc6ed29 100644 --- a/docs/features/F-024-in-app-purchase-validation-v2.md +++ b/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/docs/features/F-025-ios-receipt-validation-sandbox-toggle.md b/docs/features/F-025-ios-receipt-validation-sandbox-toggle.md index cca546f1..a5f9c0ec 100644 --- a/docs/features/F-025-ios-receipt-validation-sandbox-toggle.md +++ b/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/docs/features/F-026-additional-custom-data.md b/docs/features/F-026-additional-custom-data.md index 58b90049..6a3be71a 100644 --- a/docs/features/F-026-additional-custom-data.md +++ b/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/docs/features/F-027-user-invite-link-generation-onelink.md b/docs/features/F-027-user-invite-link-generation-onelink.md index 93a6fab1..7c3a1d75 100644 --- a/docs/features/F-027-user-invite-link-generation-onelink.md +++ b/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/docs/features/F-028-app-invite-onelink-id-configuration.md b/docs/features/F-028-app-invite-onelink-id-configuration.md index 051d4ca3..64312d70 100644 --- a/docs/features/F-028-app-invite-onelink-id-configuration.md +++ b/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/docs/features/F-029-cross-promotion-impression-click-tracking.md b/docs/features/F-029-cross-promotion-impression-click-tracking.md index 1259af17..990bcfb8 100644 --- a/docs/features/F-029-cross-promotion-impression-click-tracking.md +++ b/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/docs/features/F-030-custom-branded-onelink-domains.md b/docs/features/F-030-custom-branded-onelink-domains.md index a7dec9ac..6723dd5b 100644 --- a/docs/features/F-030-custom-branded-onelink-domains.md +++ b/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/docs/features/F-031-push-notification-data-handling.md b/docs/features/F-031-push-notification-data-handling.md index ab918c87..01d10958 100644 --- a/docs/features/F-031-push-notification-data-handling.md +++ b/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/docs/features/F-032-facebook-deferred-app-links.md b/docs/features/F-032-facebook-deferred-app-links.md index 811ab6ee..cbee6a4f 100644 --- a/docs/features/F-032-facebook-deferred-app-links.md +++ b/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/docs/features/F-033-skadnetwork-opt-out.md b/docs/features/F-033-skadnetwork-opt-out.md index e718edab..afd797d7 100644 --- a/docs/features/F-033-skadnetwork-opt-out.md +++ b/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/docs/features/F-034-advertising-identifier-collection-disable.md b/docs/features/F-034-advertising-identifier-collection-disable.md index 91554862..53e80cfd 100644 --- a/docs/features/F-034-advertising-identifier-collection-disable.md +++ b/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/docs/features/F-035-conversion-data-callback.md b/docs/features/F-035-conversion-data-callback.md index f5372ae3..91d849aa 100644 --- a/docs/features/F-035-conversion-data-callback.md +++ b/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/docs/features/F-036-app-open-attribution-callback.md b/docs/features/F-036-app-open-attribution-callback.md index a7589714..37590cf6 100644 --- a/docs/features/F-036-app-open-attribution-callback.md +++ b/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/docs/features/F-037-unified-deep-linking-callback-and-models.md b/docs/features/F-037-unified-deep-linking-callback-and-models.md index 3ee6e5bf..25c94177 100644 --- a/docs/features/F-037-unified-deep-linking-callback-and-models.md +++ b/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/docs/features/F-038-legacy-purchase-validation-notification-callback.md b/docs/features/F-038-legacy-purchase-validation-notification-callback.md index 16e4b255..222f6d9c 100644 --- a/docs/features/F-038-legacy-purchase-validation-notification-callback.md +++ b/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/docs/features/F-039-native-ios-deep-link-entry-points.md b/docs/features/F-039-native-ios-deep-link-entry-points.md index 8f0cd2d5..5904492a 100644 --- a/docs/features/F-039-native-ios-deep-link-entry-points.md +++ b/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/docs/features/F-041-current-device-language-override.md b/docs/features/F-041-current-device-language-override.md index 674426bf..a6ad6617 100644 --- a/docs/features/F-041-current-device-language-override.md +++ b/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/docs/features/F-042-partner-postback-sharing-filter.md b/docs/features/F-042-partner-postback-sharing-filter.md index 0961c8ad..f2330a49 100644 --- a/docs/features/F-042-partner-postback-sharing-filter.md +++ b/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/docs/features/F-043-out-of-store-install-source.md b/docs/features/F-043-out-of-store-install-source.md index cefd00b2..ab259544 100644 --- a/docs/features/F-043-out-of-store-install-source.md +++ b/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/docs/features/F-044-partner-specific-data.md b/docs/features/F-044-partner-specific-data.md index 6688b9c8..263ba57c 100644 --- a/docs/features/F-044-partner-specific-data.md +++ b/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/docs/features/F-045-deep-link-url-resolution-allow-list.md b/docs/features/F-045-deep-link-url-resolution-allow-list.md index e40dcb16..54af80a2 100644 --- a/docs/features/F-045-deep-link-url-resolution-allow-list.md +++ b/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/docs/features/F-046-disable-network-data.md b/docs/features/F-046-disable-network-data.md index 659c435b..c01c26f8 100644 --- a/docs/features/F-046-disable-network-data.md +++ b/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/docs/features/F-047-appset-id-collection-optout.md b/docs/features/F-047-appset-id-collection-optout.md index 192787e0..a42a6a2b 100644 --- a/docs/features/F-047-appset-id-collection-optout.md +++ b/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/docs/features/F-048-plugin-metadata-reporting.md b/docs/features/F-048-plugin-metadata-reporting.md index 58b0223b..65cfcf93 100644 --- a/docs/features/F-048-plugin-metadata-reporting.md +++ b/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/docs/features/F-054-purchase-connector-build-time-opt-in.md b/docs/features/F-054-purchase-connector-build-time-opt-in.md index a6b7aced..6badbc0e 100644 --- a/docs/features/F-054-purchase-connector-build-time-opt-in.md +++ b/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/docs/features/F-056-app-invite-link-onelink-id-init-time.md b/docs/features/F-056-app-invite-link-onelink-id-init-time.md index cb9950cd..854faa07 100644 --- a/docs/features/F-056-app-invite-link-onelink-id-init-time.md +++ b/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/docs/features/F-057-asa-collection-optout.md b/docs/features/F-057-asa-collection-optout.md index ff675b36..d7d526d3 100644 --- a/docs/features/F-057-asa-collection-optout.md +++ b/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/docs/features/F-058-att-authorization-wait-timeout.md b/docs/features/F-058-att-authorization-wait-timeout.md index 78276563..a467a2c0 100644 --- a/docs/features/F-058-att-authorization-wait-timeout.md +++ b/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/docs/features/F-059-debug-logging-toggle.md b/docs/features/F-059-debug-logging-toggle.md index 04a3bec6..453bccb0 100644 --- a/docs/features/F-059-debug-logging-toggle.md +++ b/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/docs/features/F-060-swift-package-manager-support.md b/docs/features/F-060-swift-package-manager-support.md new file mode 100644 index 00000000..ff0660cf --- /dev/null +++ b/docs/features/F-060-swift-package-manager-support.md @@ -0,0 +1,100 @@ +--- +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 from: "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, `from: "6.18.0"`. | +| `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 range `6.18.0..<7.0.0` 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). + +> **Outstanding pre-release gate**: the tech design's mandatory 4-path real-device build verification (SPM Core-only / CocoaPods Core-only / CocoaPods Core+PurchaseConnector / confirming SPM+PurchaseConnector is inert, all on real devices, not `--no-codesign` alone) has **not yet been run** — it requires a full macOS/Xcode/iOS-device environment that was unavailable during implementation. This must be completed before this ships (before promoting through the RC pipeline). See `docs/tech-designs/spm-support.md` for the exact verification steps. + +--- + +## 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 (`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 `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/docs/features/INDEX.md b/docs/features/INDEX.md index ca40fb1f..7ffa9557 100644 --- a/docs/features/INDEX.md +++ b/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/docs/prds/spm-support.md b/docs/prds/spm-support.md new file mode 100644 index 00000000..3cd10f0d --- /dev/null +++ b/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/docs/researches/R-001-spm-support.md b/docs/researches/R-001-spm-support.md new file mode 100644 index 00000000..4b14358f --- /dev/null +++ b/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/docs/tech-designs/spm-support.md b/docs/tech-designs/spm-support.md new file mode 100644 index 00000000..8788f0d5 --- /dev/null +++ b/docs/tech-designs/spm-support.md @@ -0,0 +1,136 @@ +--- +ticket: DELIVERY-125462 +prd: docs/prds/spm-support.md +research: 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", from: "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 `from: "6.18.0"` to match the podspec's existing `ss.ios.dependency 'AppsFlyerFramework','6.18.0'` — no native SDK version bump required (R-001 confirmed the 6.18.0 tag's own Package.swift resolves and is valid). + +### `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..10aa08a0 --- /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", from: "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/plans/01-ios-spm-core-support.md b/plans/01-ios-spm-core-support.md new file mode 100644 index 00000000..7efc339f --- /dev/null +++ b/plans/01-ios-spm-core-support.md @@ -0,0 +1,202 @@ +# Plan: Add Swift Package Manager (SPM) support to Core (DELIVERY-125462) + +Jira: https://appsflyer.atlassian.net/browse/DELIVERY-125462 (P1, assignee: Kobi Kagan, target: end of July 2026, ships inside v6.18.0 line) + +## Goal + +Add a `Package.swift` for the plugin's **Core** module so SPM-enabled Flutter apps (Flutter 3.44+ default) can build against this plugin without CocoaPods, while: +- Keeping the **PurchaseConnector** subspec CocoaPods-only (blocked upstream by [flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182) — no SPM opt-in mechanism exists in Flutter tooling today) +- Preserving 100% CocoaPods backward compatibility for apps not yet on SPM + +--- + +## Phase 0: Documentation Discovery (consolidated findings — do not re-derive, cite these) + +### A. Prior art in this repo — three existing draft PRs, none merged + +| PR | Approach | Verdict | +|---|---|---| +| [#454](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/454) (`nurlangarash`) | True `git mv` of `Classes/*` → `ios/appsflyer_sdk/Sources/appsflyer_sdk/` (+headers → `.../include/appsflyer_sdk/`), podspec updated to match, single source of truth. Depends on `AppsFlyerFramework-Static` / product `AppsFlyerLib-Static` (matches `static_framework = true`). Copilot flagged `.iOS("12.0")` (invalid) and product/target name mismatch — **both already fixed** in follow-up commit `98d9938dd2`. PurchaseConnector explicitly untouched. | **Use this as the base.** | +| [#455](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/455) (`TeddyYeung`) | Duplicates Core sources into a second tree, leaves podspec pointing at old `Classes/` — two copies to maintain forever. Same `.iOS("12.0")` bug, **never fixed**. Adds `.gitignore` entries (`.build/`, `.swiftpm/`) — worth cherry-picking. | Reject the architecture; take only the `.gitignore` hunk. | +| [#370](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/370) (`alejandro-all-win-software`, oldest) | Tried to fold PurchaseConnector itself into SPM via an env-var-gated target (`ENABLE_PURCHASE_CONNECTOR=1`) + dependency on `appsflyer-apple-purchase-connector`. Author's own comment: *"blocked by flutter/flutter#161182... if you find another way to opt in to Purchase Connector, I'd be happy to close this PR in favor of that approach."* | **Do not repeat this.** This is exactly the dead end DELIVERY-125462 tells us to route around by staying CocoaPods-only for PurchaseConnector. | + +None of the three have maintainer review; all are `REVIEW_REQUIRED`/`BLOCKED` on branch protection only (CI/security scans pass on all three). + +### B. Ground-truth structure (verified against a real, live, first-party plugin: `image_picker_ios` in `flutter/packages`, not just docs prose) + +Target layout (adapted to our plugin, matches what #454 already did): +``` +ios/appsflyer_sdk.podspec # unchanged location, paths updated +ios/appsflyer_sdk/Package.swift # new +ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m +ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m +ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m +ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h +ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h +ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h +ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h +``` +Our case is simpler than `image_picker_ios`: our podspec has never set a custom `s.module_map`, so no umbrella header / `.modulemap` file is needed — CocoaPods' and SwiftPM's default module generation both suffice. Don't add one (that would be inventing a requirement we don't have). + +`Package.swift` (the corrected version from #454's follow-up commit is the right shape): +```swift +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "appsflyer_sdk", + platforms: [.iOS(.v12)], + products: [ + .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) + ], + dependencies: [ + .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static.git", exact: "6.18.0") + ], + targets: [ + .target( + name: "appsflyer_sdk", + dependencies: [ + .product(name: "AppsFlyerLib-Static", package: "AppsFlyerFramework-Static") + ], + path: "Sources/appsflyer_sdk", + cSettings: [ + .headerSearchPath("include/appsflyer_sdk") + ] + ) + ] +) +``` +Anti-pattern guard (from Copilot's actual review on #454): `.iOS("12.0")` is **not** valid SwiftPM API — must be `.iOS(.v12)`. Library name uses hyphens (`appsflyer-sdk`), target/package name keeps underscores (`appsflyer_sdk`) — this is Flutter's documented convention, not a typo. + +Podspec `Core` subspec path update (mirrors #454 exactly): +```ruby +# before +ss.source_files = 'Classes/**/*' +ss.public_header_files = 'Classes/**/*.h' +# after +ss.source_files = 'appsflyer_sdk/Sources/appsflyer_sdk/**/*.{h,m}' +ss.public_header_files = 'appsflyer_sdk/Sources/appsflyer_sdk/include/**/*.h' +``` +`PurchaseConnector` subspec: **zero changes.** + +### C. Repo conventions (fact-checked, not assumed) + +- **CHANGELOG.md**: `## ` header, flat `-` bullets, newest on top. Current top entry is `## 6.18.0`. +- **pubspec.yaml**: `version: 6.18.0`, no existing SPM-related keys — none are required; Flutter auto-detects `Package.swift` by convention path, no pubspec opt-in needed. +- **CI**: `.github/workflows/lint-test-build.yml` (`build-ios` job) and `ios-e2e.yml` (`e2e-ios` job) both do `pod install` + `flutter build ios` — pure CocoaPods today, zero SPM verification exists anywhere in CI. +- **Docs**: `doc/Installation.md` has no SPM section at all today. `doc/PurchaseConnector.md:74-76` documents the CocoaPods-only opt-in (`$AppsFlyerPurchaseConnector = true` in Podfile) — this needs a caveat added (see Phase 4). +- **PurchaseConnector opt-in today**, confirmed by repo-wide grep, is exclusively: `if defined?($AppsFlyerPurchaseConnector)` in the podspec, set by the consumer's own Podfile. The example app itself does **not** set this flag. +- Public header surface for `Core` is exactly 4 files: `AppsFlyerAttribution.h`, `AppsFlyerStreamHandler.h`, `AppsflyerSdkPlugin.h`, `FlutterAppDelegate+AppsFlyerStreamHandler.h`. +- `AppsflyerSdkPlugin.m` already guards PurchaseConnector registration behind `#ifdef ENABLE_PURCHASE_CONNECTOR` (a preprocessor flag CocoaPods sets via `GCC_PREPROCESSOR_DEFINITIONS`). **This flag is never defined in the new `Package.swift`, so that code path is simply compiled out under SPM — no dangling reference, nothing to fix here.** + +### D. Known architectural limitation to surface, not hide + +Per Flutter's own SPM integration model: once a plugin ships a `Package.swift`, Flutter routes that **entire plugin** through SPM for any app that has SPM enabled — the podspec's fallback Podfile path is not used for that plugin at all in that mode. That means **an SPM-enabled consumer app cannot reach the `PurchaseConnector` subspec or its `$AppsFlyerPurchaseConnector` Podfile flag at all** — not "it might not work," but "the mechanism that would enable it never runs." This is the concrete, user-facing shape of the flutter/flutter#161182 blocker, and it must be stated explicitly in docs (Phase 4), not left for users to discover as a silent failure. + +### E. Do SwiftPM "Package Traits" close this gap? No — checked and ruled out, don't revisit without new evidence + +SwiftPM added **Package Traits** in Swift 6.1 ([docs.swift.org/swiftpm/.../packagetraits](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/packagetraits/)) — a real, build-time optional-feature mechanism (`traits: [.default(enabledTraits:...), .trait(name:...)]` in the package author's manifest; conditional compilation via `#if TraitName`). This is architecturally the kind of thing that could gate an optional `PurchaseConnector` target. It does **not** change our plan, for two concrete reasons: + +1. **Traits are enabled by the consumer's own `Package.swift`** — `.package(url: ..., traits: [.init(name: "PurchaseConnector")])` — or via CLI (`swift build --traits X`). Flutter apps don't have a hand-authored `Package.swift`; Flutter's tooling *generates* `FlutterGeneratedPluginSwiftPackage` automatically from `pubspec.yaml`, and that generation pipeline has **no trait-selection input** today. There is nowhere in `pubspec.yaml` or `flutter build` for an app developer to say "enable the PurchaseConnector trait." This is exactly the surface flutter/flutter#161182 would need to add — it remains open/unresolved as of this writing. +2. **Requires `swift-tools-version: 6.1`.** Our `Package.swift` (Phase 2) is `5.9`, matching Flutter's own official template and PR #454's precedent, chosen for the widest Xcode/toolchain compatibility against our `iOS 12` deployment target. Bumping to 6.1 to get traits would raise the minimum Xcode/Swift toolchain for every consumer, for a feature-gate mechanism Flutter can't even plumb through yet — not a reasonable trade today. + +**Conclusion: no plan change.** Keep Core-only SPM + CocoaPods-only PurchaseConnector as decided. Revisit only if Flutter ships trait pass-through from `pubspec.yaml`/`flutter build` (i.e. flutter/flutter#161182 or a successor issue closes with that shape) — at that point, adding a `purchase_connector` trait to `Package.swift` would be the natural next step, gated on bumping `swift-tools-version` to 6.1+. + +--- + +## Phase 1: Establish the branch from PR #454 + +**What to do:** +1. `gh pr checkout 454` (or fetch `nurlangarash:feat/swift-package-manager-support`) into a new local branch off current `master`. +2. Rebase onto current `master` HEAD (`df7f4854`) — resolve `CHANGELOG.md` conflicts by keeping master's `## 6.18.0` entry intact and adding the new SPM entry above/alongside it per Phase 3, not overwriting it. +3. Confirm the rebased diff still matches the structure in Phase 0.B exactly — no drift from master's current `Classes/*` file set (master has 4 headers + `AppsFlyerAttribution.m`/`AppsFlyerStreamHandler.m`/`AppsflyerSdkPlugin.m`; confirm #454's `git mv` list is unchanged since June 22). + +**Verification:** `git diff master --stat` shows only expected renames/adds — no unexpected deletions, no `ios/PurchaseConnector/**` touched, no `ios/Classes/**` files left behind untouched (they should all be gone, replaced by the SPM tree — CocoaPods now points at the new path per Phase 0.B). + +**Anti-pattern guard:** Do not adopt #455's duplication approach even partially — no two copies of the same `.m`/`.h` file should exist after this phase. + +--- + +## Phase 2: Structural fixes & cleanup + +**What to do:** +1. Cherry-pick #455's `.gitignore` addition: `.build/`, `.swiftpm/`. +2. Confirm `Package.swift` matches the corrected form in Phase 0.B exactly (`.iOS(.v12)`, not `.iOS("12.0")`; product `AppsFlyerLib-Static` from `AppsFlyerFramework-Static`, pinned `exact: "6.18.0"` to match the podspec's `AppsFlyerFramework` pin). +3. Confirm `cSettings: [.headerSearchPath("include/appsflyer_sdk")]` is present (required for the `.m` files' `#import` statements to resolve). +4. Confirm all 4 public headers live under `Sources/appsflyer_sdk/include/appsflyer_sdk/` and the 3 `.m` files live directly under `Sources/appsflyer_sdk/` (not under `include/`). + +**Verification:** +- `grep -rn "ENABLE_PURCHASE_CONNECTOR" ios/appsflyer_sdk/Sources/` — confirm the `#ifdef` guard is untouched and no unconditional reference to `PurchaseConnectorPlugin` was introduced. +- `pod lib lint ios/appsflyer_sdk.podspec --configuration=Debug --skip-tests --use-modular-headers` passes (validates the CocoaPods path still resolves against the new file paths). + +**Anti-pattern guard:** Do not add a `.modulemap`/umbrella header — our podspec never had one; don't invent structure `image_picker_ios` needed for reasons that don't apply here. + +--- + +## Phase 3: Version bump & CHANGELOG + +**Confirmed baseline (verified directly against `releases/6.x.x/6.18.x/6.18.0-rc1`, the actual last-released branch — identical to current `master`, nothing later exists in this repo):** +- Plugin version: `6.18.0` +- iOS Core `AppsFlyerFramework`: `6.18.0` +- iOS `PurchaseConnector` (podspec: `ss.ios.dependency 'PurchaseConnector', '6.18.0'`): `6.18.0` +- Android `purchase-connector` (`build.gradle`: `implementation 'com.appsflyer:purchase-connector:2.2.0'`): `2.2.0` + +This is a **plugin-only** change (adds SPM plumbing, does not touch native SDK versions) — so none of the above native pins change. `AppsFlyerFramework-Static` in `Package.swift` (Phase 2) must pin `exact: "6.18.0"` to match, not any other number. + +**What to do:** +1. Bump `pubspec.yaml` `version:` from `6.18.0` to `6.18.0+1` (matches repo's existing `+N` build-suffix convention for same-SDK-version plugin updates, e.g. `6.17.7+1` seen in CHANGELOG history). +2. Add new top `CHANGELOG.md` entry above `## 6.18.0`: + ``` + ## 6.18.0+1 + + - Added Swift Package Manager (SPM) support for the Core module (iOS). PurchaseConnector remains CocoaPods-only pending flutter/flutter#161182 (iOS PurchaseConnector 6.18.0 / Android purchase-connector 2.2.0 unchanged). + ``` + +**Verification:** `grep -A3 "^## 6.18.0+1" CHANGELOG.md` shows the new entry; `pubspec.yaml` version matches; confirm no native dependency version in podspec/build.gradle/Package.swift was changed by this phase. + +**Note:** Confirm this version number against whatever the `rc-release` skill / RC pipeline expects before tagging — don't hardcode a release version without checking the active RC process. + +--- + +## Phase 4: Documentation updates + +**What to do:** +1. `doc/Installation.md`: add a new "Swift Package Manager" section documenting that Core supports SPM as of this version, and that apps must still use CocoaPods if they need PurchaseConnector. +2. `doc/PurchaseConnector.md`: add an explicit caveat near the "How to Opt-In" section (lines ~66-82): *PurchaseConnector requires CocoaPods; it is not available in SPM-enabled apps until [flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182) is resolved.* +3. `README.md`: no structural change needed (it only links to `doc/Installation.md`), but confirm the SDK Versions table still matches `6.18.0`/`AppsFlyerFramework-Static` if that pin changes. + +**Verification:** Manual read-through; confirm no doc implies PurchaseConnector "might work" under SPM — it must state plainly that it does not. + +**Anti-pattern guard:** Do not word this as "partial support" or "coming soon" — per Phase 0.D this is a hard mechanism gap, not a rough edge. + +--- + +## Phase 5: CI verification + +**What to do:** +1. Confirm existing `lint-test-build.yml` (`build-ios`) and `ios-e2e.yml` (`e2e-ios`) jobs still pass unmodified — these exercise the CocoaPods path (regression check for apps not on SPM). +2. Add a new step or job (e.g. `build-ios-spm` in `lint-test-build.yml`) that runs against the `example/` app with SPM enabled (`flutter config --enable-swift-package-manager`) and does `flutter build ios --no-codesign` — verifying the Core-only SPM path resolves and links. +3. Manually verify (not necessarily CI-gated, since it's an intentional non-feature) that an SPM-enabled example app cannot reach PurchaseConnector — i.e. confirm there's no `pod install` step running for `appsflyer_sdk` at all in that mode, consistent with Phase 0.D. + +**Full verification matrix required by the ticket:** + +| Build mode | PurchaseConnector requested? | Expected result | +|---|---|---| +| CocoaPods (existing) | No | Builds, Core only — unchanged from today | +| CocoaPods (existing) | Yes (`$AppsFlyerPurchaseConnector = true`) | Builds with PurchaseConnector — unchanged from today | +| SPM (new) | No | Builds, Core only — **new capability** | +| SPM (new) | Yes (attempted) | No mechanism to opt in — confirm this fails/is absent cleanly, not silently broken | + +**Anti-pattern guard:** Don't treat the CI job as "just make the SPM path build once" — the ticket explicitly requires verifying all four rows above. + +--- + +## Final Phase: Sign-off checklist + +1. `git diff master --stat` reviewed — matches Phase 0/1/2 scope exactly, nothing extra. +2. All four build-matrix rows in Phase 5 verified with evidence (CI logs or local build output). +3. `CHANGELOG.md` and `pubspec.yaml` version bumped per Phase 3. +4. `doc/Installation.md` and `doc/PurchaseConnector.md` updated per Phase 4, explicitly stating the PurchaseConnector/SPM limitation. +5. No `.iOS("12.0")`-style invalid SwiftPM API left in `Package.swift` (`grep -n '\.iOS(\"' ios/appsflyer_sdk/Package.swift` should return nothing). +6. No duplicate source files between `ios/Classes/` (should no longer exist) and `ios/appsflyer_sdk/Sources/`. +7. Ready to open a PR against DELIVERY-125462, referencing and closing out #454/#455/#370 in the description (crediting their work, explaining why #454 was chosen as base). From 5a0630be8db67b4c082bd9036e0f27094df97f47 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Tue, 21 Jul 2026 19:24:11 +0300 Subject: [PATCH 2/6] Fix SPM version-pin asymmetry with CocoaPods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package.swift pinned AppsFlyerFramework with `from: "6.18.0"` (a semver range, ..<7.0.0), while the podspec pins CocoaPods to exactly 6.18.0. CI's own iOS E2E run exposed the drift: it resolved and ran against 6.18.1 via SPM. Switch to `.exact("6.18.0")` so both distribution paths pin identically, and correct the docs/plan that described the old pin as correct. Also retracts a stale claim in plans/01-ios-spm-core-support.md that `.iOS("12.0")` is invalid SwiftPM syntax — verified valid via `swift package describe`. Co-Authored-By: Claude Sonnet 5 --- docs/features/F-060-swift-package-manager-support.md | 4 ++-- docs/tech-designs/spm-support.md | 6 ++++-- ios/appsflyer_sdk/Package.swift | 2 +- plans/01-ios-spm-core-support.md | 10 +++++----- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/features/F-060-swift-package-manager-support.md b/docs/features/F-060-swift-package-manager-support.md index ff0660cf..68a28995 100644 --- a/docs/features/F-060-swift-package-manager-support.md +++ b/docs/features/F-060-swift-package-manager-support.md @@ -39,7 +39,7 @@ Shared source tree (used by both paths, single copy — no duplication): 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 from: "6.18.0" + → 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 @@ -54,7 +54,7 @@ CocoaPods path (resolved by `pod install` at install time, unchanged behavior): ## 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, `from: "6.18.0"`. | +| `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. | diff --git a/docs/tech-designs/spm-support.md b/docs/tech-designs/spm-support.md index 8788f0d5..cc6a78f5 100644 --- a/docs/tech-designs/spm-support.md +++ b/docs/tech-designs/spm-support.md @@ -53,7 +53,7 @@ let package = Package( .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) ], dependencies: [ - .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", from: "6.18.0") + .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", .exact("6.18.0")) ], targets: [ .target( @@ -69,7 +69,9 @@ let package = Package( ) ``` -**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 `from: "6.18.0"` to match the podspec's existing `ss.ios.dependency 'AppsFlyerFramework','6.18.0'` — no native SDK version bump required (R-001 confirmed the 6.18.0 tag's own Package.swift resolves and is valid). +**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 diff --git a/ios/appsflyer_sdk/Package.swift b/ios/appsflyer_sdk/Package.swift index 10aa08a0..69faf72a 100644 --- a/ios/appsflyer_sdk/Package.swift +++ b/ios/appsflyer_sdk/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) ], dependencies: [ - .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", from: "6.18.0") + .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework.git", .exact("6.18.0")) ], targets: [ .target( diff --git a/plans/01-ios-spm-core-support.md b/plans/01-ios-spm-core-support.md index 7efc339f..97b4244f 100644 --- a/plans/01-ios-spm-core-support.md +++ b/plans/01-ios-spm-core-support.md @@ -16,8 +16,8 @@ Add a `Package.swift` for the plugin's **Core** module so SPM-enabled Flutter ap | PR | Approach | Verdict | |---|---|---| -| [#454](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/454) (`nurlangarash`) | True `git mv` of `Classes/*` → `ios/appsflyer_sdk/Sources/appsflyer_sdk/` (+headers → `.../include/appsflyer_sdk/`), podspec updated to match, single source of truth. Depends on `AppsFlyerFramework-Static` / product `AppsFlyerLib-Static` (matches `static_framework = true`). Copilot flagged `.iOS("12.0")` (invalid) and product/target name mismatch — **both already fixed** in follow-up commit `98d9938dd2`. PurchaseConnector explicitly untouched. | **Use this as the base.** | -| [#455](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/455) (`TeddyYeung`) | Duplicates Core sources into a second tree, leaves podspec pointing at old `Classes/` — two copies to maintain forever. Same `.iOS("12.0")` bug, **never fixed**. Adds `.gitignore` entries (`.build/`, `.swiftpm/`) — worth cherry-picking. | Reject the architecture; take only the `.gitignore` hunk. | +| [#454](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/454) (`nurlangarash`) | True `git mv` of `Classes/*` → `ios/appsflyer_sdk/Sources/appsflyer_sdk/` (+headers → `.../include/appsflyer_sdk/`), podspec updated to match, single source of truth. Depends on `AppsFlyerFramework-Static` / product `AppsFlyerLib-Static` (matches `static_framework = true`) — this product name is wrong, corrected to `AppsFlyerLib` during implementation. Copilot flagged `.iOS("12.0")` as invalid and a product/target name mismatch — the name mismatch was real, but the `.iOS("12.0")` flag was itself wrong (verified valid via `swift package describe`; not something that needed fixing). PurchaseConnector explicitly untouched. | **Use this as the base.** | +| [#455](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/455) (`TeddyYeung`) | Duplicates Core sources into a second tree, leaves podspec pointing at old `Classes/` — two copies to maintain forever. Uses the same `.iOS("12.0")` syntax, which is valid (see #454's corrected note) and was never actually a bug. Adds `.gitignore` entries (`.build/`, `.swiftpm/`) — worth cherry-picking. | Reject the architecture; take only the `.gitignore` hunk. | | [#370](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/370) (`alejandro-all-win-software`, oldest) | Tried to fold PurchaseConnector itself into SPM via an env-var-gated target (`ENABLE_PURCHASE_CONNECTOR=1`) + dependency on `appsflyer-apple-purchase-connector`. Author's own comment: *"blocked by flutter/flutter#161182... if you find another way to opt in to Purchase Connector, I'd be happy to close this PR in favor of that approach."* | **Do not repeat this.** This is exactly the dead end DELIVERY-125462 tells us to route around by staying CocoaPods-only for PurchaseConnector. | None of the three have maintainer review; all are `REVIEW_REQUIRED`/`BLOCKED` on branch protection only (CI/security scans pass on all three). @@ -66,7 +66,7 @@ let package = Package( ] ) ``` -Anti-pattern guard (from Copilot's actual review on #454): `.iOS("12.0")` is **not** valid SwiftPM API — must be `.iOS(.v12)`. Library name uses hyphens (`appsflyer-sdk`), target/package name keeps underscores (`appsflyer_sdk`) — this is Flutter's documented convention, not a typo. +~~Anti-pattern guard (from Copilot's actual review on #454): `.iOS("12.0")` is **not** valid SwiftPM API — must be `.iOS(.v12)`.~~ **Corrected during implementation review**: this claim is wrong. `SupportedPlatform.IOSVersion` conforms to `ExpressibleByStringLiteral`, so `.iOS("12.0")` is valid and compiles. Verified directly: the shipped `Package.swift` uses `.iOS("12.0")` and `swift package describe` resolves `Platforms: Name: ios, Version: 12.0` with no error. Both `.iOS("12.0")` and `.iOS(.v12)` work; this repo uses the former. Library name uses hyphens (`appsflyer-sdk`), target/package name keeps underscores (`appsflyer_sdk`) — this is Flutter's documented convention, not a typo. Podspec `Core` subspec path update (mirrors #454 exactly): ```ruby @@ -121,7 +121,7 @@ SwiftPM added **Package Traits** in Swift 6.1 ([docs.swift.org/swiftpm/.../packa **What to do:** 1. Cherry-pick #455's `.gitignore` addition: `.build/`, `.swiftpm/`. -2. Confirm `Package.swift` matches the corrected form in Phase 0.B exactly (`.iOS(.v12)`, not `.iOS("12.0")`; product `AppsFlyerLib-Static` from `AppsFlyerFramework-Static`, pinned `exact: "6.18.0"` to match the podspec's `AppsFlyerFramework` pin). +2. Confirm `Package.swift` matches the shipped, verified form: `.iOS("12.0")` (confirmed valid — see Phase 0.B correction); product `AppsFlyerLib` from `AppsFlyerFramework` (not `AppsFlyerLib-Static`/`AppsFlyerFramework-Static` — that naming was wrong, see #454 correction above), pinned `.exact("6.18.0")` to match the podspec's `AppsFlyerFramework` pin exactly. 3. Confirm `cSettings: [.headerSearchPath("include/appsflyer_sdk")]` is present (required for the `.m` files' `#import` statements to resolve). 4. Confirm all 4 public headers live under `Sources/appsflyer_sdk/include/appsflyer_sdk/` and the 3 `.m` files live directly under `Sources/appsflyer_sdk/` (not under `include/`). @@ -197,6 +197,6 @@ This is a **plugin-only** change (adds SPM plumbing, does not touch native SDK v 2. All four build-matrix rows in Phase 5 verified with evidence (CI logs or local build output). 3. `CHANGELOG.md` and `pubspec.yaml` version bumped per Phase 3. 4. `doc/Installation.md` and `doc/PurchaseConnector.md` updated per Phase 4, explicitly stating the PurchaseConnector/SPM limitation. -5. No `.iOS("12.0")`-style invalid SwiftPM API left in `Package.swift` (`grep -n '\.iOS(\"' ios/appsflyer_sdk/Package.swift` should return nothing). +5. ~~No `.iOS("12.0")`-style invalid SwiftPM API left in `Package.swift`~~ — retracted: `.iOS("12.0")` is valid (verified via `swift package describe`); the shipped `Package.swift` intentionally uses this form. 6. No duplicate source files between `ios/Classes/` (should no longer exist) and `ios/appsflyer_sdk/Sources/`. 7. Ready to open a PR against DELIVERY-125462, referencing and closing out #454/#455/#370 in the description (crediting their work, explaining why #454 was chosen as base). From bd911e23356d7c6a5a9f0dfd4285f56d792d7c89 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Thu, 23 Jul 2026 12:59:01 +0300 Subject: [PATCH 3/6] Rename docs/ to internal-docs/ Same rationale as #460: docs/ was one letter off from the user-facing doc/, easy to confuse. Renames this branch's own docs/prds/, docs/researches/, docs/tech-designs/, and docs/features/ additions (F-054, F-060), fixes the tech design's frontmatter self-references (prd:/research: fields), and picks up two files #460 missed (ai-delivery-workflow-templat/README.md, update-existing-repo.md). Co-Authored-By: Claude Sonnet 5 --- .claude/WORKFLOW.md | 20 +++--- .claude/commands/af-quiz-me.md | 10 +-- .claude/commands/af-ship-from-prd.md | 2 +- .claude/commands/af-ship-from-tech-design.md | 2 +- .claude/commands/af-ship.md | 2 +- .claude/prompts/generate-feature-catalog.md | 6 +- .claude/prompts/generate-issue-cases.md | 32 ++++----- .claude/skills/af-ship-orch/SKILL.md | 18 ++--- .claude/skills/alice-pm/SKILL.md | 22 +++---- .../skills/bob-flutter-researcher/SKILL.md | 16 ++--- .claude/skills/dave-flutter-engineer/SKILL.md | 28 ++++---- .claude/skills/erin-flutter-analyst/SKILL.md | 4 +- CLAUDE.md | 6 +- ai-delivery-workflow-templat/README.md | 10 +-- .../update-existing-repo.md | 66 +++++++++---------- {docs => internal-docs}/features/DIAGRAM.md | 0 .../features/F-001-sdk-initialization.md | 0 .../features/F-002-sdk-start.md | 0 .../F-003-sdk-plugin-version-retrieval.md | 0 .../features/F-004-in-app-event-logging.md | 0 .../features/F-005-ad-revenue-logging.md | 0 .../F-006-custom-host-configuration.md | 0 .../F-007-device-id-collection-optout.md | 0 .../F-008-manual-imei-android-id-override.md | 0 .../F-009-min-time-between-sessions.md | 0 .../features/F-010-currency-code-setting.md | 0 ...11-tcf-dma-automatic-consent-collection.md | 0 .../F-012-manual-gdpr-dma-consent-api.md | 0 .../features/F-013-user-anonymization.md | 0 .../F-014-manual-deep-link-retrigger.md | 0 .../features/F-015-customer-user-id.md | 0 .../F-016-update-vs-fresh-install-flag.md | 0 .../features/F-017-sdk-kill-switch.md | 0 .../features/F-018-uninstall-measurement.md | 0 .../features/F-019-user-email-collection.md | 0 .../features/F-020-appsflyer-uid-retrieval.md | 0 ...-021-delayed-session-start-pending-cuid.md | 0 ...push-notification-deep-link-path-config.md | 0 .../F-023-in-app-purchase-validation-v1.md | 0 .../F-024-in-app-purchase-validation-v2.md | 0 ...5-ios-receipt-validation-sandbox-toggle.md | 0 .../features/F-026-additional-custom-data.md | 0 ...027-user-invite-link-generation-onelink.md | 0 ...028-app-invite-onelink-id-configuration.md | 0 ...oss-promotion-impression-click-tracking.md | 0 .../F-030-custom-branded-onelink-domains.md | 0 .../F-031-push-notification-data-handling.md | 0 .../F-032-facebook-deferred-app-links.md | 0 .../features/F-033-skadnetwork-opt-out.md | 0 ...vertising-identifier-collection-disable.md | 0 .../F-035-conversion-data-callback.md | 0 .../F-036-app-open-attribution-callback.md | 0 ...nified-deep-linking-callback-and-models.md | 0 ...rchase-validation-notification-callback.md | 0 ...F-039-native-ios-deep-link-entry-points.md | 0 ...android-new-intent-deep-link-forwarding.md | 0 .../F-041-current-device-language-override.md | 0 .../F-042-partner-postback-sharing-filter.md | 0 .../F-043-out-of-store-install-source.md | 0 .../features/F-044-partner-specific-data.md | 0 ...045-deep-link-url-resolution-allow-list.md | 0 .../features/F-046-disable-network-data.md | 0 .../F-047-appset-id-collection-optout.md | 0 .../F-048-plugin-metadata-reporting.md | 0 ...chase-connector-configuration-lifecycle.md | 0 ...se-connector-storekit-version-selection.md | 0 ...tor-android-validation-result-listeners.md | 0 ...nector-ios-combined-validation-callback.md | 0 ...chase-connector-google-play-data-models.md | 0 ...54-purchase-connector-build-time-opt-in.md | 0 ...e-connector-missing-configuration-guard.md | 0 ...56-app-invite-link-onelink-id-init-time.md | 0 .../features/F-057-asa-collection-optout.md | 0 .../F-058-att-authorization-wait-timeout.md | 0 .../features/F-059-debug-logging-toggle.md | 0 .../F-060-swift-package-manager-support.md | 0 {docs => internal-docs}/features/INDEX.md | 0 {docs => internal-docs}/features/TEMPLATE.md | 0 {docs => internal-docs}/prds/spm-support.md | 0 .../researches/R-001-spm-support.md | 0 .../tech-designs/spm-support.md | 4 +- 81 files changed, 124 insertions(+), 124 deletions(-) rename {docs => internal-docs}/features/DIAGRAM.md (100%) rename {docs => internal-docs}/features/F-001-sdk-initialization.md (100%) rename {docs => internal-docs}/features/F-002-sdk-start.md (100%) rename {docs => internal-docs}/features/F-003-sdk-plugin-version-retrieval.md (100%) rename {docs => internal-docs}/features/F-004-in-app-event-logging.md (100%) rename {docs => internal-docs}/features/F-005-ad-revenue-logging.md (100%) rename {docs => internal-docs}/features/F-006-custom-host-configuration.md (100%) rename {docs => internal-docs}/features/F-007-device-id-collection-optout.md (100%) rename {docs => internal-docs}/features/F-008-manual-imei-android-id-override.md (100%) rename {docs => internal-docs}/features/F-009-min-time-between-sessions.md (100%) rename {docs => internal-docs}/features/F-010-currency-code-setting.md (100%) rename {docs => internal-docs}/features/F-011-tcf-dma-automatic-consent-collection.md (100%) rename {docs => internal-docs}/features/F-012-manual-gdpr-dma-consent-api.md (100%) rename {docs => internal-docs}/features/F-013-user-anonymization.md (100%) rename {docs => internal-docs}/features/F-014-manual-deep-link-retrigger.md (100%) rename {docs => internal-docs}/features/F-015-customer-user-id.md (100%) rename {docs => internal-docs}/features/F-016-update-vs-fresh-install-flag.md (100%) rename {docs => internal-docs}/features/F-017-sdk-kill-switch.md (100%) rename {docs => internal-docs}/features/F-018-uninstall-measurement.md (100%) rename {docs => internal-docs}/features/F-019-user-email-collection.md (100%) rename {docs => internal-docs}/features/F-020-appsflyer-uid-retrieval.md (100%) rename {docs => internal-docs}/features/F-021-delayed-session-start-pending-cuid.md (100%) rename {docs => internal-docs}/features/F-022-push-notification-deep-link-path-config.md (100%) rename {docs => internal-docs}/features/F-023-in-app-purchase-validation-v1.md (100%) rename {docs => internal-docs}/features/F-024-in-app-purchase-validation-v2.md (100%) rename {docs => internal-docs}/features/F-025-ios-receipt-validation-sandbox-toggle.md (100%) rename {docs => internal-docs}/features/F-026-additional-custom-data.md (100%) rename {docs => internal-docs}/features/F-027-user-invite-link-generation-onelink.md (100%) rename {docs => internal-docs}/features/F-028-app-invite-onelink-id-configuration.md (100%) rename {docs => internal-docs}/features/F-029-cross-promotion-impression-click-tracking.md (100%) rename {docs => internal-docs}/features/F-030-custom-branded-onelink-domains.md (100%) rename {docs => internal-docs}/features/F-031-push-notification-data-handling.md (100%) rename {docs => internal-docs}/features/F-032-facebook-deferred-app-links.md (100%) rename {docs => internal-docs}/features/F-033-skadnetwork-opt-out.md (100%) rename {docs => internal-docs}/features/F-034-advertising-identifier-collection-disable.md (100%) rename {docs => internal-docs}/features/F-035-conversion-data-callback.md (100%) rename {docs => internal-docs}/features/F-036-app-open-attribution-callback.md (100%) rename {docs => internal-docs}/features/F-037-unified-deep-linking-callback-and-models.md (100%) rename {docs => internal-docs}/features/F-038-legacy-purchase-validation-notification-callback.md (100%) rename {docs => internal-docs}/features/F-039-native-ios-deep-link-entry-points.md (100%) rename {docs => internal-docs}/features/F-040-android-new-intent-deep-link-forwarding.md (100%) rename {docs => internal-docs}/features/F-041-current-device-language-override.md (100%) rename {docs => internal-docs}/features/F-042-partner-postback-sharing-filter.md (100%) rename {docs => internal-docs}/features/F-043-out-of-store-install-source.md (100%) rename {docs => internal-docs}/features/F-044-partner-specific-data.md (100%) rename {docs => internal-docs}/features/F-045-deep-link-url-resolution-allow-list.md (100%) rename {docs => internal-docs}/features/F-046-disable-network-data.md (100%) rename {docs => internal-docs}/features/F-047-appset-id-collection-optout.md (100%) rename {docs => internal-docs}/features/F-048-plugin-metadata-reporting.md (100%) rename {docs => internal-docs}/features/F-049-purchase-connector-configuration-lifecycle.md (100%) rename {docs => internal-docs}/features/F-050-purchase-connector-storekit-version-selection.md (100%) rename {docs => internal-docs}/features/F-051-purchase-connector-android-validation-result-listeners.md (100%) rename {docs => internal-docs}/features/F-052-purchase-connector-ios-combined-validation-callback.md (100%) rename {docs => internal-docs}/features/F-053-purchase-connector-google-play-data-models.md (100%) rename {docs => internal-docs}/features/F-054-purchase-connector-build-time-opt-in.md (100%) rename {docs => internal-docs}/features/F-055-purchase-connector-missing-configuration-guard.md (100%) rename {docs => internal-docs}/features/F-056-app-invite-link-onelink-id-init-time.md (100%) rename {docs => internal-docs}/features/F-057-asa-collection-optout.md (100%) rename {docs => internal-docs}/features/F-058-att-authorization-wait-timeout.md (100%) rename {docs => internal-docs}/features/F-059-debug-logging-toggle.md (100%) rename {docs => internal-docs}/features/F-060-swift-package-manager-support.md (100%) rename {docs => internal-docs}/features/INDEX.md (100%) rename {docs => internal-docs}/features/TEMPLATE.md (100%) rename {docs => internal-docs}/prds/spm-support.md (100%) rename {docs => internal-docs}/researches/R-001-spm-support.md (100%) rename {docs => internal-docs}/tech-designs/spm-support.md (99%) diff --git a/.claude/WORKFLOW.md b/.claude/WORKFLOW.md index 3a183fb3..1c5fa647 100644 --- a/.claude/WORKFLOW.md +++ b/.claude/WORKFLOW.md @@ -66,11 +66,11 @@ flowchart TD | Directory | Nickname | Owner | Consumers | |-----------|----------|-------|-----------| -| `docs/prds/` | PRDs (staging) | Alice (writes); af-ship-orch (saves external) | User review; may move to Notion | -| `docs/tech-designs/` | Tech designs (staging) | Dave (writes); af-ship-orch (saves external) | User review; may move to Notion | -| `docs/features/` | Feature catalog | Dave (writes F-NNN) | Alice, Bob, Erin (read) | -| `docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | -| `docs/researches/` | Research log | Bob (writes R-NNN) | Alice (via challenge loop) | +| `internal-docs/prds/` | PRDs (staging) | Alice (writes); af-ship-orch (saves external) | User review; may move to Notion | +| `internal-docs/tech-designs/` | Tech designs (staging) | Dave (writes); af-ship-orch (saves external) | User review; may move to Notion | +| `internal-docs/features/` | Feature catalog | Dave (writes F-NNN) | Alice, Bob, Erin (read) | +| `internal-docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | +| `internal-docs/researches/` | Research log | Bob (writes R-NNN) | Alice (via challenge loop) | | `docs/payloads/` | Payload map | Erin (writes P-NNN, FIELD_MAP) | Alice, Dave (via challenge loop) | --- @@ -100,7 +100,7 @@ If unsure whether a task is maintenance or a feature → use `/af-ship`. ``` /af-ship → af-ship-orch creates task wizard → calls alice-pm - → Alice writes PRD → saves to docs/prds/.md → asks user to review + → Alice writes PRD → saves to internal-docs/prds/.md → asks user to review → User approves PRD → Alice invokes Bob and/or Erin if needed → Bob/Erin produce findings → Alice challenges (max 2 iterations) @@ -111,7 +111,7 @@ If unsure whether a task is maintenance or a feature → use `/af-ship`. **From existing PRD:** ``` /af-ship-from-prd (or /af-ship --prd ) - → af-ship-orch fetches / reads PRD → saves to docs/prds/.md → calls alice-pm + → af-ship-orch fetches / reads PRD → saves to internal-docs/prds/.md → calls alice-pm → Alice challenges PRD for completeness → resolves gaps with user → Alice delegates to Bob/Erin/Dave (no second review pause) → [Phase 1 / 2 / 3 below] @@ -120,7 +120,7 @@ If unsure whether a task is maintenance or a feature → use `/af-ship`. **From existing tech design:** ``` /af-ship-from-tech-design (or /af-ship --tech-design ) - → af-ship-orch fetches / reads tech design → saves to docs/tech-designs/.md → calls alice-pm + → af-ship-orch fetches / reads tech design → saves to internal-docs/tech-designs/.md → calls alice-pm → Alice runs full challenge agenda → Dave addresses issues (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." → [Phase 2 / 3 below — Phase 1 skipped, PRD gate bypassed] @@ -128,7 +128,7 @@ If unsure whether a task is maintenance or a feature → use `/af-ship`. **Phase 1 — Tech design** ``` - → Dave writes tech design → saves to docs/tech-designs/.md + → Dave writes tech design → saves to internal-docs/tech-designs/.md → Alice challenges tech design (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." → Dave asks user to review tech design @@ -145,7 +145,7 @@ If unsure whether a task is maintenance or a feature → use `/af-ship`. **Phase 3 — Feature doc** ``` → Dave runs impact scan → updates any affected existing F-NNN docs - → Dave writes new F-NNN feature doc → saves to docs/features/ + → Dave writes new F-NNN feature doc → saves to internal-docs/features/ → Alice challenges feature doc (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." ``` diff --git a/.claude/commands/af-quiz-me.md b/.claude/commands/af-quiz-me.md index 1cfc50f9..e58f4bcb 100644 --- a/.claude/commands/af-quiz-me.md +++ b/.claude/commands/af-quiz-me.md @@ -3,22 +3,22 @@ Generate an interactive HTML quiz from a tech design document. ## Step 1 — Resolve the document **If $ARGUMENTS is empty:** -List all `.md` files in `docs/tech-designs/`. +List all `.md` files in `internal-docs/tech-designs/`. - If files exist, list them and ask: "Which tech design should I quiz you on? (Reply with the number or filename) Or reply **project** to generate a quiz covering the whole project from the feature catalog." Wait for the user's selection before continuing. - If the folder does not exist or is empty, ask: - "No tech designs found in `docs/tech-designs/`. What would you like to do? + "No tech designs found in `internal-docs/tech-designs/`. What would you like to do? 1. Provide a path or Notion URL (reply with the path/URL) 2. Generate a project quiz from the feature catalog (reply **project**)" Wait for the user's reply before continuing. **If the user replies `project` (or $ARGUMENTS is `project`):** -Check whether `docs/features/INDEX.md` exists. +Check whether `internal-docs/features/INDEX.md` exists. - If it does not exist, stop and say: - "No feature catalog found. Run `/af-generate-feature-catalog` first to build `docs/features/`, then try again." -- If it exists, read `docs/features/INDEX.md` to get the full list of features, then read each individual `docs/features/F-*.md` file. + "No feature catalog found. Run `/af-generate-feature-catalog` first to build `internal-docs/features/`, then try again." +- If it exists, read `internal-docs/features/INDEX.md` to get the full list of features, then read each individual `internal-docs/features/F-*.md` file. Set `` to `project` and `` to the project name derived from `INDEX.md` (e.g. `MyProject — Project Quiz`). Proceed to Step 3 in **project mode** (random 10 questions across all features). diff --git a/.claude/commands/af-ship-from-prd.md b/.claude/commands/af-ship-from-prd.md index 7ddf7543..7224dbd0 100644 --- a/.claude/commands/af-ship-from-prd.md +++ b/.claude/commands/af-ship-from-prd.md @@ -1,7 +1,7 @@ If $ARGUMENTS is empty, stop and ask: "Please provide a Notion URL or a path to a local .md file for the PRD. Example: `/af-ship-from-prd https://notion.so/team/my-prd` -Example: `/af-ship-from-prd docs/prds/my-feature.md`" +Example: `/af-ship-from-prd internal-docs/prds/my-feature.md`" Do not proceed until the user provides a URL or path. Start the feature delivery workflow using an existing PRD. diff --git a/.claude/commands/af-ship-from-tech-design.md b/.claude/commands/af-ship-from-tech-design.md index dba0b2e2..4516843b 100644 --- a/.claude/commands/af-ship-from-tech-design.md +++ b/.claude/commands/af-ship-from-tech-design.md @@ -1,7 +1,7 @@ If $ARGUMENTS is empty, stop and ask: "Please provide a Notion URL or a path to a local .md file for the tech design. Example: `/af-ship-from-tech-design https://notion.so/team/my-design` -Example: `/af-ship-from-tech-design docs/tech-designs/my-feature.md`" +Example: `/af-ship-from-tech-design internal-docs/tech-designs/my-feature.md`" Do not proceed until the user provides a URL or path. Start the delivery workflow using an existing tech design. diff --git a/.claude/commands/af-ship.md b/.claude/commands/af-ship.md index d9caf95b..d75be3be 100644 --- a/.claude/commands/af-ship.md +++ b/.claude/commands/af-ship.md @@ -13,7 +13,7 @@ The PRD source is the value extracted from $ARGUMENTS after `--prd `. Extract the URL or path that follows `--tech-design `. If nothing follows `--tech-design`, stop and ask: "Please provide a Notion URL or local .md path after --tech-design -(e.g. `/af-ship --tech-design docs/tech-designs/my-feature.md`)." +(e.g. `/af-ship --tech-design internal-docs/tech-designs/my-feature.md`)." Do not proceed until a URL or path is provided. Otherwise: invoke the `af-ship-orch` skill in Tech-Design-Given mode. The tech design source is the value extracted from $ARGUMENTS after `--tech-design `. diff --git a/.claude/prompts/generate-feature-catalog.md b/.claude/prompts/generate-feature-catalog.md index d97884ad..8b5fc51f 100644 --- a/.claude/prompts/generate-feature-catalog.md +++ b/.claude/prompts/generate-feature-catalog.md @@ -1,6 +1,6 @@ # Prompt: Create Feature Catalog -Use this prompt to generate a `docs/features/` catalog for this project. +Use this prompt to generate a `internal-docs/features/` catalog for this project. Values below are filled during workflow setup — edit them here if needed. --- @@ -20,7 +20,7 @@ JIRA_PROJECT_KEY: DELIVERY ## Prompt ```` -Create a feature catalog for this project under `docs/features/`. +Create a feature catalog for this project under `internal-docs/features/`. Project context: Flutter plugin providing mobile attribution and analytics for iOS and Android, bridging native AppsFlyer SDKs via Dart MethodChannel/EventChannel Primary language(s): Dart, Objective-C, Java, Kotlin @@ -143,7 +143,7 @@ Mark "Propose taxonomy" as completed. Mark "User reviews taxonomy" as `in_progre --- -## Phase 3 — Create `docs/features/` +## Phase 3 — Create `internal-docs/features/` Mark "User reviews taxonomy" as completed. Mark "Write feature catalog" as `in_progress`. diff --git a/.claude/prompts/generate-issue-cases.md b/.claude/prompts/generate-issue-cases.md index 5335385a..2dadc807 100644 --- a/.claude/prompts/generate-issue-cases.md +++ b/.claude/prompts/generate-issue-cases.md @@ -16,12 +16,12 @@ LANGUAGES: Dart, Objective-C, Java, Kotlin ## TASK -Mine this repository's full git history across all branches and generate `docs/issue-cases/` — an engineering issue case bank with a hot zones map and two-axis classification (Component × Bug Class). +Mine this repository's full git history across all branches and generate `internal-docs/issue-cases/` — an engineering issue case bank with a hot zones map and two-axis classification (Component × Bug Class). Project context: Flutter plugin providing mobile attribution and analytics for iOS and Android, bridging native AppsFlyer SDKs via Dart MethodChannel/EventChannel Primary language(s): Dart, Objective-C, Java, Kotlin -Create `docs/issue-cases/INDEX.md`, `docs/issue-cases/TEMPLATE.md`, `docs/issue-cases/GUARDRAILS.md`, and individual `IC-NNN-*.md` files. +Create `internal-docs/issue-cases/INDEX.md`, `internal-docs/issue-cases/TEMPLATE.md`, `internal-docs/issue-cases/GUARDRAILS.md`, and individual `IC-NNN-*.md` files. --- @@ -63,7 +63,7 @@ echo "Mining years: $START_YEAR to $CURRENT_YEAR" Create the staging directory: ```bash -mkdir -p docs/issue-cases/partial +mkdir -p internal-docs/issue-cases/partial ``` For each year from `$START_YEAR` to `$CURRENT_YEAR`, spawn one Agent in parallel. Pass the prompt below verbatim, substituting: @@ -109,7 +109,7 @@ For each confirmed bug fix, collect: ## Output format -Write one file per bug fix to docs/issue-cases/partial/ using this name pattern: +Write one file per bug fix to internal-docs/issue-cases/partial/ using this name pattern: {{YEAR}}-NNN-kebab-short-name.md where NNN is a zero-padded counter starting at 001, scoped to this year only. @@ -144,10 +144,10 @@ year: {{YEAR}} ### Takeaway [The rule that prevents this class of bug. Be specific to this codebase.] -Also write a one-line summary file docs/issue-cases/partial/{{YEAR}}-index.md listing each case you wrote: +Also write a one-line summary file internal-docs/issue-cases/partial/{{YEAR}}-index.md listing each case you wrote: {{YEAR}}-NNN-kebab-name.md — [one-line summary] -If you find zero genuine bug fixes for {{YEAR}}, write docs/issue-cases/partial/{{YEAR}}-index.md with a single line: +If you find zero genuine bug fixes for {{YEAR}}, write internal-docs/issue-cases/partial/{{YEAR}}-index.md with a single line: no cases found ``` @@ -209,7 +209,7 @@ For each unique DELIVERY-XXXXX number found: - Record the ticket's `created` date from the Jira response (use as the **Date** field in the IC case) - Find the corresponding fix commit in git (`git log --all --oneline --grep="DELIVERY-XXXXX"`) - Inspect the diff (`git show <hash>`) - - Write the IC case to `docs/issue-cases/partial/JIRA-NNN-kebab-short-name.md` (where NNN is a zero-padded counter starting at 001, scoped to this step). Use the same file format as the year-agent cases (frontmatter with `commit:` and `year:` fields, then the IC sections). Step 1b.5 will collect and align all partial files together. + - Write the IC case to `internal-docs/issue-cases/partial/JIRA-NNN-kebab-short-name.md` (where NNN is a zero-padded counter starting at 001, scoped to this step). Use the same file format as the year-agent cases (frontmatter with `commit:` and `year:` fields, then the IC sections). Step 1b.5 will collect and align all partial files together. Skip tickets where the diff shows only infrastructure changes (Dockerfile, CI config, `.edn` config files with no behavior change). @@ -219,7 +219,7 @@ Skip tickets where the diff shows only infrastructure changes (Dockerfile, CI co Mark "Cross-check with Jira" as completed. Mark "Align IC cases" as `in_progress`. -**Collect** all files matching `docs/issue-cases/partial/????-???-*.md`. +**Collect** all files matching `internal-docs/issue-cases/partial/????-???-*.md`. **Deduplicate** by commit hash: read the `commit:` frontmatter field from each file. If two files share the same hash, keep the one with more lines (richer description) and discard the other. @@ -228,7 +228,7 @@ Mark "Cross-check with Jira" as completed. Mark "Align IC cases" as `in_progress **Renumber** sequentially starting at 1. Assign each file a new ID: `IC-001`, `IC-002`, ..., `IC-NNN`. **Rename** each file from its temp name to its final name: -- `docs/issue-cases/partial/2019-003-null-dereference.md` → `docs/issue-cases/IC-007-null-dereference.md` +- `internal-docs/issue-cases/partial/2019-003-null-dereference.md` → `internal-docs/issue-cases/IC-007-null-dereference.md` - Pattern: strip the `YYYY-NNN-` prefix, prepend `IC-NNN-` (using the new sequential number, zero-padded to 3 digits) **Update** the heading inside each renamed file from `## YYYY-NNN — Name` to `## IC-NNN — Name`. @@ -240,9 +240,9 @@ Report a summary table of all actions taken: | 2019-001-foo | IC-001-foo | abc1234 | 2019-03-12 | renamed | | 2020-002-bar | — | def5678 | 2020-07-01 | duplicate, discarded | -**Clean up** the staging directory after confirming all files have been moved to `docs/issue-cases/`: +**Clean up** the staging directory after confirming all files have been moved to `internal-docs/issue-cases/`: ```bash -rm -rf docs/issue-cases/partial/ +rm -rf internal-docs/issue-cases/partial/ ``` Mark "Align IC cases" as completed. @@ -326,7 +326,7 @@ Prepend a **Tech Design Checklist** section: - [ ] All initialization paths covered - [ ] Any rewrite of a previously-reverted feature must audit the original contract -Write to `docs/issue-cases/GUARDRAILS.md`. +Write to `internal-docs/issue-cases/GUARDRAILS.md`. --- @@ -399,7 +399,7 @@ Run immediately after INDEX.md is written. Three checks: **Check 1 — IC → Feature cross-reference** -If `docs/features/INDEX.md` exists: for each IC case, find the matching F-NNN feature by component name. Add a `feature_ref: [F-NNN]` line to the frontmatter of that IC file. If no match is found, leave the field blank and flag it. +If `internal-docs/features/INDEX.md` exists: for each IC case, find the matching F-NNN feature by component name. Add a `feature_ref: [F-NNN]` line to the frontmatter of that IC file. If no match is found, leave the field blank and flag it. **Check 2 — Orphaned IC cases** @@ -428,8 +428,8 @@ Add a "Before Making Code Changes" section using **active language**: ``` ## Before Making Code Changes -Before writing any code that touches a component listed in `docs/issue-cases/INDEX.md`: -1. Open `docs/issue-cases/INDEX.md` and find the component in the Hot Zones Map +Before writing any code that touches a component listed in `internal-docs/issue-cases/INDEX.md`: +1. Open `internal-docs/issue-cases/INDEX.md` and find the component in the Hot Zones Map 2. Read each linked IC case — pay attention to the **Takeaway** rule 3. Explicitly state which past issues are relevant and how the new code avoids repeating them @@ -467,7 +467,7 @@ MSG="" # Add one block per hot-zone component (replace HotZoneFile and ComponentName # with the actual filenames and component names from the Step 2 Hot Zones Map): if echo "$FILE_PATH" | grep -qE "HotZoneFile\.(clj|java)"; then - MSG="HOT ZONE — ComponentName: read docs/issue-cases/INDEX.md for relevant cases and apply their Takeaway rules before writing code." + MSG="HOT ZONE — ComponentName: read internal-docs/issue-cases/INDEX.md for relevant cases and apply their Takeaway rules before writing code." fi if [ -n "$MSG" ]; then diff --git a/.claude/skills/af-ship-orch/SKILL.md b/.claude/skills/af-ship-orch/SKILL.md index a3aad4fe..89c8c6ac 100644 --- a/.claude/skills/af-ship-orch/SKILL.md +++ b/.claude/skills/af-ship-orch/SKILL.md @@ -59,7 +59,7 @@ Immediately mark "Fetch and validate PRD" as `in_progress`. - Starts with `http` → Notion URL - Ends with `.md` or contains `/` → local file path -- Otherwise → stop and ask: "Please provide a Notion URL or a path to a local `.md` file (e.g. `https://notion.so/team/my-prd` or `docs/prds/my-feature.md`)." +- Otherwise → stop and ask: "Please provide a Notion URL or a path to a local `.md` file (e.g. `https://notion.so/team/my-prd` or `internal-docs/prds/my-feature.md`)." **Step 2 — Fetch or read** @@ -68,9 +68,9 @@ Immediately mark "Fetch and validate PRD" as `in_progress`. **Step 3 — Save a local copy** -Save to `docs/prds/<slug>.md`. +Save to `internal-docs/prds/<slug>.md`. - Derive `<slug>` from the document title (kebab-case, e.g. `dark-mode-settings`). -- If the file is already at `docs/prds/`, use it in place. +- If the file is already at `internal-docs/prds/`, use it in place. - If no title is detectable, ask: "What slug should I use for this PRD? (e.g. `dark-mode-settings`)" Mark "Fetch and validate PRD" as `completed`, "Challenge PRD" as `in_progress`. @@ -103,7 +103,7 @@ Immediately mark "Fetch and validate tech design" as `in_progress`. - Starts with `http` → Notion URL - Ends with `.md` or contains `/` → local file path -- Otherwise → stop and ask: "Please provide a Notion URL or a path to a local `.md` file (e.g. `https://notion.so/team/my-design` or `docs/tech-designs/my-feature.md`)." +- Otherwise → stop and ask: "Please provide a Notion URL or a path to a local `.md` file (e.g. `https://notion.so/team/my-design` or `internal-docs/tech-designs/my-feature.md`)." **Step 2 — Fetch or read** @@ -112,9 +112,9 @@ Immediately mark "Fetch and validate tech design" as `in_progress`. **Step 3 — Save a local copy** -Save to `docs/tech-designs/<slug>.md`. +Save to `internal-docs/tech-designs/<slug>.md`. - Derive `<slug>` from the document title (kebab-case). -- If the file is already at `docs/tech-designs/`, use it in place. +- If the file is already at `internal-docs/tech-designs/`, use it in place. - If no title is detectable, ask: "What slug should I use for this tech design? (e.g. `dark-mode-settings`)" Mark "Fetch and validate tech design" as `completed`, "Challenge tech design" as `in_progress`. @@ -143,13 +143,13 @@ BLOCKING REQUIREMENT: Call the `Skill` tool with `af-ship-orch` BEFORE any other ``` /af-ship <description> → af-ship-orch creates tasks → calls alice-pm - → Alice writes PRD → saves to docs/prds/<slug>.md → asks user to review + → Alice writes PRD → saves to internal-docs/prds/<slug>.md → asks user to review → User approves PRD → Alice invokes Bob and/or Erin if needed → Bob/Erin produce findings → Alice challenges (max 2 iterations) → Alice updates PRD if scope changed → Alice invokes Dave - → Dave writes tech design → saves to docs/tech-designs/<slug>.md + → Dave writes tech design → saves to internal-docs/tech-designs/<slug>.md → Alice challenges tech design (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on tech design) → Dave asks user to review tech design @@ -157,7 +157,7 @@ BLOCKING REQUIREMENT: Call the `Skill` tool with `af-ship-orch` BEFORE any other → Dave implements + writes unit tests → Alice challenges implementation (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on implementation) - → Dave writes F-NNN feature doc → saves to docs/features/ + → Dave writes F-NNN feature doc → saves to internal-docs/features/ → Alice challenges feature doc (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on feature doc) → If unresolved after 2 iterations → Alice escalates to user diff --git a/.claude/skills/alice-pm/SKILL.md b/.claude/skills/alice-pm/SKILL.md index ac002b3e..331ef037 100644 --- a/.claude/skills/alice-pm/SKILL.md +++ b/.claude/skills/alice-pm/SKILL.md @@ -26,12 +26,12 @@ When starting a new feature delivery, write the PRD with these sections: | **Risks** | Release risk, compliance risk, accuracy risk. | | **Open questions** | What is unknown before Dave can start? | -Save the PRD to `docs/prds/<feature-slug>.md`, then write exactly: +Save the PRD to `internal-docs/prds/<feature-slug>.md`, then write exactly: --- ## ⏸ Waiting for your review -PRD saved to `docs/prds/<feature-slug>.md`. +PRD saved to `internal-docs/prds/<feature-slug>.md`. The workflow is paused. Reply **approved** to continue, or share your feedback and I'll update the PRD. --- @@ -80,7 +80,7 @@ Flag: missing sections, vague requirements ("improve performance"), unmeasurable If gaps found: 1. List every gap clearly. 2. Pause and ask the user to resolve them. -3. Update `docs/prds/<slug>.md` with resolved content. +3. Update `internal-docs/prds/<slug>.md` with resolved content. 4. Repeat until satisfied. Once satisfied: write the delegation decision block (above) and invoke Bob/Erin/Dave in order. Do NOT output a `⏸ Waiting for your review` pause — the PRD was externally authored and team-reviewed. @@ -189,7 +189,7 @@ Use `TaskList` to find tasks by subject, then `TaskUpdate` to advance them. Skip ### 1. GUARDRAILS Coverage - Did Dave's context table appear before the code? -- For every file touched: was the component checked against `docs/issue-cases/INDEX.md`? +- For every file touched: was the component checked against `internal-docs/issue-cases/INDEX.md`? - Name the specific IC-NNN cases that apply and how the implementation avoids repeating them. ### 2. Migration & Rollout Risk @@ -201,13 +201,13 @@ Use `TaskList` to find tasks by subject, then `TaskUpdate` to advance them. Skip ### 3. Feature Documentation **During tech design review:** -- Is the tech design saved to `docs/tech-designs/<slug>.md`? +- Is the tech design saved to `internal-docs/tech-designs/<slug>.md`? - Does the tech design cover all PRD requirements and acceptance criteria? - Is the planned F-NNN ID noted in the design? **During feature doc review (Phase 3 only — do not check during tech design or implementation review):** -- Is the F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md`? -- Does it follow `docs/features/TEMPLATE.md`? +- Is the F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md`? +- Does it follow `internal-docs/features/TEMPLATE.md`? - Are Business Purpose, Call Chain, Files, and Tests sections complete? ### 4. Concurrency & Thread Safety @@ -250,9 +250,9 @@ Use `TaskList` to find tasks by subject, then `TaskUpdate` to advance them. Skip - [ ] No open challenge items without a response ### Alice is satisfied with Dave's feature doc when: -- [ ] Impact scan table was printed — every changed file checked against `docs/features/INDEX.md` +- [ ] Impact scan table was printed — every changed file checked against `internal-docs/features/INDEX.md` - [ ] All affected existing F-NNN docs updated, or "none affected" explicitly stated -- [ ] F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md` +- [ ] F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md` - [ ] All template sections complete (Business Purpose, Call Chain, Files, Tests) - [ ] No open challenge items without a response @@ -315,8 +315,8 @@ Automated via GitHub Actions: `.github/workflows/rc-release.yml`, `rc-smoke.yml` ## Docs Locations -- PRDs → `docs/prds/<slug>.md` (temporary — user may push to Notion for review) -- Feature catalog docs → `docs/features/` (permanent) +- PRDs → `internal-docs/prds/<slug>.md` (temporary — user may push to Notion for review) +- Feature catalog docs → `internal-docs/features/` (permanent) --- diff --git a/.claude/skills/bob-flutter-researcher/SKILL.md b/.claude/skills/bob-flutter-researcher/SKILL.md index a4898503..117f77c5 100644 --- a/.claude/skills/bob-flutter-researcher/SKILL.md +++ b/.claude/skills/bob-flutter-researcher/SKILL.md @@ -17,21 +17,21 @@ Domain researcher for AppsFlyer Flutter Plugin. Knows how platform APIs and exte 1. Check if research already exists: ``` - ls docs/researches/ + ls internal-docs/researches/ ``` 2. Find related features: ``` - grep -i "<topic>" docs/features/INDEX.md + grep -i "<topic>" internal-docs/features/INDEX.md ``` 3. Find related issue cases: ``` - grep -i "<topic>" docs/issue-cases/INDEX.md + grep -i "<topic>" internal-docs/issue-cases/INDEX.md ``` 4. State what existing docs cover and what gap this research fills. ### Required output -Every research task produces `docs/researches/R-NNN-slug.md`. After writing: +Every research task produces `internal-docs/researches/R-NNN-slug.md`. After writing: - Flag which feature docs (F-NNN) should be updated based on findings — for Dave to action ### After completing research @@ -110,10 +110,10 @@ After Bob presents any research findings, `alice-pm` is invoked automatically. B ## Reference -- `docs/researches/TEMPLATE.md` — blank template -- `docs/features/INDEX.md` — feature catalog to cross-reference -- `docs/issue-cases/INDEX.md` — historical bugs to cross-reference -- `docs/issue-cases/GUARDRAILS.md` — engineering guardrails Bob's research should inform +- `internal-docs/researches/TEMPLATE.md` — blank template +- `internal-docs/features/INDEX.md` — feature catalog to cross-reference +- `internal-docs/issue-cases/INDEX.md` — historical bugs to cross-reference +- `internal-docs/issue-cases/GUARDRAILS.md` — engineering guardrails Bob's research should inform --- diff --git a/.claude/skills/dave-flutter-engineer/SKILL.md b/.claude/skills/dave-flutter-engineer/SKILL.md index e9e4af68..551e8190 100644 --- a/.claude/skills/dave-flutter-engineer/SKILL.md +++ b/.claude/skills/dave-flutter-engineer/SKILL.md @@ -23,16 +23,16 @@ If neither exists, stop and call `Skill('alice-pm')` to produce one. ### Before writing any code or tech design -0. Load `docs/issue-cases/GUARDRAILS.md`. For tech designs, work through the Tech Design Checklist at the top. +0. Load `internal-docs/issue-cases/GUARDRAILS.md`. For tech designs, work through the Tech Design Checklist at the top. 1. Check if the target component is a hot zone: ``` - grep "ComponentName" docs/issue-cases/INDEX.md + grep "ComponentName" internal-docs/issue-cases/INDEX.md ``` -2. Load only the matching `docs/issue-cases/IC-NNN.md` files. +2. Load only the matching `internal-docs/issue-cases/IC-NNN.md` files. 3. State which cases apply and how the new code avoids repeating them. 4. Find and load relevant feature docs: ``` - grep "ComponentName" docs/features/INDEX.md + grep "ComponentName" internal-docs/features/INDEX.md ``` ### Before writing — required output @@ -52,9 +52,9 @@ If no issue cases apply, write "none — component not in hot zones." Never skip ### Phase 1 — Tech design -Write the tech design to `docs/tech-designs/<feature-slug>.md` where `<feature-slug>` is the same kebab-case slug used for the PRD (e.g. `device-farm-3d-header`). +Write the tech design to `internal-docs/tech-designs/<feature-slug>.md` where `<feature-slug>` is the same kebab-case slug used for the PRD (e.g. `device-farm-3d-header`). -Do NOT write tech designs in `docs/features/` — that directory is for finished feature catalog docs only. +Do NOT write tech designs in `internal-docs/features/` — that directory is for finished feature catalog docs only. Note the planned F-NNN ID in the design as "F-NNN — doc to be written after development is complete." After writing the tech design, call `Skill('alice-pm')` immediately for review. @@ -64,7 +64,7 @@ When Alice writes "Satisfied — Dave, this is ready." on the tech design, write --- ## ⏸ Waiting for your review -Tech design saved to `docs/tech-designs/<feature-slug>.md`. Alice has signed off. +Tech design saved to `internal-docs/tech-designs/<feature-slug>.md`. Alice has signed off. The workflow is paused. Reply **approved** to start implementation, or share your feedback. --- @@ -89,7 +89,7 @@ After Alice writes "Satisfied — Dave, this is ready." on the implementation: For every file changed during implementation, run: ``` -grep "<changed-file>" docs/features/INDEX.md +grep "<changed-file>" internal-docs/features/INDEX.md ``` Run once per changed file. Then print this table: @@ -101,7 +101,7 @@ For every affected F-NNN doc found: open it and update every section whose behav **Step 2 — Write the new feature doc** -Write the full F-NNN feature catalog doc to `docs/features/<F-NNN-slug>.md` and add it to `docs/features/INDEX.md`. +Write the full F-NNN feature catalog doc to `internal-docs/features/<F-NNN-slug>.md` and add it to `internal-docs/features/INDEX.md`. **Step 3 — Call Alice** @@ -154,11 +154,11 @@ BLOCKING REQUIREMENT: Include a `Skill('dave-flutter-engineer')` tool call in th ## Reference -- `docs/issue-cases/GUARDRAILS.md` — rules from real bugs; Tech Design Checklist -- `docs/issue-cases/INDEX.md` — hot zones, bug classes, component→case mapping -- `docs/issue-cases/IC-NNN.md` — individual cases (load only what you need) -- `docs/features/INDEX.md` — feature catalog index -- `docs/features/TEMPLATE.md` — required template for all feature docs +- `internal-docs/issue-cases/GUARDRAILS.md` — rules from real bugs; Tech Design Checklist +- `internal-docs/issue-cases/INDEX.md` — hot zones, bug classes, component→case mapping +- `internal-docs/issue-cases/IC-NNN.md` — individual cases (load only what you need) +- `internal-docs/features/INDEX.md` — feature catalog index +- `internal-docs/features/TEMPLATE.md` — required template for all feature docs --- diff --git a/.claude/skills/erin-flutter-analyst/SKILL.md b/.claude/skills/erin-flutter-analyst/SKILL.md index 35b4f5ba..913dff8d 100644 --- a/.claude/skills/erin-flutter-analyst/SKILL.md +++ b/.claude/skills/erin-flutter-analyst/SKILL.md @@ -86,8 +86,8 @@ After Erin presents any analysis findings, `alice-pm` is invoked automatically. - `docs/payloads/template.json` — canonical reference payload (sanitized) - `docs/payloads/FIELD_MAP.md` — complete field-to-feature-to-issue-case mapping - `docs/payloads/INDEX.md` — index of all payload analyses -- `docs/features/INDEX.md` — feature catalog -- `docs/issue-cases/INDEX.md` — bug history +- `internal-docs/features/INDEX.md` — feature catalog +- `internal-docs/issue-cases/INDEX.md` — bug history --- diff --git a/CLAUDE.md b/CLAUDE.md index 2586db2a..b2002a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,10 +90,10 @@ The following do not require a PRD or Alice review — invoke Dave directly: Every `/af-ship` deliverable must include: -- Alice PRD (`docs/prds/`) +- Alice PRD (`internal-docs/prds/`) - Bob findings (if invoked) - Erin payload impact (if invoked) -- Dave tech design (`docs/tech-designs/`) +- Dave tech design (`internal-docs/tech-designs/`) - Dave implementation + unit tests -- Dave feature doc (`docs/features/`) +- Dave feature doc (`internal-docs/features/`) - Alice sign-off at each phase diff --git a/ai-delivery-workflow-templat/README.md b/ai-delivery-workflow-templat/README.md index 02af1e61..1c11d6c5 100644 --- a/ai-delivery-workflow-templat/README.md +++ b/ai-delivery-workflow-templat/README.md @@ -53,9 +53,9 @@ Determine: - The tech stack (languages, frameworks, build tools) - How to run the test suite - How releases are cut and published -- Where feature docs live (or suggest docs/features/) -- Where research docs live (or suggest docs/researches/) -- Where issue cases live (or suggest docs/issue-cases/) +- Where feature docs live (or suggest internal-docs/features/) +- Where research docs live (or suggest internal-docs/researches/) +- Where issue cases live (or suggest internal-docs/issue-cases/) - What counts as a maintenance task (no Alice review needed) Present your findings and wait for my confirmation before continuing. @@ -161,7 +161,7 @@ Wait for my instructions before making any further changes. Open `.claude/prompts/generate-feature-catalog.md`, copy its full contents, and paste into Claude Code. -This builds `docs/features/` — a catalog of every feature in the codebase with: +This builds `internal-docs/features/` — a catalog of every feature in the codebase with: - **Business Purpose** — what the product loses if the feature is removed (enriched from Notion if connected) - **Call Chain** — from public entry point to leaf implementation - **Dependency Diagrams** — runtime flow, initialization flow, and a full dependency table @@ -175,7 +175,7 @@ The prompt walks you through each phase with a live progress view and pauses for Open `.claude/prompts/generate-issue-cases.md`, copy its full contents, and paste into Claude Code. -This builds `docs/issue-cases/` — an engineering scar book mined from the full git history: +This builds `internal-docs/issue-cases/` — an engineering scar book mined from the full git history: - **IC-NNN-*.md** — one file per real bug: what happened, root cause, fix, and takeaway - **GUARDRAILS.md** — engineering rules derived from past incidents, with a tech design checklist - **Hot Zones Map** — which components carry the most historical risk diff --git a/ai-delivery-workflow-templat/update-existing-repo.md b/ai-delivery-workflow-templat/update-existing-repo.md index 29aa6cd6..5b5872a4 100644 --- a/ai-delivery-workflow-templat/update-existing-repo.md +++ b/ai-delivery-workflow-templat/update-existing-repo.md @@ -52,7 +52,7 @@ Find this entire block: Replace with: **Step 2 — Save PRD and ask user to review** - Save the PRD to `docs/prds/<feature-slug>.md` where `<feature-slug>` is a short kebab-case name (e.g. `device-farm-3d-header`). + Save the PRD to `internal-docs/prds/<feature-slug>.md` where `<feature-slug>` is a short kebab-case name (e.g. `device-farm-3d-header`). Then write: Then write exactly: @@ -60,7 +60,7 @@ Replace with: --- ## ⏸ Waiting for your review - PRD saved to `docs/prds/<feature-slug>.md`. + PRD saved to `internal-docs/prds/<feature-slug>.md`. The workflow is paused. Reply **approved** to continue, or share your feedback and I'll update the PRD. --- @@ -103,7 +103,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace it ``` User presents feature idea - → Alice writes PRD → saves to docs/prds/<slug>.md → asks user to review + → Alice writes PRD → saves to internal-docs/prds/<slug>.md → asks user to review → User approves PRD → Alice invokes Bob and/or Erin if needed → Bob/Erin produce findings @@ -111,7 +111,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace it → Bob/Erin address every open item → Alice updates PRD if scope changed → Alice invokes Dave - → Dave writes tech design → saves to docs/tech-designs/<slug>.md + → Dave writes tech design → saves to internal-docs/tech-designs/<slug>.md → Alice challenges tech design (Challenger Mode, max 2 iterations) → Dave addresses every open item → Alice: "Satisfied — Dave, this is ready." (on tech design) @@ -121,7 +121,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace it → Alice challenges implementation (Challenger Mode, max 2 iterations) → Dave addresses every open item → Alice: "Satisfied — Dave, this is ready." (on implementation) - → Dave writes F-NNN feature doc → saves to docs/features/ + → Dave writes F-NNN feature doc → saves to internal-docs/features/ → Alice challenges feature doc (Challenger Mode, max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on feature doc) → If unresolved after 2 iterations → Alice escalates to user @@ -139,13 +139,13 @@ Replace with: ### 3. Feature Documentation **During tech design review:** - - Is the tech design saved to `docs/tech-designs/<slug>.md`? + - Is the tech design saved to `internal-docs/tech-designs/<slug>.md`? - Does the tech design cover all PRD requirements and acceptance criteria? - Is the planned F-NNN ID noted in the design? **During feature doc review (Phase 3 only — do not check during tech design or implementation review):** - - Is the F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md`? - - Does it follow `docs/features/TEMPLATE.md`? + - Is the F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md`? + - Does it follow `internal-docs/features/TEMPLATE.md`? - Are Business Purpose, Call Chain, Files, and Tests sections complete? --- @@ -178,7 +178,7 @@ Replace with: - [ ] No open challenge items without a response ### Alice is satisfied with Dave's feature doc when: - - [ ] F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md` + - [ ] F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md` - [ ] All template sections complete (Business Purpose, Call Chain, Files, Tests) - [ ] No open challenge items without a response @@ -193,7 +193,7 @@ Find: Replace with: ## Docs Locations - - PRDs → `docs/prds/<slug>.md` (temporary — user may push to Notion for review) + - PRDs → `internal-docs/prds/<slug>.md` (temporary — user may push to Notion for review) - Feature catalog docs → `<keep the same path that was here>` (permanent) --- @@ -208,26 +208,26 @@ Find this entire block: > "Where should I write this tech design? > 1. **Notion** — tech design board - > 2. **Local file** — `docs/tech-designs/<ticket-or-feature-slug>.md`" + > 2. **Local file** — `internal-docs/tech-designs/<ticket-or-feature-slug>.md`" - Do NOT write tech designs in `docs/features/` — that directory is for finished feature catalog docs only. + Do NOT write tech designs in `internal-docs/features/` — that directory is for finished feature catalog docs only. Note the planned F-NNN ID in the design as "F-NNN — doc to be written after development is complete." ### After completing a code change 5. Find which feature docs reference each changed file: ``` - grep "ChangedFile" docs/features/INDEX.md + grep "ChangedFile" internal-docs/features/INDEX.md ``` 6. Update any section whose behavior, public API, configuration, or data flow changed. - 7. If the change introduces a new feature: write the full F-NNN doc and add it to `docs/features/INDEX.md` **only after Alice has written "Satisfied — Dave, this is ready."** Not before. + 7. If the change introduces a new feature: write the full F-NNN doc and add it to `internal-docs/features/INDEX.md` **only after Alice has written "Satisfied — Dave, this is ready."** Not before. Replace with: ### Phase 1 — Tech design - Write the tech design to `docs/tech-designs/<feature-slug>.md` where `<feature-slug>` is the same kebab-case slug used for the PRD (e.g. `device-farm-3d-header`). + Write the tech design to `internal-docs/tech-designs/<feature-slug>.md` where `<feature-slug>` is the same kebab-case slug used for the PRD (e.g. `device-farm-3d-header`). - Do NOT write tech designs in `docs/features/` — that directory is for finished feature catalog docs only. + Do NOT write tech designs in `internal-docs/features/` — that directory is for finished feature catalog docs only. Note the planned F-NNN ID in the design as "F-NNN — doc to be written after development is complete." After writing the tech design, call `Skill('<alice-skill-name>')` immediately for review. @@ -238,7 +238,7 @@ Replace with: --- ## ⏸ Waiting for your review - Tech design saved to `docs/tech-designs/<feature-slug>.md`. Alice has signed off. + Tech design saved to `internal-docs/tech-designs/<feature-slug>.md`. Alice has signed off. The workflow is paused. Reply **approved** to start implementation, or share your feedback. --- @@ -263,7 +263,7 @@ Replace with: For every file changed during implementation, run: ``` - grep "<changed-file>" docs/features/INDEX.md + grep "<changed-file>" internal-docs/features/INDEX.md ``` Run once per changed file. Then print this table: @@ -275,7 +275,7 @@ Replace with: **Step 2 — Write the new feature doc** - Write the full F-NNN feature catalog doc to `docs/features/<F-NNN-slug>.md` and add it to `docs/features/INDEX.md`. + Write the full F-NNN feature catalog doc to `internal-docs/features/<F-NNN-slug>.md` and add it to `internal-docs/features/INDEX.md`. **Step 3 — Call Alice** @@ -350,15 +350,15 @@ Replace with: ### Change 2 — Docs Layer table (full replacement) Find: | `<feature doc path>` | Feature catalog | Dave (writes F-NNN) | Alice, Bob, Erin (read) | - | `docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | + | `internal-docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | | `<research path>` | Research log | Bob (writes R-NNN) | Alice (via challenge loop) | | `docs/payloads/` | Payload map | Erin (writes P-NNN, FIELD_MAP) | Alice, Dave (via challenge loop) | Replace with: - | `docs/prds/` | PRDs (staging) | Alice (writes) | User review; may move to Notion | - | `docs/tech-designs/` | Tech designs (staging) | Dave (writes) | User review; may move to Notion | + | `internal-docs/prds/` | PRDs (staging) | Alice (writes) | User review; may move to Notion | + | `internal-docs/tech-designs/` | Tech designs (staging) | Dave (writes) | User review; may move to Notion | | `<keep the same feature doc path>` | Feature catalog | Dave (writes F-NNN) | Alice, Bob, Erin (read) | - | `docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | + | `internal-docs/issue-cases/` | Scar book | Human / eng team | Alice, Dave, Bob, Erin (read) | | `<keep the same research path>` | Research log | Bob (writes R-NNN) | Alice (via challenge loop) | | `docs/payloads/` | Payload map | Erin (writes P-NNN, FIELD_MAP) | Alice, Dave (via challenge loop) | @@ -387,7 +387,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace wit ``` User runs /af-ship <description> - → Alice writes PRD → saves to docs/prds/<slug>.md → asks user to review + → Alice writes PRD → saves to internal-docs/prds/<slug>.md → asks user to review → User approves PRD → Alice invokes Bob and/or Erin if needed → Bob/Erin produce findings @@ -396,7 +396,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace wit → Alice invokes Dave Phase 1 — Tech design - → Dave writes tech design → saves to docs/tech-designs/<slug>.md + → Dave writes tech design → saves to internal-docs/tech-designs/<slug>.md → Alice challenges tech design (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on tech design) → Dave asks user to review tech design @@ -408,7 +408,7 @@ Find the entire code block inside Loop Mechanics (the ``` block) and replace wit → Alice: "Satisfied — Dave, this is ready." (on implementation) Phase 3 — Feature doc - → Dave writes F-NNN feature doc → saves to docs/features/ + → Dave writes F-NNN feature doc → saves to internal-docs/features/ → Alice challenges feature doc (max 2 iterations) → Alice: "Satisfied — Dave, this is ready." (on feature doc) ``` @@ -424,7 +424,7 @@ Create this file at `.claude/commands/af-ship.md` with the following content exa $ARGUMENTS Invoke the `alice-feature-orchestrator` skill now to begin. Alice will write a PRD, - save it to docs/prds/, ask for your review, then coordinate research and engineering + save it to internal-docs/prds/, ask for your review, then coordinate research and engineering through tech design, implementation, and feature documentation. --- @@ -470,12 +470,12 @@ Replace the full contents of CLAUDE.md with: Every `/af-ship` deliverable must include: - - Alice PRD (`docs/prds/`) + - Alice PRD (`internal-docs/prds/`) - Bob findings (if invoked) - Erin payload impact (if invoked) - - Dave tech design (`docs/tech-designs/`) + - Dave tech design (`internal-docs/tech-designs/`) - Dave implementation + unit tests - - Dave feature doc (`docs/features/`) + - Dave feature doc (`internal-docs/features/`) - Alice sign-off at each phase Note: preserve the existing maintenance tasks list and skill names (domain-specific). @@ -518,15 +518,15 @@ Replace with: ### Change 6 — Alice satisfaction criteria for feature doc Find: ### Alice is satisfied with Dave's feature doc when: - - [ ] F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md` + - [ ] F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md` - [ ] All template sections complete (Business Purpose, Call Chain, Files, Tests) - [ ] No open challenge items without a response Replace with: ### Alice is satisfied with Dave's feature doc when: - - [ ] Impact scan table was printed — every changed file checked against `docs/features/INDEX.md` + - [ ] Impact scan table was printed — every changed file checked against `internal-docs/features/INDEX.md` - [ ] All affected existing F-NNN docs updated, or "none affected" explicitly stated - - [ ] F-NNN doc written to `docs/features/` and added to `docs/features/INDEX.md` + - [ ] F-NNN doc written to `internal-docs/features/` and added to `internal-docs/features/INDEX.md` - [ ] All template sections complete (Business Purpose, Call Chain, Files, Tests) - [ ] No open challenge items without a response diff --git a/docs/features/DIAGRAM.md b/internal-docs/features/DIAGRAM.md similarity index 100% rename from docs/features/DIAGRAM.md rename to internal-docs/features/DIAGRAM.md diff --git a/docs/features/F-001-sdk-initialization.md b/internal-docs/features/F-001-sdk-initialization.md similarity index 100% rename from docs/features/F-001-sdk-initialization.md rename to internal-docs/features/F-001-sdk-initialization.md diff --git a/docs/features/F-002-sdk-start.md b/internal-docs/features/F-002-sdk-start.md similarity index 100% rename from docs/features/F-002-sdk-start.md rename to internal-docs/features/F-002-sdk-start.md diff --git a/docs/features/F-003-sdk-plugin-version-retrieval.md b/internal-docs/features/F-003-sdk-plugin-version-retrieval.md similarity index 100% rename from docs/features/F-003-sdk-plugin-version-retrieval.md rename to internal-docs/features/F-003-sdk-plugin-version-retrieval.md diff --git a/docs/features/F-004-in-app-event-logging.md b/internal-docs/features/F-004-in-app-event-logging.md similarity index 100% rename from docs/features/F-004-in-app-event-logging.md rename to internal-docs/features/F-004-in-app-event-logging.md diff --git a/docs/features/F-005-ad-revenue-logging.md b/internal-docs/features/F-005-ad-revenue-logging.md similarity index 100% rename from docs/features/F-005-ad-revenue-logging.md rename to internal-docs/features/F-005-ad-revenue-logging.md diff --git a/docs/features/F-006-custom-host-configuration.md b/internal-docs/features/F-006-custom-host-configuration.md similarity index 100% rename from docs/features/F-006-custom-host-configuration.md rename to internal-docs/features/F-006-custom-host-configuration.md diff --git a/docs/features/F-007-device-id-collection-optout.md b/internal-docs/features/F-007-device-id-collection-optout.md similarity index 100% rename from docs/features/F-007-device-id-collection-optout.md rename to internal-docs/features/F-007-device-id-collection-optout.md diff --git a/docs/features/F-008-manual-imei-android-id-override.md b/internal-docs/features/F-008-manual-imei-android-id-override.md similarity index 100% rename from docs/features/F-008-manual-imei-android-id-override.md rename to internal-docs/features/F-008-manual-imei-android-id-override.md diff --git a/docs/features/F-009-min-time-between-sessions.md b/internal-docs/features/F-009-min-time-between-sessions.md similarity index 100% rename from docs/features/F-009-min-time-between-sessions.md rename to internal-docs/features/F-009-min-time-between-sessions.md diff --git a/docs/features/F-010-currency-code-setting.md b/internal-docs/features/F-010-currency-code-setting.md similarity index 100% rename from docs/features/F-010-currency-code-setting.md rename to internal-docs/features/F-010-currency-code-setting.md diff --git a/docs/features/F-011-tcf-dma-automatic-consent-collection.md b/internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md similarity index 100% rename from docs/features/F-011-tcf-dma-automatic-consent-collection.md rename to internal-docs/features/F-011-tcf-dma-automatic-consent-collection.md diff --git a/docs/features/F-012-manual-gdpr-dma-consent-api.md b/internal-docs/features/F-012-manual-gdpr-dma-consent-api.md similarity index 100% rename from docs/features/F-012-manual-gdpr-dma-consent-api.md rename to internal-docs/features/F-012-manual-gdpr-dma-consent-api.md diff --git a/docs/features/F-013-user-anonymization.md b/internal-docs/features/F-013-user-anonymization.md similarity index 100% rename from docs/features/F-013-user-anonymization.md rename to internal-docs/features/F-013-user-anonymization.md diff --git a/docs/features/F-014-manual-deep-link-retrigger.md b/internal-docs/features/F-014-manual-deep-link-retrigger.md similarity index 100% rename from docs/features/F-014-manual-deep-link-retrigger.md rename to internal-docs/features/F-014-manual-deep-link-retrigger.md diff --git a/docs/features/F-015-customer-user-id.md b/internal-docs/features/F-015-customer-user-id.md similarity index 100% rename from docs/features/F-015-customer-user-id.md rename to internal-docs/features/F-015-customer-user-id.md diff --git a/docs/features/F-016-update-vs-fresh-install-flag.md b/internal-docs/features/F-016-update-vs-fresh-install-flag.md similarity index 100% rename from docs/features/F-016-update-vs-fresh-install-flag.md rename to internal-docs/features/F-016-update-vs-fresh-install-flag.md diff --git a/docs/features/F-017-sdk-kill-switch.md b/internal-docs/features/F-017-sdk-kill-switch.md similarity index 100% rename from docs/features/F-017-sdk-kill-switch.md rename to internal-docs/features/F-017-sdk-kill-switch.md diff --git a/docs/features/F-018-uninstall-measurement.md b/internal-docs/features/F-018-uninstall-measurement.md similarity index 100% rename from docs/features/F-018-uninstall-measurement.md rename to internal-docs/features/F-018-uninstall-measurement.md diff --git a/docs/features/F-019-user-email-collection.md b/internal-docs/features/F-019-user-email-collection.md similarity index 100% rename from docs/features/F-019-user-email-collection.md rename to internal-docs/features/F-019-user-email-collection.md diff --git a/docs/features/F-020-appsflyer-uid-retrieval.md b/internal-docs/features/F-020-appsflyer-uid-retrieval.md similarity index 100% rename from docs/features/F-020-appsflyer-uid-retrieval.md rename to internal-docs/features/F-020-appsflyer-uid-retrieval.md diff --git a/docs/features/F-021-delayed-session-start-pending-cuid.md b/internal-docs/features/F-021-delayed-session-start-pending-cuid.md similarity index 100% rename from docs/features/F-021-delayed-session-start-pending-cuid.md rename to internal-docs/features/F-021-delayed-session-start-pending-cuid.md diff --git a/docs/features/F-022-push-notification-deep-link-path-config.md b/internal-docs/features/F-022-push-notification-deep-link-path-config.md similarity index 100% rename from docs/features/F-022-push-notification-deep-link-path-config.md rename to internal-docs/features/F-022-push-notification-deep-link-path-config.md diff --git a/docs/features/F-023-in-app-purchase-validation-v1.md b/internal-docs/features/F-023-in-app-purchase-validation-v1.md similarity index 100% rename from docs/features/F-023-in-app-purchase-validation-v1.md rename to internal-docs/features/F-023-in-app-purchase-validation-v1.md diff --git a/docs/features/F-024-in-app-purchase-validation-v2.md b/internal-docs/features/F-024-in-app-purchase-validation-v2.md similarity index 100% rename from docs/features/F-024-in-app-purchase-validation-v2.md rename to internal-docs/features/F-024-in-app-purchase-validation-v2.md diff --git a/docs/features/F-025-ios-receipt-validation-sandbox-toggle.md b/internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md similarity index 100% rename from docs/features/F-025-ios-receipt-validation-sandbox-toggle.md rename to internal-docs/features/F-025-ios-receipt-validation-sandbox-toggle.md diff --git a/docs/features/F-026-additional-custom-data.md b/internal-docs/features/F-026-additional-custom-data.md similarity index 100% rename from docs/features/F-026-additional-custom-data.md rename to internal-docs/features/F-026-additional-custom-data.md diff --git a/docs/features/F-027-user-invite-link-generation-onelink.md b/internal-docs/features/F-027-user-invite-link-generation-onelink.md similarity index 100% rename from docs/features/F-027-user-invite-link-generation-onelink.md rename to internal-docs/features/F-027-user-invite-link-generation-onelink.md diff --git a/docs/features/F-028-app-invite-onelink-id-configuration.md b/internal-docs/features/F-028-app-invite-onelink-id-configuration.md similarity index 100% rename from docs/features/F-028-app-invite-onelink-id-configuration.md rename to internal-docs/features/F-028-app-invite-onelink-id-configuration.md diff --git a/docs/features/F-029-cross-promotion-impression-click-tracking.md b/internal-docs/features/F-029-cross-promotion-impression-click-tracking.md similarity index 100% rename from docs/features/F-029-cross-promotion-impression-click-tracking.md rename to internal-docs/features/F-029-cross-promotion-impression-click-tracking.md diff --git a/docs/features/F-030-custom-branded-onelink-domains.md b/internal-docs/features/F-030-custom-branded-onelink-domains.md similarity index 100% rename from docs/features/F-030-custom-branded-onelink-domains.md rename to internal-docs/features/F-030-custom-branded-onelink-domains.md diff --git a/docs/features/F-031-push-notification-data-handling.md b/internal-docs/features/F-031-push-notification-data-handling.md similarity index 100% rename from docs/features/F-031-push-notification-data-handling.md rename to internal-docs/features/F-031-push-notification-data-handling.md diff --git a/docs/features/F-032-facebook-deferred-app-links.md b/internal-docs/features/F-032-facebook-deferred-app-links.md similarity index 100% rename from docs/features/F-032-facebook-deferred-app-links.md rename to internal-docs/features/F-032-facebook-deferred-app-links.md diff --git a/docs/features/F-033-skadnetwork-opt-out.md b/internal-docs/features/F-033-skadnetwork-opt-out.md similarity index 100% rename from docs/features/F-033-skadnetwork-opt-out.md rename to internal-docs/features/F-033-skadnetwork-opt-out.md diff --git a/docs/features/F-034-advertising-identifier-collection-disable.md b/internal-docs/features/F-034-advertising-identifier-collection-disable.md similarity index 100% rename from docs/features/F-034-advertising-identifier-collection-disable.md rename to internal-docs/features/F-034-advertising-identifier-collection-disable.md diff --git a/docs/features/F-035-conversion-data-callback.md b/internal-docs/features/F-035-conversion-data-callback.md similarity index 100% rename from docs/features/F-035-conversion-data-callback.md rename to internal-docs/features/F-035-conversion-data-callback.md diff --git a/docs/features/F-036-app-open-attribution-callback.md b/internal-docs/features/F-036-app-open-attribution-callback.md similarity index 100% rename from docs/features/F-036-app-open-attribution-callback.md rename to internal-docs/features/F-036-app-open-attribution-callback.md diff --git a/docs/features/F-037-unified-deep-linking-callback-and-models.md b/internal-docs/features/F-037-unified-deep-linking-callback-and-models.md similarity index 100% rename from docs/features/F-037-unified-deep-linking-callback-and-models.md rename to internal-docs/features/F-037-unified-deep-linking-callback-and-models.md diff --git a/docs/features/F-038-legacy-purchase-validation-notification-callback.md b/internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md similarity index 100% rename from docs/features/F-038-legacy-purchase-validation-notification-callback.md rename to internal-docs/features/F-038-legacy-purchase-validation-notification-callback.md diff --git a/docs/features/F-039-native-ios-deep-link-entry-points.md b/internal-docs/features/F-039-native-ios-deep-link-entry-points.md similarity index 100% rename from docs/features/F-039-native-ios-deep-link-entry-points.md rename to internal-docs/features/F-039-native-ios-deep-link-entry-points.md diff --git a/docs/features/F-040-android-new-intent-deep-link-forwarding.md b/internal-docs/features/F-040-android-new-intent-deep-link-forwarding.md similarity index 100% rename from docs/features/F-040-android-new-intent-deep-link-forwarding.md rename to internal-docs/features/F-040-android-new-intent-deep-link-forwarding.md diff --git a/docs/features/F-041-current-device-language-override.md b/internal-docs/features/F-041-current-device-language-override.md similarity index 100% rename from docs/features/F-041-current-device-language-override.md rename to internal-docs/features/F-041-current-device-language-override.md diff --git a/docs/features/F-042-partner-postback-sharing-filter.md b/internal-docs/features/F-042-partner-postback-sharing-filter.md similarity index 100% rename from docs/features/F-042-partner-postback-sharing-filter.md rename to internal-docs/features/F-042-partner-postback-sharing-filter.md diff --git a/docs/features/F-043-out-of-store-install-source.md b/internal-docs/features/F-043-out-of-store-install-source.md similarity index 100% rename from docs/features/F-043-out-of-store-install-source.md rename to internal-docs/features/F-043-out-of-store-install-source.md diff --git a/docs/features/F-044-partner-specific-data.md b/internal-docs/features/F-044-partner-specific-data.md similarity index 100% rename from docs/features/F-044-partner-specific-data.md rename to internal-docs/features/F-044-partner-specific-data.md diff --git a/docs/features/F-045-deep-link-url-resolution-allow-list.md b/internal-docs/features/F-045-deep-link-url-resolution-allow-list.md similarity index 100% rename from docs/features/F-045-deep-link-url-resolution-allow-list.md rename to internal-docs/features/F-045-deep-link-url-resolution-allow-list.md diff --git a/docs/features/F-046-disable-network-data.md b/internal-docs/features/F-046-disable-network-data.md similarity index 100% rename from docs/features/F-046-disable-network-data.md rename to internal-docs/features/F-046-disable-network-data.md diff --git a/docs/features/F-047-appset-id-collection-optout.md b/internal-docs/features/F-047-appset-id-collection-optout.md similarity index 100% rename from docs/features/F-047-appset-id-collection-optout.md rename to internal-docs/features/F-047-appset-id-collection-optout.md diff --git a/docs/features/F-048-plugin-metadata-reporting.md b/internal-docs/features/F-048-plugin-metadata-reporting.md similarity index 100% rename from docs/features/F-048-plugin-metadata-reporting.md rename to internal-docs/features/F-048-plugin-metadata-reporting.md diff --git a/docs/features/F-049-purchase-connector-configuration-lifecycle.md b/internal-docs/features/F-049-purchase-connector-configuration-lifecycle.md similarity index 100% rename from docs/features/F-049-purchase-connector-configuration-lifecycle.md rename to internal-docs/features/F-049-purchase-connector-configuration-lifecycle.md diff --git a/docs/features/F-050-purchase-connector-storekit-version-selection.md b/internal-docs/features/F-050-purchase-connector-storekit-version-selection.md similarity index 100% rename from docs/features/F-050-purchase-connector-storekit-version-selection.md rename to internal-docs/features/F-050-purchase-connector-storekit-version-selection.md diff --git a/docs/features/F-051-purchase-connector-android-validation-result-listeners.md b/internal-docs/features/F-051-purchase-connector-android-validation-result-listeners.md similarity index 100% rename from docs/features/F-051-purchase-connector-android-validation-result-listeners.md rename to internal-docs/features/F-051-purchase-connector-android-validation-result-listeners.md diff --git a/docs/features/F-052-purchase-connector-ios-combined-validation-callback.md b/internal-docs/features/F-052-purchase-connector-ios-combined-validation-callback.md similarity index 100% rename from docs/features/F-052-purchase-connector-ios-combined-validation-callback.md rename to internal-docs/features/F-052-purchase-connector-ios-combined-validation-callback.md diff --git a/docs/features/F-053-purchase-connector-google-play-data-models.md b/internal-docs/features/F-053-purchase-connector-google-play-data-models.md similarity index 100% rename from docs/features/F-053-purchase-connector-google-play-data-models.md rename to internal-docs/features/F-053-purchase-connector-google-play-data-models.md diff --git a/docs/features/F-054-purchase-connector-build-time-opt-in.md b/internal-docs/features/F-054-purchase-connector-build-time-opt-in.md similarity index 100% rename from docs/features/F-054-purchase-connector-build-time-opt-in.md rename to internal-docs/features/F-054-purchase-connector-build-time-opt-in.md diff --git a/docs/features/F-055-purchase-connector-missing-configuration-guard.md b/internal-docs/features/F-055-purchase-connector-missing-configuration-guard.md similarity index 100% rename from docs/features/F-055-purchase-connector-missing-configuration-guard.md rename to internal-docs/features/F-055-purchase-connector-missing-configuration-guard.md diff --git a/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 similarity index 100% rename from docs/features/F-056-app-invite-link-onelink-id-init-time.md rename to internal-docs/features/F-056-app-invite-link-onelink-id-init-time.md diff --git a/docs/features/F-057-asa-collection-optout.md b/internal-docs/features/F-057-asa-collection-optout.md similarity index 100% rename from docs/features/F-057-asa-collection-optout.md rename to internal-docs/features/F-057-asa-collection-optout.md diff --git a/docs/features/F-058-att-authorization-wait-timeout.md b/internal-docs/features/F-058-att-authorization-wait-timeout.md similarity index 100% rename from docs/features/F-058-att-authorization-wait-timeout.md rename to internal-docs/features/F-058-att-authorization-wait-timeout.md diff --git a/docs/features/F-059-debug-logging-toggle.md b/internal-docs/features/F-059-debug-logging-toggle.md similarity index 100% rename from docs/features/F-059-debug-logging-toggle.md rename to internal-docs/features/F-059-debug-logging-toggle.md diff --git a/docs/features/F-060-swift-package-manager-support.md b/internal-docs/features/F-060-swift-package-manager-support.md similarity index 100% rename from docs/features/F-060-swift-package-manager-support.md rename to internal-docs/features/F-060-swift-package-manager-support.md diff --git a/docs/features/INDEX.md b/internal-docs/features/INDEX.md similarity index 100% rename from docs/features/INDEX.md rename to internal-docs/features/INDEX.md diff --git a/docs/features/TEMPLATE.md b/internal-docs/features/TEMPLATE.md similarity index 100% rename from docs/features/TEMPLATE.md rename to internal-docs/features/TEMPLATE.md diff --git a/docs/prds/spm-support.md b/internal-docs/prds/spm-support.md similarity index 100% rename from docs/prds/spm-support.md rename to internal-docs/prds/spm-support.md diff --git a/docs/researches/R-001-spm-support.md b/internal-docs/researches/R-001-spm-support.md similarity index 100% rename from docs/researches/R-001-spm-support.md rename to internal-docs/researches/R-001-spm-support.md diff --git a/docs/tech-designs/spm-support.md b/internal-docs/tech-designs/spm-support.md similarity index 99% rename from docs/tech-designs/spm-support.md rename to internal-docs/tech-designs/spm-support.md index cc6a78f5..5766fa06 100644 --- a/docs/tech-designs/spm-support.md +++ b/internal-docs/tech-designs/spm-support.md @@ -1,7 +1,7 @@ --- ticket: DELIVERY-125462 -prd: docs/prds/spm-support.md -research: docs/researches/R-001-spm-support.md +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 --- From a6fe723b279baf28b0aa04ef54f214788f136e2d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" <kobi.kagan@appsflyer.com> Date: Sun, 26 Jul 2026 09:46:58 +0300 Subject: [PATCH 4/6] Remove plans/ and templates/ artifacts per review Per Dani's review on #459: plans/01-ios-spm-core-support.md and templates/af-tech-quiz-template.html shouldn't be committed to the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --- plans/01-ios-spm-core-support.md | 202 --------------------------- templates/af-tech-quiz-template.html | 194 ------------------------- 2 files changed, 396 deletions(-) delete mode 100644 plans/01-ios-spm-core-support.md delete mode 100644 templates/af-tech-quiz-template.html diff --git a/plans/01-ios-spm-core-support.md b/plans/01-ios-spm-core-support.md deleted file mode 100644 index 97b4244f..00000000 --- a/plans/01-ios-spm-core-support.md +++ /dev/null @@ -1,202 +0,0 @@ -# Plan: Add Swift Package Manager (SPM) support to Core (DELIVERY-125462) - -Jira: https://appsflyer.atlassian.net/browse/DELIVERY-125462 (P1, assignee: Kobi Kagan, target: end of July 2026, ships inside v6.18.0 line) - -## Goal - -Add a `Package.swift` for the plugin's **Core** module so SPM-enabled Flutter apps (Flutter 3.44+ default) can build against this plugin without CocoaPods, while: -- Keeping the **PurchaseConnector** subspec CocoaPods-only (blocked upstream by [flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182) — no SPM opt-in mechanism exists in Flutter tooling today) -- Preserving 100% CocoaPods backward compatibility for apps not yet on SPM - ---- - -## Phase 0: Documentation Discovery (consolidated findings — do not re-derive, cite these) - -### A. Prior art in this repo — three existing draft PRs, none merged - -| PR | Approach | Verdict | -|---|---|---| -| [#454](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/454) (`nurlangarash`) | True `git mv` of `Classes/*` → `ios/appsflyer_sdk/Sources/appsflyer_sdk/` (+headers → `.../include/appsflyer_sdk/`), podspec updated to match, single source of truth. Depends on `AppsFlyerFramework-Static` / product `AppsFlyerLib-Static` (matches `static_framework = true`) — this product name is wrong, corrected to `AppsFlyerLib` during implementation. Copilot flagged `.iOS("12.0")` as invalid and a product/target name mismatch — the name mismatch was real, but the `.iOS("12.0")` flag was itself wrong (verified valid via `swift package describe`; not something that needed fixing). PurchaseConnector explicitly untouched. | **Use this as the base.** | -| [#455](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/455) (`TeddyYeung`) | Duplicates Core sources into a second tree, leaves podspec pointing at old `Classes/` — two copies to maintain forever. Uses the same `.iOS("12.0")` syntax, which is valid (see #454's corrected note) and was never actually a bug. Adds `.gitignore` entries (`.build/`, `.swiftpm/`) — worth cherry-picking. | Reject the architecture; take only the `.gitignore` hunk. | -| [#370](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/pull/370) (`alejandro-all-win-software`, oldest) | Tried to fold PurchaseConnector itself into SPM via an env-var-gated target (`ENABLE_PURCHASE_CONNECTOR=1`) + dependency on `appsflyer-apple-purchase-connector`. Author's own comment: *"blocked by flutter/flutter#161182... if you find another way to opt in to Purchase Connector, I'd be happy to close this PR in favor of that approach."* | **Do not repeat this.** This is exactly the dead end DELIVERY-125462 tells us to route around by staying CocoaPods-only for PurchaseConnector. | - -None of the three have maintainer review; all are `REVIEW_REQUIRED`/`BLOCKED` on branch protection only (CI/security scans pass on all three). - -### B. Ground-truth structure (verified against a real, live, first-party plugin: `image_picker_ios` in `flutter/packages`, not just docs prose) - -Target layout (adapted to our plugin, matches what #454 already did): -``` -ios/appsflyer_sdk.podspec # unchanged location, paths updated -ios/appsflyer_sdk/Package.swift # new -ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m -ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m -ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m -ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h -ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h -ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h -ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h -``` -Our case is simpler than `image_picker_ios`: our podspec has never set a custom `s.module_map`, so no umbrella header / `.modulemap` file is needed — CocoaPods' and SwiftPM's default module generation both suffice. Don't add one (that would be inventing a requirement we don't have). - -`Package.swift` (the corrected version from #454's follow-up commit is the right shape): -```swift -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "appsflyer_sdk", - platforms: [.iOS(.v12)], - products: [ - .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) - ], - dependencies: [ - .package(url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static.git", exact: "6.18.0") - ], - targets: [ - .target( - name: "appsflyer_sdk", - dependencies: [ - .product(name: "AppsFlyerLib-Static", package: "AppsFlyerFramework-Static") - ], - path: "Sources/appsflyer_sdk", - cSettings: [ - .headerSearchPath("include/appsflyer_sdk") - ] - ) - ] -) -``` -~~Anti-pattern guard (from Copilot's actual review on #454): `.iOS("12.0")` is **not** valid SwiftPM API — must be `.iOS(.v12)`.~~ **Corrected during implementation review**: this claim is wrong. `SupportedPlatform.IOSVersion` conforms to `ExpressibleByStringLiteral`, so `.iOS("12.0")` is valid and compiles. Verified directly: the shipped `Package.swift` uses `.iOS("12.0")` and `swift package describe` resolves `Platforms: Name: ios, Version: 12.0` with no error. Both `.iOS("12.0")` and `.iOS(.v12)` work; this repo uses the former. Library name uses hyphens (`appsflyer-sdk`), target/package name keeps underscores (`appsflyer_sdk`) — this is Flutter's documented convention, not a typo. - -Podspec `Core` subspec path update (mirrors #454 exactly): -```ruby -# before -ss.source_files = 'Classes/**/*' -ss.public_header_files = 'Classes/**/*.h' -# after -ss.source_files = 'appsflyer_sdk/Sources/appsflyer_sdk/**/*.{h,m}' -ss.public_header_files = 'appsflyer_sdk/Sources/appsflyer_sdk/include/**/*.h' -``` -`PurchaseConnector` subspec: **zero changes.** - -### C. Repo conventions (fact-checked, not assumed) - -- **CHANGELOG.md**: `## <version>` header, flat `-` bullets, newest on top. Current top entry is `## 6.18.0`. -- **pubspec.yaml**: `version: 6.18.0`, no existing SPM-related keys — none are required; Flutter auto-detects `Package.swift` by convention path, no pubspec opt-in needed. -- **CI**: `.github/workflows/lint-test-build.yml` (`build-ios` job) and `ios-e2e.yml` (`e2e-ios` job) both do `pod install` + `flutter build ios` — pure CocoaPods today, zero SPM verification exists anywhere in CI. -- **Docs**: `doc/Installation.md` has no SPM section at all today. `doc/PurchaseConnector.md:74-76` documents the CocoaPods-only opt-in (`$AppsFlyerPurchaseConnector = true` in Podfile) — this needs a caveat added (see Phase 4). -- **PurchaseConnector opt-in today**, confirmed by repo-wide grep, is exclusively: `if defined?($AppsFlyerPurchaseConnector)` in the podspec, set by the consumer's own Podfile. The example app itself does **not** set this flag. -- Public header surface for `Core` is exactly 4 files: `AppsFlyerAttribution.h`, `AppsFlyerStreamHandler.h`, `AppsflyerSdkPlugin.h`, `FlutterAppDelegate+AppsFlyerStreamHandler.h`. -- `AppsflyerSdkPlugin.m` already guards PurchaseConnector registration behind `#ifdef ENABLE_PURCHASE_CONNECTOR` (a preprocessor flag CocoaPods sets via `GCC_PREPROCESSOR_DEFINITIONS`). **This flag is never defined in the new `Package.swift`, so that code path is simply compiled out under SPM — no dangling reference, nothing to fix here.** - -### D. Known architectural limitation to surface, not hide - -Per Flutter's own SPM integration model: once a plugin ships a `Package.swift`, Flutter routes that **entire plugin** through SPM for any app that has SPM enabled — the podspec's fallback Podfile path is not used for that plugin at all in that mode. That means **an SPM-enabled consumer app cannot reach the `PurchaseConnector` subspec or its `$AppsFlyerPurchaseConnector` Podfile flag at all** — not "it might not work," but "the mechanism that would enable it never runs." This is the concrete, user-facing shape of the flutter/flutter#161182 blocker, and it must be stated explicitly in docs (Phase 4), not left for users to discover as a silent failure. - -### E. Do SwiftPM "Package Traits" close this gap? No — checked and ruled out, don't revisit without new evidence - -SwiftPM added **Package Traits** in Swift 6.1 ([docs.swift.org/swiftpm/.../packagetraits](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/packagetraits/)) — a real, build-time optional-feature mechanism (`traits: [.default(enabledTraits:...), .trait(name:...)]` in the package author's manifest; conditional compilation via `#if TraitName`). This is architecturally the kind of thing that could gate an optional `PurchaseConnector` target. It does **not** change our plan, for two concrete reasons: - -1. **Traits are enabled by the consumer's own `Package.swift`** — `.package(url: ..., traits: [.init(name: "PurchaseConnector")])` — or via CLI (`swift build --traits X`). Flutter apps don't have a hand-authored `Package.swift`; Flutter's tooling *generates* `FlutterGeneratedPluginSwiftPackage` automatically from `pubspec.yaml`, and that generation pipeline has **no trait-selection input** today. There is nowhere in `pubspec.yaml` or `flutter build` for an app developer to say "enable the PurchaseConnector trait." This is exactly the surface flutter/flutter#161182 would need to add — it remains open/unresolved as of this writing. -2. **Requires `swift-tools-version: 6.1`.** Our `Package.swift` (Phase 2) is `5.9`, matching Flutter's own official template and PR #454's precedent, chosen for the widest Xcode/toolchain compatibility against our `iOS 12` deployment target. Bumping to 6.1 to get traits would raise the minimum Xcode/Swift toolchain for every consumer, for a feature-gate mechanism Flutter can't even plumb through yet — not a reasonable trade today. - -**Conclusion: no plan change.** Keep Core-only SPM + CocoaPods-only PurchaseConnector as decided. Revisit only if Flutter ships trait pass-through from `pubspec.yaml`/`flutter build` (i.e. flutter/flutter#161182 or a successor issue closes with that shape) — at that point, adding a `purchase_connector` trait to `Package.swift` would be the natural next step, gated on bumping `swift-tools-version` to 6.1+. - ---- - -## Phase 1: Establish the branch from PR #454 - -**What to do:** -1. `gh pr checkout 454` (or fetch `nurlangarash:feat/swift-package-manager-support`) into a new local branch off current `master`. -2. Rebase onto current `master` HEAD (`df7f4854`) — resolve `CHANGELOG.md` conflicts by keeping master's `## 6.18.0` entry intact and adding the new SPM entry above/alongside it per Phase 3, not overwriting it. -3. Confirm the rebased diff still matches the structure in Phase 0.B exactly — no drift from master's current `Classes/*` file set (master has 4 headers + `AppsFlyerAttribution.m`/`AppsFlyerStreamHandler.m`/`AppsflyerSdkPlugin.m`; confirm #454's `git mv` list is unchanged since June 22). - -**Verification:** `git diff master --stat` shows only expected renames/adds — no unexpected deletions, no `ios/PurchaseConnector/**` touched, no `ios/Classes/**` files left behind untouched (they should all be gone, replaced by the SPM tree — CocoaPods now points at the new path per Phase 0.B). - -**Anti-pattern guard:** Do not adopt #455's duplication approach even partially — no two copies of the same `.m`/`.h` file should exist after this phase. - ---- - -## Phase 2: Structural fixes & cleanup - -**What to do:** -1. Cherry-pick #455's `.gitignore` addition: `.build/`, `.swiftpm/`. -2. Confirm `Package.swift` matches the shipped, verified form: `.iOS("12.0")` (confirmed valid — see Phase 0.B correction); product `AppsFlyerLib` from `AppsFlyerFramework` (not `AppsFlyerLib-Static`/`AppsFlyerFramework-Static` — that naming was wrong, see #454 correction above), pinned `.exact("6.18.0")` to match the podspec's `AppsFlyerFramework` pin exactly. -3. Confirm `cSettings: [.headerSearchPath("include/appsflyer_sdk")]` is present (required for the `.m` files' `#import` statements to resolve). -4. Confirm all 4 public headers live under `Sources/appsflyer_sdk/include/appsflyer_sdk/` and the 3 `.m` files live directly under `Sources/appsflyer_sdk/` (not under `include/`). - -**Verification:** -- `grep -rn "ENABLE_PURCHASE_CONNECTOR" ios/appsflyer_sdk/Sources/` — confirm the `#ifdef` guard is untouched and no unconditional reference to `PurchaseConnectorPlugin` was introduced. -- `pod lib lint ios/appsflyer_sdk.podspec --configuration=Debug --skip-tests --use-modular-headers` passes (validates the CocoaPods path still resolves against the new file paths). - -**Anti-pattern guard:** Do not add a `.modulemap`/umbrella header — our podspec never had one; don't invent structure `image_picker_ios` needed for reasons that don't apply here. - ---- - -## Phase 3: Version bump & CHANGELOG - -**Confirmed baseline (verified directly against `releases/6.x.x/6.18.x/6.18.0-rc1`, the actual last-released branch — identical to current `master`, nothing later exists in this repo):** -- Plugin version: `6.18.0` -- iOS Core `AppsFlyerFramework`: `6.18.0` -- iOS `PurchaseConnector` (podspec: `ss.ios.dependency 'PurchaseConnector', '6.18.0'`): `6.18.0` -- Android `purchase-connector` (`build.gradle`: `implementation 'com.appsflyer:purchase-connector:2.2.0'`): `2.2.0` - -This is a **plugin-only** change (adds SPM plumbing, does not touch native SDK versions) — so none of the above native pins change. `AppsFlyerFramework-Static` in `Package.swift` (Phase 2) must pin `exact: "6.18.0"` to match, not any other number. - -**What to do:** -1. Bump `pubspec.yaml` `version:` from `6.18.0` to `6.18.0+1` (matches repo's existing `+N` build-suffix convention for same-SDK-version plugin updates, e.g. `6.17.7+1` seen in CHANGELOG history). -2. Add new top `CHANGELOG.md` entry above `## 6.18.0`: - ``` - ## 6.18.0+1 - - - Added Swift Package Manager (SPM) support for the Core module (iOS). PurchaseConnector remains CocoaPods-only pending flutter/flutter#161182 (iOS PurchaseConnector 6.18.0 / Android purchase-connector 2.2.0 unchanged). - ``` - -**Verification:** `grep -A3 "^## 6.18.0+1" CHANGELOG.md` shows the new entry; `pubspec.yaml` version matches; confirm no native dependency version in podspec/build.gradle/Package.swift was changed by this phase. - -**Note:** Confirm this version number against whatever the `rc-release` skill / RC pipeline expects before tagging — don't hardcode a release version without checking the active RC process. - ---- - -## Phase 4: Documentation updates - -**What to do:** -1. `doc/Installation.md`: add a new "Swift Package Manager" section documenting that Core supports SPM as of this version, and that apps must still use CocoaPods if they need PurchaseConnector. -2. `doc/PurchaseConnector.md`: add an explicit caveat near the "How to Opt-In" section (lines ~66-82): *PurchaseConnector requires CocoaPods; it is not available in SPM-enabled apps until [flutter/flutter#161182](https://github.com/flutter/flutter/issues/161182) is resolved.* -3. `README.md`: no structural change needed (it only links to `doc/Installation.md`), but confirm the SDK Versions table still matches `6.18.0`/`AppsFlyerFramework-Static` if that pin changes. - -**Verification:** Manual read-through; confirm no doc implies PurchaseConnector "might work" under SPM — it must state plainly that it does not. - -**Anti-pattern guard:** Do not word this as "partial support" or "coming soon" — per Phase 0.D this is a hard mechanism gap, not a rough edge. - ---- - -## Phase 5: CI verification - -**What to do:** -1. Confirm existing `lint-test-build.yml` (`build-ios`) and `ios-e2e.yml` (`e2e-ios`) jobs still pass unmodified — these exercise the CocoaPods path (regression check for apps not on SPM). -2. Add a new step or job (e.g. `build-ios-spm` in `lint-test-build.yml`) that runs against the `example/` app with SPM enabled (`flutter config --enable-swift-package-manager`) and does `flutter build ios --no-codesign` — verifying the Core-only SPM path resolves and links. -3. Manually verify (not necessarily CI-gated, since it's an intentional non-feature) that an SPM-enabled example app cannot reach PurchaseConnector — i.e. confirm there's no `pod install` step running for `appsflyer_sdk` at all in that mode, consistent with Phase 0.D. - -**Full verification matrix required by the ticket:** - -| Build mode | PurchaseConnector requested? | Expected result | -|---|---|---| -| CocoaPods (existing) | No | Builds, Core only — unchanged from today | -| CocoaPods (existing) | Yes (`$AppsFlyerPurchaseConnector = true`) | Builds with PurchaseConnector — unchanged from today | -| SPM (new) | No | Builds, Core only — **new capability** | -| SPM (new) | Yes (attempted) | No mechanism to opt in — confirm this fails/is absent cleanly, not silently broken | - -**Anti-pattern guard:** Don't treat the CI job as "just make the SPM path build once" — the ticket explicitly requires verifying all four rows above. - ---- - -## Final Phase: Sign-off checklist - -1. `git diff master --stat` reviewed — matches Phase 0/1/2 scope exactly, nothing extra. -2. All four build-matrix rows in Phase 5 verified with evidence (CI logs or local build output). -3. `CHANGELOG.md` and `pubspec.yaml` version bumped per Phase 3. -4. `doc/Installation.md` and `doc/PurchaseConnector.md` updated per Phase 4, explicitly stating the PurchaseConnector/SPM limitation. -5. ~~No `.iOS("12.0")`-style invalid SwiftPM API left in `Package.swift`~~ — retracted: `.iOS("12.0")` is valid (verified via `swift package describe`); the shipped `Package.swift` intentionally uses this form. -6. No duplicate source files between `ios/Classes/` (should no longer exist) and `ios/appsflyer_sdk/Sources/`. -7. Ready to open a PR against DELIVERY-125462, referencing and closing out #454/#455/#370 in the description (crediting their work, explaining why #454 was chosen as base). 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 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8"> -<meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>Tech Quiz · {{QUIZ_TITLE}} - - - -
- - - From 56be5494fa14d70de992fe640aa605f5a8c69651 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 26 Jul 2026 10:12:40 +0300 Subject: [PATCH 5/6] Add user-facing SPM guidance, fix stale path refs in F-060 Per Dani's review on #459: - doc/Installation.md: new section on iOS SPM support for Core, with a pointer to the PurchaseConnector CocoaPods-only caveat. - doc/PurchaseConnector.md: explicit warning that Purchase Connector requires CocoaPods even when the app has adopted SPM for Core, and what happens (MissingPluginException) if it's skipped entirely. - F-060: fixed two docs/researches/... references missed in the earlier docs/ -> internal-docs/ rename sweep. Co-Authored-By: Claude Sonnet 5 --- doc/Installation.md | 7 +++++++ doc/PurchaseConnector.md | 2 ++ .../features/F-060-swift-package-manager-support.md | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) 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-060-swift-package-manager-support.md b/internal-docs/features/F-060-swift-package-manager-support.md index 68a28995..1b5a962b 100644 --- a/internal-docs/features/F-060-swift-package-manager-support.md +++ b/internal-docs/features/F-060-swift-package-manager-support.md @@ -83,8 +83,8 @@ No dedicated automated test — this is a build-configuration/distribution-mecha ## 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 (`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 `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). +- **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. --- From c6ba1391b7391641cb5291260241a680512e6995 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 26 Jul 2026 10:19:22 +0300 Subject: [PATCH 6/6] Reconcile F-060's verification status with actual E2E evidence Per Dani's review: F-060 said device verification hadn't run while the PR description said E2E passed in full. Reconciled with links to all 3 real CI runs now completed, and honestly flagged the one remaining gap (SPM+PurchaseConnector inertness was reasoned through, not actively tested). Co-Authored-By: Claude Sonnet 5 --- .../features/F-060-swift-package-manager-support.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal-docs/features/F-060-swift-package-manager-support.md b/internal-docs/features/F-060-swift-package-manager-support.md index 1b5a962b..5ba4dddf 100644 --- a/internal-docs/features/F-060-swift-package-manager-support.md +++ b/internal-docs/features/F-060-swift-package-manager-support.md @@ -73,11 +73,15 @@ CocoaPods path (resolved by `pod install` at install time, unchanged behavior): ## 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 range `6.18.0..<7.0.0` and picks up all 3 Core `.m` sources correctly. +- `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). -> **Outstanding pre-release gate**: the tech design's mandatory 4-path real-device build verification (SPM Core-only / CocoaPods Core-only / CocoaPods Core+PurchaseConnector / confirming SPM+PurchaseConnector is inert, all on real devices, not `--no-codesign` alone) has **not yet been run** — it requires a full macOS/Xcode/iOS-device environment that was unavailable during implementation. This must be completed before this ships (before promoting through the RC pipeline). See `docs/tech-designs/spm-support.md` for the exact verification steps. +> **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. ---