diff --git a/.github/scripts/check-single-copy.sh b/.github/scripts/check-single-copy.sh new file mode 100755 index 0000000..2b3179c --- /dev/null +++ b/.github/scripts/check-single-copy.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Counts the Mach-O images in an .app that define each class and expects exactly 1 each. Exits non-zero +# when an SDK is linked twice, for example from both CocoaPods and Swift Package Manager. +# Usage: .github/scripts/check-single-copy.sh path/to/Runner.app +set -u +APP="${1}" +bins=("${APP}/$(basename "${APP}" .app)" "${APP}/$(basename "${APP}" .app).debug.dylib") +while IFS= read -r fw; do + bins+=("${fw}/$(basename "${fw}" .framework)") +done < <(find "${APP}/Frameworks" -maxdepth 1 -name '*.framework' 2>/dev/null || true) + +status=0 +for cls in MParticle RoktEmbeddedView MPKitRokt; do + hits=() + for b in "${bins[@]}"; do + [[ -f ${b} ]] || continue + if nm -U "${b}" 2>/dev/null | grep -qE "_OBJC_CLASS_\\\$_${cls}\$"; then hits+=("$(basename "${b}")"); fi + done + echo "${cls}: ${#hits[@]} [${hits[*]-}]" + [[ ${#hits[@]} -eq 1 ]] || status=1 +done +exit "${status}" diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 78a82ac..2b96614 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -34,13 +34,65 @@ jobs: - name: Test working-directory: example run: flutter test + - name: Switch example to CocoaPods + working-directory: example/ios + run: | + # The example gets the kits from Swift Package Manager. Remove every Swift package from the + # project and add the kits as pods instead, otherwise the app links a second copy of the SDK. + # That includes Flutter's generated packages, which Flutter versions before 3.24 never generate. + ruby -rxcodeproj -e ' + project = Xcodeproj::Project.open("Runner.xcodeproj") + target = project.targets.find { |t| t.name == "Runner" } + target.package_product_dependencies.dup.each do |dep| + target.frameworks_build_phase.files.select { |f| f.product_ref == dep }.each(&:remove_from_project) + dep.remove_from_project + end + project.root_object.package_references.dup.each(&:remove_from_project) + project.files.select { |f| f.path.to_s.start_with?("Flutter/ephemeral/Packages/") }.each(&:remove_from_project) + project.save' + cat > Podfile <<'PODFILE' + platform :ios, '15.6' + ENV['COCOAPODS_DISABLE_STATS'] = 'true' + project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release } + + def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + end + + require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + flutter_ios_podfile_setup + + target 'Runner' do + use_frameworks! + use_modular_headers! + pod 'mParticle-Rokt' + pod 'RoktPaymentExtension' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + end + + post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + # mParticle-Apple-SDK's podspec declares iOS 15.0 but re-exports RoktContracts, which requires 15.6. + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6' + end + end + end + PODFILE - name: Build example iOS app working-directory: example run: | flutter pub get flutter build ios --config-only --no-codesign --release cd ios - xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release -sdk iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -quiet + xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release -sdk iphoneos -derivedDataPath ../build/dd CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -quiet + - name: Check the SDK is linked once + run: .github/scripts/check-single-copy.sh example/build/dd/Build/Products/Release-iphoneos/Runner.app - name: Cleanup if: ${{ always() }} working-directory: example @@ -54,13 +106,9 @@ jobs: name: Build iOS flutter app (Swift Package Manager) runs-on: macos-15 steps: + # The default checkout folder, mparticle-flutter-sdk, differs from the Dart package name on + # purpose: the example must build from a normal clone (https://github.com/flutter/flutter/issues/186881). - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - # The example app depends on the plugin by path, and Swift Package - # Manager derives a path dependency's identity from the enclosing - # git directory name. That name must equal the Dart package name or - # resolution fails. See https://github.com/flutter/flutter/issues/186881. - path: mparticle_flutter_sdk - name: Set up Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: @@ -68,21 +116,30 @@ jobs: - name: Setup Flutter SDK uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: - # Flutter's SwiftPM plugin scheme depends on the generated - # FlutterFramework package, which only exists in 3.44 and later. + # The plugin's Package.swift depends on the generated FlutterFramework + # package, which only exists in Flutter 3.41 and later. flutter-version: ${{ vars.SPM_FLUTTER_VERSION || '3.47.4' }} channel: stable cache: true - name: Enable Swift Package Manager run: flutter config --enable-swift-package-manager - name: Install example dependencies - working-directory: mparticle_flutter_sdk/example + working-directory: example run: flutter pub get + - name: Remove the CocoaPods spec + # Without the podspec the plugin cannot fall back to CocoaPods unnoticed. + run: rm ios/mparticle_flutter_sdk.podspec - name: Generate iOS project with Swift Package Manager - working-directory: mparticle_flutter_sdk/example - run: flutter build ios --config-only --no-codesign --release + working-directory: example + run: | + set -o pipefail + flutter build ios --config-only --no-codesign --release | tee config.log + if grep -q 'Running pod install' config.log; then + echo "CocoaPods ran in the Swift Package Manager build" + exit 1 + fi - name: Verify plugin is resolved via Swift Package Manager - working-directory: mparticle_flutter_sdk/example + working-directory: example run: | set -euo pipefail python3 - <<'PY' @@ -111,12 +168,14 @@ jobs: print("Swift Package Manager is enabled and mparticle_flutter_sdk is in FlutterGeneratedPluginSwiftPackage") PY - name: Build example iOS app - working-directory: mparticle_flutter_sdk/example/ios + working-directory: example/ios run: | - xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release -sdk iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -quiet + xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release -sdk iphoneos -derivedDataPath ../build/dd CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -quiet + - name: Check the SDK is linked once + run: .github/scripts/check-single-copy.sh example/build/dd/Build/Products/Release-iphoneos/Runner.app - name: Cleanup if: ${{ always() }} - working-directory: mparticle_flutter_sdk/example + working-directory: example run: | rm -rf build if command -v flutter &>/dev/null; then diff --git a/CHANGELOG.md b/CHANGELOG.md index eb20aea..dec989e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- iOS: log a warning at launch when the mParticle SDK is linked more than once, for example from both CocoaPods and Swift Package Manager. +- Document Swift Package Manager setup for iOS, including how to add kits, and add a CocoaPods to Swift Package Manager migration guide. + +### Changed + +- iOS: Rokt methods now return an error when the Rokt kit is not in the app, matching Android. Previously they reported success and did nothing. +- iOS: the CocoaPods spec reads the mParticle Apple SDK version from `Package.swift` (now `>= 9.3.1, < 10.0`), and the Swift package uses the same `mparticle-apple-sdk` package URL as the mParticle kits. +- The example app uses Swift Package Manager on iOS. + ## [2.2.1] - 2026-09-15 ### Fixed diff --git a/MIGRATING.md b/MIGRATING.md index 70fe374..b2424c5 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -6,6 +6,20 @@ This document describes upgrade steps for breaking changes in the mParticle Flut For changes in the underlying native iOS SDK (database migration, deprecated `UIApplicationDelegate` methods, removed `AppDelegateProxy`, regional routing / ATS, Rokt event class renames at the Swift/Objective-C level, etc.), refer to the [mParticle Apple SDK 9 migration guide](https://github.com/mParticle/mparticle-apple-sdk/blob/main/MIGRATING.md#migrating-from-versions--900). +## iOS: moving from CocoaPods to Swift Package Manager + +CocoaPods trunk becomes read-only on 2 December 2026, and Flutter uses Swift Package Manager by default from 3.44. The plugin supports both. With Swift Package Manager on, the plugin brings in the mParticle Apple SDK as a Swift package, so any mParticle or Rokt pod left in `ios/Podfile` links a second copy of the SDK. That app builds, but kits register with the wrong copy. + +1. Use Flutter 3.44 or later, or Flutter 3.41 or later with `flutter config --enable-swift-package-manager`. +2. Remove every mParticle and Rokt pod from `ios/Podfile`, for example `mParticle-Apple-SDK`, `mParticle-Rokt`, `RoktPaymentExtension` and `RoktSDKPlus`. + - If nothing else is left in the Podfile, run `pod deintegrate` in `ios/`, delete `Podfile` and `Podfile.lock`, and remove the `Pods-Runner` includes from `ios/Flutter/Debug.xcconfig` and `ios/Flutter/Release.xcconfig`. + - Otherwise, run `pod install`. +3. Add the kits as Swift packages to the Runner target, as described in the iOS section of the [README](README.md#iOS). +4. Run a Debug build once and check the console for `[mParticle] The mParticle SDK is loaded more than once`. If it appears, a pod still brings in the SDK. +5. Commit the Xcode project change and `ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved`. + +To stay on CocoaPods for now, turn Swift Package Manager off for your app by adding `enable-swift-package-manager: false` under `flutter: config:` in your app's `pubspec.yaml`. Flutter plans to remove this option in a future release. + ## Migrating from versions < 2.0.0 Version 2.0.0 wraps the mParticle Apple SDK 9 on iOS. No Dart source changes are required for existing `selectPlacements`, `purchaseFinalized`, or `MPRoktEvents` integrations, but the iOS build configuration must be updated. diff --git a/README.md b/README.md index 63b4809..0572254 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,50 @@ To install mParticle on an iOS platform: [1]: https://app.mparticle.com/apps -2. Install the SDK using CocoaPods: +2. Install the native SDKs. The plugin brings in the mParticle Apple SDK (9.3.1 or later). Add the kits your app uses, such as the Rokt kit for Rokt APIs and `RoktLayout`, with the dependency manager Flutter uses for your app. -```bash -$ # Update your Podfile to depend on 'mParticle-Apple-SDK' version 9.3.1 or later -$ pod install +#### Swift Package Manager + +Flutter uses Swift Package Manager by default from 3.44, and you can turn it on from 3.41. The plugin's Swift package brings in the mParticle Apple SDK, so don't add any mParticle or Rokt pods to `ios/Podfile`. + +Add each kit to the Runner target in Xcode: open `ios/Runner.xcworkspace`, choose **File > Add Package Dependencies…**, enter the package URL, and add the product to the **Runner** target. + +| Kit | Package URL | Product | +| --- | --- | --- | +| Rokt (required for Rokt APIs and `RoktLayout`) | `https://github.com/mparticle-integrations/mp-apple-integration-rokt` | `mParticle-Rokt` | +| Rokt payment extension (shoppable ads payments) | `https://github.com/ROKT/rokt-payment-extension-ios` | `RoktPaymentExtension` | + +Use a 9.x kit version. Other kits are listed in the [mParticle Apple SDK README](https://github.com/mParticle/mparticle-apple-sdk#currently-supported-kits); use the Swift package of their 9.x version. If you use `RoktSDKPlus`, it already includes the Rokt kit and the payment extension, so don't add those separately. Commit the Xcode project change and `ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved`. + +> **Warning:** Get each mParticle and Rokt SDK from only one dependency manager. If a pod and a Swift package both bring in the SDK, the app still builds, but it contains two copies and kits register with the wrong one. The plugin logs `[mParticle] The mParticle SDK is loaded more than once` at launch when this happens. See [Moving iOS from CocoaPods to Swift Package Manager](MIGRATING.md#ios-moving-from-cocoapods-to-swift-package-manager). + +Flutter versions before 3.41 can't build the plugin with Swift Package Manager. Keep it turned off there and use CocoaPods. + +#### CocoaPods (deprecated) + +CocoaPods trunk becomes read-only on 2 December 2026, so no new mParticle or Rokt pod versions can be published after that date. If Swift Package Manager is turned off for your app, add the kits to `ios/Podfile`: + +```ruby +platform :ios, '15.6' + +target 'Runner' do + use_frameworks! + + # Required for Rokt APIs and RoktLayout. + pod 'mParticle-Rokt' + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end ``` +Then run `pod install`. + +#### Troubleshooting + +- `Rokt is unavailable. Add the mParticle Rokt kit (mParticle-Rokt) to your iOS app.`: the Rokt kit isn't linked into the app. Add it as shown above. +- `[mParticle] The mParticle SDK is loaded more than once`: the SDK comes from both CocoaPods and Swift Package Manager. Remove the mParticle and Rokt pods from `ios/Podfile`. +- Package resolution fails, for example `no versions of '…' match the requirement`, or the build uses an old minimum iOS version: run `flutter clean`, then in Xcode choose **File > Packages > Reset Package Caches**. + The mParticle SDK is initialized by calling the `startWithOptions` method within the `application:didFinishLaunchingWithOptions:` delegate call. Preferably the location of the initialization method call should be one of the last statements in the `application:didFinishLaunchingWithOptions:`. The `startWithOptions` method requires an options argument containing your key and secret and an initial Identity request. > Note that you must initialize the SDK in the `application:didFinishLaunchingWithOptions:` method. Other parts of the SDK rely on the `UIApplicationDidBecomeActiveNotification` notification to function properly. Failing to start the SDK as indicated will impair it. Also, please do **not** use _GCD_'s `dispatch_async` to start the SDK. @@ -332,11 +369,7 @@ await mpInstance?.rokt.selectShoppableAds( ); ``` -To enable iOS payment flows for shoppable ads, add the payment extension pod in your app `ios/Podfile` and register it natively after `MParticle.sharedInstance().start(with:)`: - -```ruby -pod 'RoktPaymentExtension' -``` +To enable iOS payment flows for shoppable ads, add the Rokt payment extension to your app (with Swift Package Manager, the `RoktPaymentExtension` product from `https://github.com/ROKT/rokt-payment-extension-ios`; with CocoaPods, `pod 'RoktPaymentExtension'`) and register it natively after `MParticle.sharedInstance().start(with:)`: ```swift import RoktPaymentExtension diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 151026b..3925ddb 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -31,3 +31,7 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +# Swift Package Manager state. Versions float within the ranges in Runner.xcodeproj so CI builds what new apps get. +**/xcshareddata/swiftpm/ +Runner.xcodeproj/project.xcworkspace/ diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index ec97fc6..592ceee 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index c4855bf..592ceee 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index 4b9acb0..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,49 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '15.6' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - pod 'mParticle-Rokt' - pod 'RoktPaymentExtension' - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - target.build_configurations.each do |config| - # mParticle-Apple-SDK's podspec declares iOS 15.0 but re-exports - # RoktContracts, which requires 15.6. - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6' - end - end -end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4e0d26d..90ea3f9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,9 +8,11 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2E6DD6020906C0CBDF6ECB0F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B540045C113462A82FE59359 /* Pods_Runner.framework */; }; + 38E5282952F7201A9A1A88E1 /* mParticle-Rokt in Frameworks */ = {isa = PBXBuildFile; productRef = AEC6DF1C5490764CD7931E63 /* mParticle-Rokt */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 967BF53408CCCD4669D86239 /* RoktPaymentExtension in Frameworks */ = {isa = PBXBuildFile; productRef = A431758FCD128B4FAF673BF0 /* RoktPaymentExtension */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -30,14 +32,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0B59249B2C0A2D4979AD9DF8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 645DE86915C9172407A9AFFE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 752F1691AE8F9F6BBF6C035F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -46,7 +47,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B540045C113462A82FE59359 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,24 +54,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2E6DD6020906C0CBDF6ECB0F /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 38E5282952F7201A9A1A88E1 /* mParticle-Rokt in Frameworks */, + 967BF53408CCCD4669D86239 /* RoktPaymentExtension in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 8AFFCC4DD74311E1444F2AD6 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B540045C113462A82FE59359 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -86,8 +82,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - B6EA3C913DA1438894EFD834 /* Pods */, - 8AFFCC4DD74311E1444F2AD6 /* Frameworks */, ); sourceTree = ""; }; @@ -114,16 +108,6 @@ path = Runner; sourceTree = ""; }; - B6EA3C913DA1438894EFD834 /* Pods */ = { - isa = PBXGroup; - children = ( - 645DE86915C9172407A9AFFE /* Pods-Runner.debug.xcconfig */, - 0B59249B2C0A2D4979AD9DF8 /* Pods-Runner.release.xcconfig */, - 752F1691AE8F9F6BBF6C035F /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -131,20 +115,23 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - A870C5FF29BB6640750A453A /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 7C453701A596F4EF9D60D868 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + AEC6DF1C5490764CD7931E63 /* mParticle-Rokt */, + A431758FCD128B4FAF673BF0 /* RoktPaymentExtension */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -173,6 +160,11 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + D5005918A7F8195CBB9EFE7C /* XCRemoteSwiftPackageReference "mp-apple-integration-rokt" */, + E7D7B2DA8C5F5BC9848A18E9 /* XCRemoteSwiftPackageReference "rokt-payment-extension-ios" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -213,23 +205,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 7C453701A596F4EF9D60D868 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -245,28 +220,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - A870C5FF29BB6640750A453A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -549,6 +502,49 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D5005918A7F8195CBB9EFE7C /* XCRemoteSwiftPackageReference "mp-apple-integration-rokt" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mparticle-integrations/mp-apple-integration-rokt"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.7.4; + }; + }; + E7D7B2DA8C5F5BC9848A18E9 /* XCRemoteSwiftPackageReference "rokt-payment-extension-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ROKT/rokt-payment-extension-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.5.5; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; + A431758FCD128B4FAF673BF0 /* RoktPaymentExtension */ = { + isa = XCSwiftPackageProductDependency; + package = E7D7B2DA8C5F5BC9848A18E9 /* XCRemoteSwiftPackageReference "rokt-payment-extension-ios" */; + productName = RoktPaymentExtension; + }; + AEC6DF1C5490764CD7931E63 /* mParticle-Rokt */ = { + isa = XCSwiftPackageProductDependency; + package = D5005918A7F8195CBB9EFE7C /* XCRemoteSwiftPackageReference "mp-apple-integration-rokt" */; + productName = "mParticle-Rokt"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c12df5..5db441f 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + '.' } s.source_files = 'mparticle_flutter_sdk/Sources/mparticle_flutter_sdk/**/*.swift' s.dependency 'Flutter' + # The Swift package manifest holds the one copy of the core SDK version; read it so the two cannot drift. + manifest = File.read(File.join(__dir__, 'mparticle_flutter_sdk', 'Package.swift')) + core_version = manifest[/mparticle-apple-sdk",\s*\.upToNextMajor\(from: "([^"]+)"\)/, 1] or + raise 'mparticle_flutter_sdk.podspec: mparticle-apple-sdk version not found in Package.swift' # SDK 9.3 umbrella pod pulls required transitive dependencies. - s.dependency 'mParticle-Apple-SDK', '~> 9.3' + s.dependency 'mParticle-Apple-SDK', ">= #{core_version}", "< #{core_version.to_i + 1}.0" s.platform = :ios, '15.6' # Flutter.framework does not contain a i386 slice. diff --git a/ios/mparticle_flutter_sdk/Package.swift b/ios/mparticle_flutter_sdk/Package.swift index dc3d326..7da1b04 100644 --- a/ios/mparticle_flutter_sdk/Package.swift +++ b/ios/mparticle_flutter_sdk/Package.swift @@ -13,8 +13,9 @@ let package = Package( ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework"), + // mparticle_flutter_sdk.podspec reads this version: keep the URL and the upToNextMajor(from:) form. .package( - url: "https://github.com/mParticle/mparticle-apple-sdk.git", + url: "https://github.com/mParticle/mparticle-apple-sdk", .upToNextMajor(from: "9.3.1") ), .package( diff --git a/ios/mparticle_flutter_sdk/Sources/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.swift b/ios/mparticle_flutter_sdk/Sources/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.swift index 0518cfd..e15103a 100644 --- a/ios/mparticle_flutter_sdk/Sources/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.swift +++ b/ios/mparticle_flutter_sdk/Sources/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.swift @@ -6,6 +6,7 @@ import RoktContracts public class MparticleFlutterSdkPlugin: NSObject, FlutterPlugin { fileprivate static let VIEW_CALL_DELEGATE = "rokt_sdk.rokt.com/rokt_layout" + private static let ROKT_METHODS: Set = ["roktSubscribeToEvents", "roktSelectPlacements", "roktSelectShoppableAds", "roktPurchaseFinalized"] let roktLayoutFactory: RoktLayoutFactory let channel: FlutterMethodChannel let registrar: FlutterPluginRegistrar @@ -19,12 +20,48 @@ public class MparticleFlutterSdkPlugin: NSObject, FlutterPlugin { } public static func register(with registrar: FlutterPluginRegistrar) { + _ = warnIfSDKLoadedTwice let instance = MparticleFlutterSdkPlugin(messenger: registrar.messenger(), registrar: registrar) registrar.addMethodCallDelegate(instance, channel: instance.channel) registrar.register(instance.roktLayoutFactory, withId: MparticleFlutterSdkPlugin.VIEW_CALL_DELEGATE) } + // Getting the SDK from both CocoaPods and Swift Package Manager links a second copy. That builds + // cleanly, then kits register with one copy while the app talks to the other. Runs once. + private static let warnIfSDKLoadedTwice: Void = { + let duplicates = imageCounts(forClassNames: ["MParticle", "RoktEmbeddedView", "MPKitRokt"]) + .filter { $0.value > 1 } + .map { "\($0.key) x\($0.value)" } + .sorted() + if !duplicates.isEmpty { + NSLog("[mParticle] The mParticle SDK is loaded more than once (%@). This happens when it comes from both CocoaPods and Swift Package Manager: remove the mParticle and Rokt pods from your Podfile. See the iOS section of the mparticle_flutter_sdk README.", duplicates.joined(separator: ", ")) + } + }() + + private static func imageCounts(forClassNames names: [String]) -> [String: Int] { + var counts = Dictionary(uniqueKeysWithValues: names.map { ($0, 0) }) + let bundlePath = Bundle.main.bundlePath + var imageCount: UInt32 = 0 + let images = objc_copyImageNames(&imageCount) + defer { free(images) } + for i in 0..