diff --git a/.github/scripts/check-version-drift.sh b/.github/scripts/check-version-drift.sh new file mode 100755 index 0000000..e3777f5 --- /dev/null +++ b/.github/scripts/check-version-drift.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Verify OptimizelySwiftSDK version is the same in both podspec and Package.swift. +# Exits non-zero if versions diverge (FR-014). + +set -euo pipefail + +PODSPEC="ios/optimizely_flutter_sdk.podspec" +PACKAGE_SWIFT="ios/optimizely_flutter_sdk/Package.swift" + +PODSPEC_VER=$(grep "OptimizelySwiftSDK" "$PODSPEC" | grep -oE "'[0-9]+\.[0-9]+\.[0-9]+'" | tr -d "'") +SPM_VER=$(grep 'exact' "$PACKAGE_SWIFT" | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') + +if [ -z "$PODSPEC_VER" ] || [ -z "$SPM_VER" ]; then + echo "FAIL: Could not extract version from one or both files" + echo " podspec: ${PODSPEC_VER:-}" + echo " Package.swift: ${SPM_VER:-}" + exit 1 +fi + +if [ "$PODSPEC_VER" != "$SPM_VER" ]; then + echo "FAIL: OptimizelySwiftSDK version mismatch" + echo " podspec: $PODSPEC_VER" + echo " Package.swift: $SPM_VER" + exit 1 +fi + +echo "OK: OptimizelySwiftSDK version matches ($PODSPEC_VER)" diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 4aa2445..e8399b2 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -17,14 +17,23 @@ on: # default: "master" jobs: + version_drift_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check OptimizelySwiftSDK version parity + run: bash .github/scripts/check-version-drift.sh + unit_test_coverage: runs-on: macos-latest - + needs: version_drift_check + steps: - uses: actions/checkout@v3 - name: Install Flutter uses: subosito/flutter-action@v2 with: + flutter-version: '3.44.0' channel: 'stable' architecture: x64 - name: Install app dependencies @@ -127,32 +136,32 @@ jobs: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: - channel: 'stable' # or: 'beta' or 'master' + flutter-version: '3.44.0' + channel: 'stable' cache: true - cache-key: 'flutter-:os:-:arch:-:channel:-:version:-:hash:' # optional, change this to force refresh cache - cache-path: ${{ runner.tool_cache }}/flutter # optional, change this to specify the cache path - architecture: x64 # optional, x64 or arm64 - - run: flutter --version - + cache-key: 'flutter-:os:-:arch:-:channel:-:version:-:hash:' + cache-path: ${{ runner.tool_cache }}/flutter + architecture: x64 - name: Setup java uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '11' - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.0.5' - run: flutter pub get - run: flutter test build_test_ios: runs-on: macos-latest - + needs: version_drift_check + steps: - uses: actions/checkout@v3 - uses: subosito/flutter-action@v2 with: + flutter-version: '3.44.0' channel: 'stable' architecture: x64 + - name: Enable Swift Package Manager + run: flutter config --enable-swift-package-manager - run: flutter pub get - run: flutter test diff --git a/.specify/feature.json b/.specify/feature.json new file mode 100644 index 0000000..7b91d9a --- /dev/null +++ b/.specify/feature.json @@ -0,0 +1,3 @@ +{ + "feature_directory": "specs/001-cocoapods-to-spm-migration" +} diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 1a0c996..b5f391e 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -141,7 +141,9 @@ The source-of-truth for current versions is always the build files themselves — not this document: - **iOS**: `OptimizelySwiftSDK` version in - `ios/optimizely_flutter_sdk.podspec`. + `ios/optimizely_flutter_sdk.podspec` (CocoaPods) and + `ios/optimizely_flutter_sdk/Package.swift` (SPM). Both files MUST + declare the same version. - **Android**: `com.optimizely.ab:android-sdk` version in `android/build.gradle`. @@ -343,4 +345,4 @@ All code changes MUST comply with the principles defined above. - Use `CLAUDE.md` for runtime development guidance that supplements (but does not override) this constitution. -**Version**: 1.1.0 | **Ratified**: 2022-06-07 | **Last Amended**: 2026-08-20 +**Version**: 1.1.1 | **Ratified**: 2022-06-07 | **Last Amended**: 2026-08-22 diff --git a/CLAUDE.md b/CLAUDE.md index 7d68689..4e2b952 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,6 @@ Optimizely Flutter SDK - Cross-platform plugin wrapping native Optimizely SDKs ( ```bash # Setup flutter pub get -cd ios && pod install # iOS dependencies # Testing flutter test # All tests @@ -74,11 +73,17 @@ Native Optimizely SDKs (check podspec/build.gradle for current versions) ## Version Management -**Three locations must stay synchronized:** +**Three locations must stay synchronized (SDK version):** 1. `pubspec.yaml` → `version: X.Y.Z` 2. `lib/package_info.dart` → `version = 'X.Y.Z'` 3. `README.md` → Installation example `^X.Y.Z` +**Two locations must stay synchronized (native iOS SDK version):** +1. `ios/optimizely_flutter_sdk.podspec` → `s.dependency 'OptimizelySwiftSDK', 'A.B.C'` +2. `ios/optimizely_flutter_sdk/Package.swift` → `.package(url: "...swift-sdk.git", exact: "A.B.C")` + +A CI check (`.github/scripts/check-version-drift.sh`) enforces parity between these two files. + ## Release Workflow ### With Claude Code (Recommended) @@ -187,7 +192,7 @@ Follow [Angular guidelines](https://github.com/angular/angular/blob/master/CONTR **iOS:** - Minimum: iOS 10.0 - Swift: 5.0 -- Native SDK: OptimizelySwiftSDK (see ios/optimizely_flutter_sdk.podspec for current version) +- Native SDK: OptimizelySwiftSDK (see ios/optimizely_flutter_sdk.podspec and ios/optimizely_flutter_sdk/Package.swift for current version) ## Key Implementation Files @@ -203,8 +208,9 @@ Follow [Angular guidelines](https://github.com/angular/angular/blob/master/CONTR - `android/build.gradle` - Dependencies & SDK versions **iOS Layer:** -- `ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift` - Plugin implementation (786 LOC) -- `ios/Classes/OptimizelyFlutterLogger.swift` - Logger bridge (main-thread dispatch) +- `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/SwiftOptimizelyFlutterSdkPlugin.swift` - Plugin implementation (786 LOC) +- `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterLogger.swift` - Logger bridge (main-thread dispatch) +- `ios/optimizely_flutter_sdk/Package.swift` - SPM dependencies - `ios/optimizely_flutter_sdk.podspec` - CocoaPods dependencies diff --git a/README.md b/README.md index 744388c..bc688b0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ See the [pubspec.yaml](https://github.com/optimizely/optimizely-flutter-sdk/blob On the Android platform, the SDK requires a minimum SDK version of 21 or higher and compile SDK version of 32. -On the iOS platform, the SDK requires a minimum version of 10.0. +On the iOS platform, the SDK requires a minimum version of 10.0. The SDK supports both Swift Package Manager (SPM) and CocoaPods for iOS dependency resolution. Flutter 3.44+ uses SPM by default; older versions use CocoaPods. Other Flutter platforms are not currently supported by this SDK. diff --git a/analysis_options.yaml b/analysis_options.yaml index 67d44e7..2a7f74d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,13 @@ analyzer: exclude: - example/** - test/** + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 7a7f987..ecbb615 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -27,6 +27,8 @@ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* +**/swiftpm/ + # Exceptions to above rules. !default.mode1v3 !default.mode2v3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 56af059..7a54079 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,11 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 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 */; }; 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 */; }; - E567413E4441EFD8E182B8E3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFCE984DC7CBCFB140E88864 /* Pods_Runner.framework */; }; + E7471FE6544841B0C836D129 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC1798EC3215B94C8A840B51 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -30,15 +31,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0199F036B95C7315646FF6B8 /* 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 = ""; }; 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 = ""; }; + 2407820457C3954D6394333D /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3D86A8B550CB0FBA7A8F2A03 /* 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 = ""; }; 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 = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* optimizely_flutter_sdk */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = optimizely_flutter_sdk; path = ../../ios/optimizely_flutter_sdk; 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 = ""; }; - 8E60C66DA76D705E5A9DCACA /* 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 = ""; }; 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 = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -46,7 +48,9 @@ 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 = ""; }; - CFCE984DC7CBCFB140E88864 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ACF0C93900CB1B99B5E9C0CE /* 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 = ""; }; + D74274CA5486F97165BE309E /* 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 = ""; }; + DC1798EC3215B94C8A840B51 /* 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,7 +58,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E567413E4441EFD8E182B8E3 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + E7471FE6544841B0C836D129 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -64,17 +69,17 @@ 3B54408533A9D1A272B285B5 /* Pods */ = { isa = PBXGroup; children = ( - 0199F036B95C7315646FF6B8 /* Pods-Runner.debug.xcconfig */, - 8E60C66DA76D705E5A9DCACA /* Pods-Runner.release.xcconfig */, - 3D86A8B550CB0FBA7A8F2A03 /* Pods-Runner.profile.xcconfig */, + D74274CA5486F97165BE309E /* Pods-Runner.debug.xcconfig */, + 2407820457C3954D6394333D /* Pods-Runner.release.xcconfig */, + ACF0C93900CB1B99B5E9C0CE /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; - 812DB522D2DCFB756795F343 /* Frameworks */ = { + 5922E7F1B4E66D8046FD9D1E /* Frameworks */ = { isa = PBXGroup; children = ( - CFCE984DC7CBCFB140E88864 /* Pods_Runner.framework */, + DC1798EC3215B94C8A840B51 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -82,6 +87,9 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* optimizely_flutter_sdk */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -97,7 +105,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 3B54408533A9D1A272B285B5 /* Pods */, - 812DB522D2DCFB756795F343 /* Frameworks */, + 5922E7F1B4E66D8046FD9D1E /* Frameworks */, ); sourceTree = ""; }; @@ -131,20 +139,22 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - CC1B24FA947A559E173BFFB3 /* [CP] Check Pods Manifest.lock */, + 73C63109C125F11478860EB1 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 8AD3511808907367925A4750 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -173,6 +183,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -213,21 +226,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 8AD3511808907367925A4750 /* [CP] Embed Pods Frameworks */ = { + 73C63109C125F11478860EB1 /* [CP] Check Pods Manifest.lock */ = { 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"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + 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; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -245,28 +263,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - CC1B24FA947A559E173BFFB3 /* [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 */ @@ -552,6 +548,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* 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 @@ + + + + + + + + + + "Apache License, Version 2.0", :file => "../LICENSE" } s.author = { "Optimizely" => "support@optimizely.com" } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/**/*' s.dependency 'Flutter' s.dependency 'OptimizelySwiftSDK', '5.4.2' s.platform = :ios, '10.0' diff --git a/ios/optimizely_flutter_sdk/Package.swift b/ios/optimizely_flutter_sdk/Package.swift new file mode 100644 index 0000000..aa92218 --- /dev/null +++ b/ios/optimizely_flutter_sdk/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +// ObjC files (OptimizelyFlutterSdkPlugin.m/.h) are excluded from the SPM +// target because SPM does not support mixed Swift/ObjC in a single target. +// They remain on disk for CocoaPods builds, which still use them. + +import PackageDescription + +let package = Package( + name: "optimizely_flutter_sdk", + platforms: [.iOS("10.0")], + products: [ + .library(name: "optimizely-flutter-sdk", targets: ["optimizely_flutter_sdk"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + .package(url: "https://github.com/optimizely/swift-sdk.git", exact: "5.4.2") + ], + targets: [ + .target( + name: "optimizely_flutter_sdk", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "Optimizely", package: "swift-sdk") + ], + exclude: ["OptimizelyFlutterSdkPlugin.m", "include"] + ) + ] +) diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Constants.swift similarity index 100% rename from ios/Classes/HelperClasses/Constants.swift rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Constants.swift diff --git a/ios/Classes/HelperClasses/OptimizelyConfig+Extension.swift b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyConfig+Extension.swift similarity index 100% rename from ios/Classes/HelperClasses/OptimizelyConfig+Extension.swift rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyConfig+Extension.swift diff --git a/ios/Classes/OptimizelyFlutterLogger.swift b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterLogger.swift similarity index 100% rename from ios/Classes/OptimizelyFlutterLogger.swift rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterLogger.swift diff --git a/ios/Classes/OptimizelyFlutterSdkPlugin.m b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.m similarity index 96% rename from ios/Classes/OptimizelyFlutterSdkPlugin.m rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.m index 3013972..7bb833f 100644 --- a/ios/Classes/OptimizelyFlutterSdkPlugin.m +++ b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.m @@ -14,7 +14,7 @@ * limitations under the License. * ***************************************************************************/ -#import "OptimizelyFlutterSdkPlugin.h" +#import "./include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h" #if __has_include() #import #else diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/SwiftOptimizelyFlutterSdkPlugin.swift similarity index 100% rename from ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/SwiftOptimizelyFlutterSdkPlugin.swift diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Utils.swift similarity index 100% rename from ios/Classes/HelperClasses/Utils.swift rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Utils.swift diff --git a/ios/Classes/OptimizelyFlutterSdkPlugin.h b/ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h similarity index 100% rename from ios/Classes/OptimizelyFlutterSdkPlugin.h rename to ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h diff --git a/pubspec.yaml b/pubspec.yaml index 7aa3fb7..d4c9ea2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ flutter: package: com.optimizely.optimizely_flutter_sdk pluginClass: OptimizelyFlutterSdkPlugin ios: - pluginClass: OptimizelyFlutterSdkPlugin + pluginClass: SwiftOptimizelyFlutterSdkPlugin macos: pluginClass: OptimizelyFlutterSdkPlugin windows: diff --git a/specs/001-cocoapods-to-spm-migration/checklists/requirements.md b/specs/001-cocoapods-to-spm-migration/checklists/requirements.md new file mode 100644 index 0000000..e00fbcf --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/checklists/requirements.md @@ -0,0 +1,38 @@ +# Specification Quality Checklist: Add SPM Support with CocoaPods Backward Compatibility + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-08-20 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass validation. Spec is ready for `/speckit-plan`. +- Spec revised on 2026-08-20: changed from "clean cut" to dual CocoaPods + SPM support. +- FR-010 clarified: SDK keeps iOS 10.0, test app keeps iOS 13.0. +- The spec references both the SDK repo and the test app repo as in-scope. +- Re-validated after spec revision (dual support) on 2026-08-20. diff --git a/specs/001-cocoapods-to-spm-migration/data-model.md b/specs/001-cocoapods-to-spm-migration/data-model.md new file mode 100644 index 0000000..60b9462 --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/data-model.md @@ -0,0 +1,81 @@ +# Data Model: Add SPM Support with CocoaPods Backward Compatibility + +**Date**: 2026-08-20 (Revised) | **Feature**: [spec.md](spec.md) + +This is an infrastructure change — no new data entities, attributes, or state transitions are introduced. The SDK's existing data models (`lib/src/data_objects/`) are unchanged. + +## File Structure Changes + +The migration changes the physical layout of iOS source files. Both CocoaPods and SPM reference the same files in the new location. + +### Before + +| Path | Role | +|------|------| +| `ios/optimizely_flutter_sdk.podspec` | Dependency manifest (CocoaPods) | +| `ios/Classes/*.swift` | Plugin Swift sources | +| `ios/Classes/HelperClasses/*.swift` | Helper Swift sources | +| `ios/Classes/*.h`, `*.m` | ObjC bridging for plugin registration | +| `ios/Assets/` | Empty assets directory | + +### After + +| Path | Role | +|------|------| +| `ios/optimizely_flutter_sdk.podspec` | Dependency manifest (CocoaPods) — **updated `source_files` path** | +| `ios/optimizely_flutter_sdk/Package.swift` | Dependency manifest (SPM) — **NEW** | +| `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/*.swift` | All plugin Swift sources (flattened) | +| `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.m` | ObjC bridge implementation | +| `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h` | ObjC public header | + +### Removed + +| Path | Reason | +|------|--------| +| `ios/Classes/` | Sources moved to SPM layout | +| `ios/Assets/` | Empty, not needed | + +### Retained (Updated) + +| Path | Change | +|------|--------| +| `ios/optimizely_flutter_sdk.podspec` | `source_files` path updated to `'optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/**/*'` | + +## Configuration Changes + +### SDK (`pubspec.yaml`) + +No changes needed — Flutter detects SPM support by the presence of Package.swift and falls back to CocoaPods if SPM is not available. + +### Example App (`example/`) + +No changes — keeps existing Podfile for CocoaPods backward compatibility. Flutter tooling selects the appropriate mechanism. + +### Test App + +The test app needs CI workflow updates and SPM configuration added to its iOS project. The existing Podfile is kept for CocoaPods fallback. CI workflows (`ios.yml`) are updated for SPM-compatible build steps. + +## Dependency Graph + +``` +SPM Path: pubspec.yaml → Package.swift → SPM → OptimizelySwiftSDK (SPM package) +CocoaPods: pubspec.yaml → podspec → CocoaPods → OptimizelySwiftSDK (pod) +``` + +Both paths coexist. Flutter selects which path to use based on: +- Flutter version (3.44+ defaults to SPM) +- `--enable-swift-package-manager` / `--no-enable-swift-package-manager` flag +- Presence of Package.swift in the plugin + +The Dart layer, MethodChannel bridge, and Android layer are completely unaffected. + +## Version Synchronization + +Two files now declare the OptimizelySwiftSDK version: + +| File | Format | Current | +|------|--------|---------| +| `ios/optimizely_flutter_sdk.podspec` | `s.dependency 'OptimizelySwiftSDK', '5.4.2'` | 5.4.2 | +| `ios/optimizely_flutter_sdk/Package.swift` | `.package(url: "...", exact: "5.4.2")` | 5.4.2 (new) | + +Both MUST be updated simultaneously when bumping native SDK versions (Constitution Principle VIII). diff --git a/specs/001-cocoapods-to-spm-migration/plan.md b/specs/001-cocoapods-to-spm-migration/plan.md new file mode 100644 index 0000000..36edbdd --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/plan.md @@ -0,0 +1,136 @@ +# Implementation Plan: Add SPM Support with CocoaPods Backward Compatibility + +**Branch**: `001-cocoapods-to-spm-migration` | **Date**: 2026-08-20 (Revised) | **Spec**: [spec.md](spec.md) + +**Input**: Feature specification from `specs/001-cocoapods-to-spm-migration/spec.md` + +## Summary + +Add Swift Package Manager support to the Optimizely Flutter SDK's iOS plugin while retaining CocoaPods backward compatibility. Source files move from `ios/Classes/` to `ios/optimizely-flutter-sdk/Classes/`. The hyphenated directory name is required because SPM normalizes package identity by replacing underscores with hyphens (SE-0292); a symlink `ios/optimizely_flutter_sdk` → `ios/optimizely-flutter-sdk/` lets Flutter discover the Package.swift. Both the new Package.swift (using `path: "Classes"` override) and the existing podspec (with updated source paths) reference the same files. ObjC files are excluded from the SPM target due to mixed-language limitations but remain for CocoaPods. Flutter selects the dependency mechanism based on version and configuration. The Dart and Android layers are unaffected. + +## Technical Context + +**Language/Version**: Dart >=2.16.2 <4.0.0, Swift 5.0+, Java/Kotlin 2.1.0 + +**Primary Dependencies**: Flutter SDK (stable), OptimizelySwiftSDK 5.4.2 (via SPM AND CocoaPods), android-sdk (unchanged via Gradle) + +**Storage**: N/A + +**Testing**: `flutter test` (Dart unit tests), `flutter test integration_test` (e2e via test app) + +**Target Platform**: iOS 10.0+ (SDK), iOS 13.0 (test app), Android API 21+ (unchanged) + +**Project Type**: Flutter plugin (cross-platform library wrapping native SDKs) + +**Performance Goals**: N/A — infrastructure change, no behavioral changes + +**Constraints**: Must maintain backward compatibility with CocoaPods. iOS minimum stays at 10.0. All existing tests must pass. Both dependency mechanisms must pin the same OptimizelySwiftSDK version. + +**Scale/Scope**: 2 repositories (SDK + test app), ~7 Swift/ObjC source files to move, 1 podspec to update, 1 Package.swift to create + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. Bridge Pattern Integrity | PASS | No changes to the three-layer architecture. Only the dependency resolution mechanism changes. | +| II. Response Object Pattern | PASS | No API changes — all methods continue returning BaseResponse derivatives. | +| III. Platform Parity | PASS | Android layer is completely unaffected. iOS functionality is preserved. | +| IV. Type Safety Across Bridge | PASS | No changes to type encoding or `Utils.convertToTypedMap()`. | +| V. Thread Safety | PASS | No changes to main-thread dispatch patterns. | +| VI. Multi-Instance State Isolation | PASS | No changes to state management. | +| VII. Version Synchronisation | PASS | No version bump for this change (infra). | +| VIII. Native SDK Version Pinning | PASS | Version pinning preserved in BOTH files: `.exact("5.4.2")` in Package.swift AND `'5.4.2'` in podspec. Source-of-truth files now include both. | +| IX. CMAB & Async Decide | PASS | No changes to CMAB functionality. | +| X. ODP Integration | PASS | No changes to ODP functionality. | +| XI. Event Batching Configuration | PASS | No changes to event batching. | +| XII. Logger Bridge Architecture | PASS | Logger channel and bridge files are moved but unchanged. | + +**Gate Result**: ALL PASS — no violations. + +**Post-Design Re-Check**: ALL PASS — dual support adds a version synchronization requirement across two files, which strengthens Principle VIII rather than violating it. FR-014 (CI version drift check) provides automated enforcement. + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-cocoapods-to-spm-migration/ +├── plan.md # This file +├── research.md # Phase 0 output — dual support research +├── data-model.md # Phase 1 output — file structure changes +├── quickstart.md # Phase 1 output — validation guide +└── tasks.md # Phase 2 output (created by /speckit-tasks) +``` + +### Source Code (repository root) + +```text +# SDK repo — iOS layer changes +ios/ +├── optimizely_flutter_sdk.podspec # UPDATED: source_files → 'optimizely-flutter-sdk/Classes/**/*' +├── optimizely-flutter-sdk/ # NEW: SPM package directory (hyphens — matches SPM identity) +│ ├── Package.swift # NEW: SPM manifest (path: "Classes" override) +│ └── Classes/ # Source files (shared by SPM and CocoaPods) +│ ├── include/ +│ │ └── optimizely_flutter_sdk/ +│ │ └── OptimizelyFlutterSdkPlugin.h # MOVED (excluded from SPM, used by CocoaPods) +│ ├── OptimizelyFlutterSdkPlugin.m # MOVED (excluded from SPM, used by CocoaPods) +│ ├── SwiftOptimizelyFlutterSdkPlugin.swift # MOVED from Classes/ +│ ├── OptimizelyFlutterLogger.swift # MOVED from Classes/ +│ ├── Constants.swift # MOVED from Classes/HelperClasses/ +│ ├── OptimizelyConfig+Extension.swift # MOVED from Classes/HelperClasses/ +│ └── Utils.swift # MOVED from Classes/HelperClasses/ +├── optimizely_flutter_sdk -> optimizely-flutter-sdk/ # SYMLINK (underscores — Flutter discovery) +├── Classes/ # REMOVED (files moved) +└── Assets/ # REMOVED (empty) + +# pubspec.yaml — pluginClass changed to SwiftOptimizelyFlutterSdkPlugin + +# Example app — NO CHANGES (keeps Podfile for CocoaPods backward compat) +example/ios/ +├── Podfile # KEPT +├── Podfile.lock # KEPT (regenerated on next pod install) +└── Pods/ # KEPT + +# Test app — CI pinned to Flutter 3.16.0 for CocoaPods validation +optimizely-flutter-testapp/.github/workflows/ +└── ios.yml # UPDATED: Flutter 3.16.0, SPM step removed + +# SDK CI +.github/workflows/ +└── flutter.yml # UPDATED: Flutter 3.44.0, SPM enabled, version drift check (FR-014) + +# Dart layer — NO CHANGES +# Android layer — NO CHANGES +``` + +**Structure Decision**: Source files move from `ios/Classes/` to `ios/optimizely-flutter-sdk/Classes/`. The hyphenated directory name is required by SPM identity normalization (SE-0292). A symlink `ios/optimizely_flutter_sdk` → `ios/optimizely-flutter-sdk/` allows Flutter's plugin discovery to find the Package.swift. Package.swift uses `path: "Classes"` to override the default `Sources//` convention. ObjC files are excluded from the SPM target (mixed-language limitation) but kept for CocoaPods. Both dependency mechanisms share the same physical Swift source files. + +## Complexity Tracking + +No constitution violations — this section is empty. + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| (none) | — | — | + +## Research Artifacts + +See [research.md](research.md) for detailed findings on: +- Flutter plugin dual SPM + CocoaPods support (official recommendation) +- Source file layout for dual support +- ObjC bridging file handling in SPM +- Podspec path updates +- OptimizelySwiftSDK SPM compatibility +- Version synchronization requirements + +## Key Risks + +1. **SPM version tag**: Verified — git tag `5.4.2` exists on the `swift-sdk` repo (no `v` prefix). +2. **SPM identity normalization**: SPM 5.6+ normalizes underscores to hyphens in package identity (SE-0292). **Mitigated**: directory uses hyphens (`optimizely-flutter-sdk/`), symlink uses underscores (`optimizely_flutter_sdk`) for Flutter discovery. +3. **Mixed Swift/ObjC**: SPM cannot compile Swift and ObjC in the same target. **Mitigated**: ObjC files excluded from SPM target via `exclude`, kept for CocoaPods. +4. **Podspec source path**: After moving files, the glob `'optimizely-flutter-sdk/Classes/**/*'` includes both Swift and ObjC files for CocoaPods. +5. **Version drift**: Two files now declare the native SDK version. **Mitigated by FR-014**: a CI validation check MUST fail the build if versions diverge. +6. **Mixed CocoaPods/SPM state**: Resolved — Flutter's tooling handles resolution automatically. No SDK-level detection or intervention needed. diff --git a/specs/001-cocoapods-to-spm-migration/quickstart.md b/specs/001-cocoapods-to-spm-migration/quickstart.md new file mode 100644 index 0000000..9f7bd7c --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/quickstart.md @@ -0,0 +1,145 @@ +# Quickstart Validation Guide: Add SPM Support with CocoaPods Backward Compatibility + +**Date**: 2026-08-20 (Revised) | **Feature**: [spec.md](spec.md) + +## Prerequisites + +- Flutter SDK (stable channel) +- Xcode 15+ with iOS simulators +- CocoaPods installed (for backward compatibility validation) +- Access to `optimizely-flutter-testapp` repo + +## Validation Scenario 1: SDK Plugin Builds via SPM + +**Goal**: Verify the SDK's Package.swift is correctly structured and OptimizelySwiftSDK resolves via SPM. + +```bash +# From SDK repo root (with Flutter 3.44+ or SPM enabled) +flutter config --enable-swift-package-manager +flutter clean +flutter pub get +cd example +flutter build ios --simulator --no-codesign +``` + +**Expected**: Build succeeds. Console shows SPM resolving `swift-sdk` package. No CocoaPods warnings. + +## Validation Scenario 2: SDK Plugin Builds via CocoaPods + +**Goal**: Verify the podspec still works with updated source file paths. + +```bash +# From SDK repo root (with CocoaPods path) +flutter config --no-enable-swift-package-manager +flutter clean +flutter pub get +cd example +flutter build ios --simulator --no-codesign +``` + +**Expected**: Build succeeds via CocoaPods. `pod install` finds source files at the new path. No missing file errors. + +## Validation Scenario 3: Podspec Lint + +**Goal**: Verify the podspec is valid after source path changes. + +```bash +cd ios +pod lib lint optimizely_flutter_sdk.podspec --allow-warnings +``` + +**Expected**: Lint passes. Source files found at updated paths. + +## Validation Scenario 4: Unit Tests Pass + +**Goal**: Verify no test regressions from the file restructuring. + +```bash +# From SDK repo root +flutter test +``` + +**Expected**: All existing tests pass with zero failures. + +## Validation Scenario 5: Lint Clean + +**Goal**: Verify no analysis issues introduced. + +```bash +flutter analyze +``` + +**Expected**: No issues found. + +## Validation Scenario 6: Test App Builds via SPM + +**Goal**: Verify the test app resolves the SDK via SPM and integration tests pass. + +```bash +# From optimizely-flutter-testapp repo (with SPM enabled) +flutter config --enable-swift-package-manager +flutter clean +flutter pub get +flutter build ios --simulator --no-codesign +``` + +**Expected**: Build succeeds with SPM dependency resolution. + +## Validation Scenario 7: Test App Integration Tests + +**Goal**: Verify end-to-end functionality on iOS simulator. + +```bash +# From optimizely-flutter-testapp repo +flutter test integration_test -d +``` + +**Expected**: All e2e test cases pass with zero failures. + +## Validation Scenario 8: Version Consistency + +**Goal**: Verify both dependency files declare the same OptimizelySwiftSDK version. + +```bash +# From SDK repo root +grep "OptimizelySwiftSDK" ios/optimizely_flutter_sdk.podspec +grep "exact:" ios/optimizely_flutter_sdk/Package.swift +``` + +**Expected**: Both show version `5.4.2` (or whatever the current pinned version is). + +## Validation Scenario 9: CI Version Drift Check (FR-014) + +**Goal**: Verify the CI validation step detects version mismatches between Package.swift and podspec. + +```bash +# Extract versions and compare +PODSPEC_VER=$(grep "OptimizelySwiftSDK" ios/optimizely_flutter_sdk.podspec | grep -oE "'[0-9]+\.[0-9]+\.[0-9]+'" | tr -d "'") +SPM_VER=$(grep 'exact:' ios/optimizely_flutter_sdk/Package.swift | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') +[ "$PODSPEC_VER" = "$SPM_VER" ] && echo "PASS: versions match ($PODSPEC_VER)" || echo "FAIL: podspec=$PODSPEC_VER spm=$SPM_VER" +``` + +**Expected**: Versions match. If intentionally mismatched for testing, the check should fail. + +## Validation Scenario 10: CI Pipeline + +**Goal**: Verify all five CI jobs pass. + +Push the branch and create a PR against `master`. Monitor: + +1. `unit_test_coverage` — Dart tests + coverage +2. `build_test_android` — Android build (unaffected) +3. `build_test_ios` — iOS build +4. `integration_android_tests` — Android e2e (unaffected) +5. `integration_ios_tests` — iOS e2e + +**Expected**: All five jobs pass green. + +## Troubleshooting + +- **SPM resolution fails**: Verify the `swift-sdk` git tag for version 5.4.2 exists. Check Package.swift URL. +- **CocoaPods can't find source files**: Verify podspec `source_files` path matches the new directory layout. +- **ObjC header not found**: Check that the `.h` file is in `include/optimizely_flutter_sdk/` and import path in `.m` is updated. +- **Mixed ObjC/Swift errors in SPM**: Ensure `cSettings: [.headerSearchPath("include/optimizely_flutter_sdk")]` is in Package.swift target. +- **Xcode cache issues**: Run `flutter clean`, delete `~/Library/Developer/Xcode/DerivedData/`, rebuild. +- **Wrong dependency path used**: Use `flutter config --enable-swift-package-manager` or `--no-enable-swift-package-manager` to force a specific path for testing. diff --git a/specs/001-cocoapods-to-spm-migration/research.md b/specs/001-cocoapods-to-spm-migration/research.md new file mode 100644 index 0000000..5167d92 --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/research.md @@ -0,0 +1,159 @@ +# Research: Add SPM Support with CocoaPods Backward Compatibility + +**Date**: 2026-08-20 (Revised) | **Feature**: [spec.md](spec.md) + +## 1. Flutter Plugin Dual SPM + CocoaPods Support + +**Decision**: Add SPM support via Package.swift while retaining the podspec with updated source paths. Follow Flutter's official recommendation for dual support. + +**Rationale**: Flutter explicitly states: "Flutter plugins should support both Swift Package Manager and CocoaPods until further notice." The official guide provides a migration path that preserves CocoaPods compatibility. + +**Key Findings**: + +- **Package.swift location**: `ios/optimizely-flutter-sdk/Package.swift` (hyphens — matches SPM normalized identity) +- **Source files location**: `ios/optimizely-flutter-sdk/Classes/` (simplified from SPM default `Sources//` using `path: "Classes"` override) +- **Symlink**: `ios/optimizely_flutter_sdk` → `ios/optimizely-flutter-sdk/` (underscores → hyphens, required for Flutter's plugin discovery) +- Source files move from `ios/Classes/` to `ios/optimizely-flutter-sdk/Classes/` +- Library product name replaces underscores with hyphens: `optimizely-flutter-sdk` +- `FlutterFramework` dependency with `path: "../FlutterFramework"` is mandatory — Flutter tooling provides this at build time (same as CocoaPods' `s.dependency 'Flutter'`) +- **Podspec stays**: Update `s.source_files` from `'Classes/**/*'` to `'optimizely-flutter-sdk/Classes/**/*'` +- Both Package.swift and podspec reference the same files in the new location +- **SPM identity normalization** (SE-0292): SPM 5.6+ normalizes underscores to hyphens in package identity. Directory must use hyphens to match. +- **Mixed Swift/ObjC limitation**: SPM cannot have Swift and ObjC in the same target. ObjC files are excluded from the SPM target via `exclude` but kept on disk for CocoaPods. +- **pluginClass change**: Changed from `OptimizelyFlutterSdkPlugin` (ObjC) to `SwiftOptimizelyFlutterSdkPlugin` (Swift) in `pubspec.yaml` for direct SPM registration. + +**Package.swift (actual)**: +```swift +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "optimizely_flutter_sdk", + platforms: [.iOS("10.0")], + products: [ + .library(name: "optimizely-flutter-sdk", targets: ["optimizely_flutter_sdk"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + .package(url: "https://github.com/optimizely/swift-sdk.git", exact: "5.4.2") + ], + targets: [ + .target( + name: "optimizely_flutter_sdk", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + .product(name: "Optimizely", package: "swift-sdk") + ], + path: "Classes", + exclude: ["OptimizelyFlutterSdkPlugin.m", "include"] + ) + ] +) +``` + +**Updated Podspec Changes**: +```ruby +# Before: s.source_files = 'Classes/**/*' +# After: +s.source_files = 'optimizely-flutter-sdk/Classes/**/*' +``` + +**Alternatives Considered**: +- Clean cut (remove CocoaPods entirely): Rejected by user — backward compatibility required +- Keep source files in `ios/Classes/` and symlink for SPM: Rejected — fragile, not recommended by Flutter +- Use `Sources/optimizely_flutter_sdk/` (SPM default layout): Rejected — `Classes/` is simpler and SPM's `path` parameter officially supports it (Apple docs: `Target.path`) + +## 2. OptimizelySwiftSDK SPM Compatibility + +**Decision**: Use `https://github.com/optimizely/swift-sdk.git` with `.exact("5.4.2")` version pinning in Package.swift. Keep `OptimizelySwiftSDK`, `5.4.2` in podspec. + +**Rationale**: The OptimizelySwiftSDK has first-class SPM support. Both dependency mechanisms must pin the same version to prevent drift (FR-006). + +**Key Findings**: +- SPM repo URL: `https://github.com/optimizely/swift-sdk.git` +- Package name: `swift-sdk` +- Product/library name: `Optimizely` +- Supported platforms: iOS 10.0+, tvOS 10.0+, macOS 10.14+, watchOS 3.0+ +- CocoaPods pod name: `OptimizelySwiftSDK` + +**Verification Required**: Confirm that git tag `5.4.2` exists on the `swift-sdk` repo. If the tag format is `v5.4.2`, the exact version string may need adjustment. + +## 3. Source File Migration (Dual Support Layout) + +**Decision**: Move all source files from `ios/Classes/` to `ios/optimizely-flutter-sdk/Classes/`. ObjC files are excluded from the SPM target but kept on disk for CocoaPods. Update podspec paths to match. + +**Rationale**: The `Classes/` directory name is simpler than SPM's default `Sources//` convention. SPM's `path` parameter officially supports custom source paths. The hyphenated directory name is required because SPM normalizes package identity by replacing underscores with hyphens (SE-0292). + +**Current Files** (before migration): +- `ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift` — main plugin +- `ios/Classes/OptimizelyFlutterLogger.swift` — logger bridge +- `ios/Classes/HelperClasses/Constants.swift` — constants +- `ios/Classes/HelperClasses/OptimizelyConfig+Extension.swift` — config extension +- `ios/Classes/HelperClasses/Utils.swift` — utilities +- `ios/Classes/OptimizelyFlutterSdkPlugin.h` — ObjC header (bridging) +- `ios/Classes/OptimizelyFlutterSdkPlugin.m` — ObjC implementation (bridging) + +**New Layout**: +``` +ios/optimizely-flutter-sdk/ # Real directory (hyphens — matches SPM identity) +├── Package.swift # SPM manifest +└── Classes/ # Source files (path override in Package.swift) + ├── include/ + │ └── optimizely_flutter_sdk/ + │ └── OptimizelyFlutterSdkPlugin.h # ObjC header (excluded from SPM, used by CocoaPods) + ├── OptimizelyFlutterSdkPlugin.m # ObjC bridge (excluded from SPM, used by CocoaPods) + ├── SwiftOptimizelyFlutterSdkPlugin.swift + ├── OptimizelyFlutterLogger.swift + ├── Constants.swift + ├── OptimizelyConfig+Extension.swift + └── Utils.swift +ios/optimizely_flutter_sdk -> optimizely-flutter-sdk/ # Symlink (underscores — Flutter discovery) +``` + +**ObjC Bridging Files**: +- ObjC files (`.m`, `.h`) are excluded from the SPM target because SPM does not support mixed Swift/ObjC in a single target +- They remain on disk for CocoaPods builds, which still compile them +- Package.swift uses `exclude: ["OptimizelyFlutterSdkPlugin.m", "include"]` to skip them + +## 4. Podspec Updates + +**Decision**: Keep `optimizely_flutter_sdk.podspec` at `ios/optimizely_flutter_sdk.podspec` and update `source_files` to point to the new directory layout. + +**Changes Required**: +```ruby +# Before +s.source_files = 'Classes/**/*' + +# After +s.source_files = 'optimizely-flutter-sdk/Classes/**/*' +``` + +**Verification**: Run `pod lib lint optimizely_flutter_sdk.podspec` after changes to ensure CocoaPods can still find and compile all source files. + +## 5. Example App + +**Decision**: No changes needed to the example app itself. Flutter tooling will use whichever dependency mechanism is available (SPM on 3.44+, CocoaPods on older versions). The example app's Podfile, Podfile.lock, and Pods/ remain as-is for CocoaPods compatibility. + +**Rationale**: The example app is a consumer of the plugin. Flutter handles the CocoaPods-to-SPM routing transparently. Removing the Podfile would break builds on older Flutter versions. + +## 6. Test App + +**Decision**: The test app needs both CI workflow updates and SPM configuration added to its iOS project. The test app's Podfile can be kept for CocoaPods fallback or removed if the CI Flutter version defaults to SPM. + +**Rationale**: The test app validates the SDK end-to-end. Per clarification, changes include CI workflow updates + adding SPM configuration to the test app's iOS project. This ensures US-3 acceptance scenarios can be verified. + +## 7. SDK CI Impact + +**Decision**: SDK CI workflows (`flutter.yml`) are updated to pin Flutter 3.44.0 across all jobs and enable SPM for the `build_test_ios` job. The `build_test_ios` job depends on `version_drift_check` to ensure podspec/Package.swift version parity before building. The test app CI (`ios.yml`) is pinned to Flutter 3.16.0 (pre-SPM) to validate the CocoaPods path. + +**Changes**: +- SDK CI: All jobs pin Flutter 3.44.0 for consistent caching. `build_test_ios` enables SPM and depends on `version_drift_check`. +- Test app CI: Pinned to Flutter 3.16.0, SPM enable step removed, CocoaPods steps (`brew install cocoapods`, `pod repo update`) remain. + +## 8. Version Synchronization (Critical) + +**Decision**: Both Package.swift and podspec MUST declare the same OptimizelySwiftSDK version. When bumping native SDK versions, both files must be updated simultaneously. A CI validation check (FR-014) MUST fail the build if versions diverge. + +**Implementation**: Add a CI validation step that extracts and compares the OptimizelySwiftSDK version from both files. Add a note to CLAUDE.md's version management section about the dual-file requirement. Constitution Principle VIII (Native SDK Version Pinning) already covers this — the source-of-truth files now include both `ios/optimizely_flutter_sdk.podspec` AND `ios/optimizely_flutter_sdk/Package.swift`. + +**No Deprecation Notices**: Per clarification, this release does not include deprecation notices or migration guides for CocoaPods users. That will be addressed closer to Flutter's actual CocoaPods removal. diff --git a/specs/001-cocoapods-to-spm-migration/spec.md b/specs/001-cocoapods-to-spm-migration/spec.md new file mode 100644 index 0000000..956d47b --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/spec.md @@ -0,0 +1,146 @@ +# Feature Specification: Add SPM Support with CocoaPods Backward Compatibility + +**Feature Branch**: `001-cocoapods-to-spm-migration` + +**Created**: 2026-08-20 + +**Status**: Draft (Revised) + +**Input**: User description: "Flutter going to drop CocoaPods support. Want to move to Swift Package Manager. Support both SPM and CocoaPods. Migrate the Flutter SDK and test app gracefully." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - SDK Consumers Build iOS via SPM (Priority: P1) + +A developer who integrates the Optimizely Flutter SDK into their Flutter app can build and run the iOS target using Swift Package Manager. The SDK's iOS dependency (OptimizelySwiftSDK) is resolved through SPM when the developer's Flutter tooling supports it. The developer's existing workflow (`flutter build ios`, `flutter run`) continues to work without manual SPM configuration steps. + +**Why this priority**: This is the core deliverable. Flutter is deprecating CocoaPods support, and SPM readiness ensures the SDK works with future Flutter versions. + +**Independent Test**: Run `flutter build ios` against a fresh Flutter app (on Flutter 3.44+) that depends on `optimizely_flutter_sdk`, and verify a successful build with OptimizelySwiftSDK resolved via SPM. + +**Acceptance Scenarios**: + +1. **Given** a Flutter app with `optimizely_flutter_sdk` as a dependency on Flutter 3.44+, **When** the developer runs `flutter build ios`, **Then** the build succeeds and OptimizelySwiftSDK is resolved via SPM. +2. **Given** the SDK plugin's iOS platform configuration, **When** Flutter tooling detects SPM support (Package.swift present), **Then** the OptimizelySwiftSDK package is fetched from its SPM-compatible repository and linked correctly. + +--- + +### User Story 2 - SDK Consumers Continue Building iOS via CocoaPods (Priority: P1) + +A developer on an older Flutter version (pre-3.44) or one who has not yet adopted SPM can continue building the iOS target using CocoaPods. The existing podspec remains functional and resolves the same OptimizelySwiftSDK version. + +**Why this priority**: Backward compatibility is essential. Not all consumers will be on Flutter 3.44+ immediately. Dropping CocoaPods support abruptly would break existing users. + +**Independent Test**: Run `flutter build ios` against a Flutter app (on any supported Flutter version) using CocoaPods, and verify the build succeeds with the same SDK functionality. + +**Acceptance Scenarios**: + +1. **Given** a Flutter app with `optimizely_flutter_sdk` as a dependency on Flutter <3.44, **When** the developer runs `flutter build ios`, **Then** the build succeeds using CocoaPods dependency resolution. +2. **Given** the updated podspec with new source file paths, **When** CocoaPods resolves the SDK, **Then** all Swift source files are found and compiled correctly. +3. **Given** a developer who was already using the SDK via CocoaPods, **When** they upgrade to this version, **Then** `pod install` succeeds and the app builds without changes to their Podfile. + +--- + +### User Story 3 - Test App Works with SPM (Priority: P2) + +The `optimizely-flutter-testapp` project's iOS target builds and runs using SPM. Integration tests and CI pipelines that depend on the test app continue to pass when using the SPM-based dependency path. + +**Why this priority**: The test app is the SDK's validation layer. CI integration tests (`integration_ios_tests`) depend on it. The test app should validate the SPM path to ensure it works end-to-end. + +**Independent Test**: Build and run the test app's iOS target from the `optimizely-flutter-testapp` repo using SPM, and verify all integration tests pass. + +**Acceptance Scenarios**: + +1. **Given** the test app repository with SPM-enabled Flutter, **When** a developer runs `flutter build ios` targeting iOS, **Then** the build succeeds using SPM for native dependency resolution. +2. **Given** the test app's CI pipeline configured for SPM, **When** the `integration_ios_tests` workflow runs, **Then** it completes successfully. +3. **Given** the test app's iOS project with SPM configuration added, **When** the SDK's native dependency is resolved, **Then** OptimizelySwiftSDK is fetched via SPM and the test app compiles and runs all integration tests. + +--- + +### User Story 4 - CI Pipeline Validates Both Dependency Paths (Priority: P2) + +CI workflows validate that the SDK works with both CocoaPods and SPM. The existing CocoaPods-based CI continues to work, and SPM-based builds are also verified. + +**Why this priority**: CI must validate both paths to ensure neither is broken. This is essential for maintaining dual support confidence. + +**Independent Test**: Trigger CI and verify all five jobs pass, with iOS builds validating SPM resolution. + +**Acceptance Scenarios**: + +1. **Given** the CI workflow for `build_test_ios`, **When** the job runs, **Then** it builds successfully. +2. **Given** the CI workflow for `integration_ios_tests`, **When** the job runs against the test app, **Then** integration tests pass. +3. **Given** both CocoaPods and SPM configurations exist, **When** CI runs, **Then** all five pipeline jobs pass without conflicts between the two dependency mechanisms. + +--- + +### Edge Cases + +- **Mixed CocoaPods/SPM state (Podfile + SPM-capable Flutter)**: Flutter's tooling handles resolution automatically. The SDK provides both Package.swift and podspec; Flutter picks the appropriate mechanism based on version and project configuration. No SDK-level detection or intervention is needed. +- **SPM package URL or version tag format differs from CocoaPods**: Mitigated by FR-006 — both manifests pin the same OptimizelySwiftSDK version. The SPM repository URL must match the official OptimizelySwiftSDK SPM-compatible repository. +- **Podspec source file paths after directory restructuring**: The podspec's `source_files` declaration is updated to point to the new directory structure (FR-008). Existing CocoaPods users running `pod install` will pick up the new paths automatically. +- **Package.swift and podspec declare different OptimizelySwiftSDK versions**: Prevented by FR-006 and enforced by FR-014 — a CI validation check fails the build if versions diverge. +- **SPM identity normalization (underscore vs hyphen)**: SPM 5.6+ normalizes package identities by replacing underscores with hyphens (SE-0292). The plugin name `optimizely_flutter_sdk` becomes `optimizely-flutter-sdk` in SPM. The real directory uses hyphens (`ios/optimizely-flutter-sdk/`) to match SPM's normalized identity, and a symlink with underscores (`ios/optimizely_flutter_sdk` → `optimizely-flutter-sdk/`) allows Flutter's tooling to discover the Package.swift at the expected path. Both paths are required — without the hyphenated directory SPM resolution fails, without the underscore symlink Flutter won't detect SPM support. +- **Mixed Swift/ObjC in SPM target**: SPM does not support mixed Swift and Objective-C in a single target. The ObjC files (`OptimizelyFlutterSdkPlugin.m`, `OptimizelyFlutterSdkPlugin.h`) are excluded from the SPM target via the `exclude` parameter but remain on disk for CocoaPods builds, which still use them. +- **Plugin registration class change**: The `pluginClass` in `pubspec.yaml` is changed from the ObjC class `OptimizelyFlutterSdkPlugin` to the Swift class `SwiftOptimizelyFlutterSdkPlugin` for direct SPM registration (since SPM cannot compile the ObjC registration shim). + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The SDK MUST declare its iOS native dependency (OptimizelySwiftSDK) using Swift Package Manager via a Package.swift file. +- **FR-002**: The SDK MUST use Flutter's official SPM integration mechanism (Package.swift in the plugin's iOS directory) to declare the native dependency. +- **FR-003**: The SDK MUST specify the OptimizelySwiftSDK version using exact pinning (`.exact("5.4.2")`) in the SPM package dependency, matching the version pinned in the podspec. +- **FR-004**: The SDK MUST retain the `optimizely_flutter_sdk.podspec` file with updated source file paths pointing to the new SPM-compatible directory structure. Both Package.swift and podspec MUST coexist. +- **FR-005**: The SDK MUST maintain all existing iOS functionality (MethodChannel handlers, logger bridge, type encoding) without any behavioral changes. +- **FR-006**: The podspec and Package.swift MUST declare the same OptimizelySwiftSDK version to prevent version drift. +- **FR-007**: The SDK's `pubspec.yaml` plugin declaration for iOS MUST remain compatible with both Flutter's SPM plugin system and CocoaPods. +- **FR-008**: Source files MUST be organized in a directory structure that is accessible from both the SPM Package.swift target and the podspec `source_files` declaration. The actual layout uses `ios/optimizely-flutter-sdk/Classes/` with SPM's `path: "Classes"` override, and ObjC files excluded from the SPM target via `exclude`. +- **FR-009**: The SDK documentation (README.md, CLAUDE.md) MUST be updated to document both CocoaPods and SPM setup paths. +- **FR-010**: The SDK MUST retain iOS 10.0 as its minimum deployment target. The test app maintains its own minimum of iOS 13.0. +- **FR-011**: All existing SDK unit tests (`flutter test`) MUST pass with zero failures after the changes — no test regressions are acceptable. +- **FR-012**: All test app end-to-end test cases MUST pass with zero failures on both Android and iOS platforms. +- **FR-013**: All five CI pipeline jobs MUST pass: `unit_test_coverage`, `build_test_android`, `build_test_ios`, `integration_android_tests`, and `integration_ios_tests`. The changes MUST NOT break any existing CI workflow. +- **FR-014**: CI MUST include a validation check that verifies the OptimizelySwiftSDK version declared in Package.swift matches the version declared in the podspec. The build MUST fail if the versions diverge. + +### Key Entities + +- **Package.swift**: SPM package manifest at `ios/optimizely-flutter-sdk/Package.swift`, declaring the OptimizelySwiftSDK dependency and linking the plugin's Swift source files. Uses `path: "Classes"` to override the default SPM `Sources//` convention. Coexists with the podspec. +- **optimizely_flutter_sdk.podspec**: Updated CocoaPods spec with `source_files` pointing to `optimizely-flutter-sdk/Classes/**/*`. +- **Plugin Registration**: The `pluginClass` in `pubspec.yaml` is set to `SwiftOptimizelyFlutterSdkPlugin` (Swift class) for SPM compatibility. CocoaPods builds still compile the ObjC shim but Flutter routes registration through the Swift class. +- **Symlink**: `ios/optimizely_flutter_sdk` → `ios/optimizely-flutter-sdk/` — bridges Flutter's underscore-based plugin discovery with SPM's hyphen-normalized package identity. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A fresh Flutter project (3.44+) depending on `optimizely_flutter_sdk` builds for iOS successfully via SPM. +- **SC-002**: A fresh Flutter project (pre-3.44 or CocoaPods-configured) depending on `optimizely_flutter_sdk` builds for iOS successfully via CocoaPods. +- **SC-003**: All existing SDK unit tests (`flutter test`) pass with zero failures — no test regressions. +- **SC-004**: All five CI pipeline jobs pass: `unit_test_coverage`, `build_test_android`, `build_test_ios`, `integration_android_tests`, and `integration_ios_tests`. +- **SC-005**: All test app end-to-end test cases pass on both Android and iOS platforms with zero failures. +- **SC-006**: Developer setup time for iOS does not increase — no new manual steps required beyond `flutter pub get` and `flutter build ios` for either dependency mechanism. +- **SC-007**: The podspec and Package.swift both reference the same OptimizelySwiftSDK version (no version drift). + +## Clarifications + +### Session 2026-08-20 + +- Q: What should the minimum iOS deployment target be after the SPM migration? → A: SDK keeps iOS 10.0; test app keeps iOS 13.0 (independent targets). +- Q: Should the SDK support both CocoaPods and SPM during a transition period? → A: ~~Clean cut~~ **Revised**: Support both CocoaPods and SPM simultaneously. Keep the podspec with updated paths alongside the new Package.swift. +- Q: How should the OptimizelySwiftSDK version be pinned in SPM Package.swift? → A: Exact version pinning (`.exact("5.4.2")`), matching current podspec behavior. +- Q: What specific changes are needed in the `optimizely-flutter-testapp` repo? → A: CI workflow updates + add SPM configuration to the test app's iOS project. +- Q: For mixed CocoaPods/SPM states, does the SDK need intervention logic? → A: No. Flutter's tooling handles resolution automatically; the SDK just provides both manifests. +- Q: How should version parity between Package.swift and podspec (FR-006) be enforced? → A: CI validation check that fails the build if versions diverge. +- Q: Should this release include deprecation notices or migration guidance for CocoaPods users? → A: No — premature until Flutter officially drops CocoaPods. + +## Assumptions + +- The OptimizelySwiftSDK already publishes SPM-compatible releases (it does — the library supports SPM via its Package.swift). +- Flutter's SPM integration for plugins is stable enough for production use (Flutter has been shipping SPM support and is actively deprecating CocoaPods). +- The SDK retains iOS 10.0 as its minimum deployment target; the test app uses iOS 13.0. These are independent and intentional. +- Source files are organized in `ios/optimizely-flutter-sdk/Classes/`. Both Package.swift (via `path: "Classes"`) and podspec (via `source_files: 'optimizely-flutter-sdk/Classes/**/*'`) reference the same files. +- SPM 5.6+ normalizes package identities by replacing underscores with hyphens (SE-0292). A symlink `ios/optimizely_flutter_sdk` → `ios/optimizely-flutter-sdk/` is required so Flutter can discover the Package.swift. +- The example app (`example/`) within the SDK repo will work with whichever dependency mechanism the local Flutter version prefers. +- macOS plugin platform (declared but experimental in pubspec.yaml) is out of scope for this effort. +- The `optimizely-flutter-testapp` repository is accessible and modifiable as part of this effort. Changes include CI workflow updates and adding SPM configuration to the test app's iOS project. +- CocoaPods support will be maintained for as long as Flutter officially supports it, then can be dropped in a future release. No deprecation notices or migration guides are included in this release — that will be addressed closer to Flutter's actual CocoaPods removal. diff --git a/specs/001-cocoapods-to-spm-migration/tasks.md b/specs/001-cocoapods-to-spm-migration/tasks.md new file mode 100644 index 0000000..25deac0 --- /dev/null +++ b/specs/001-cocoapods-to-spm-migration/tasks.md @@ -0,0 +1,220 @@ +# Tasks: Add SPM Support with CocoaPods Backward Compatibility + +**Input**: Design documents from `specs/001-cocoapods-to-spm-migration/` + +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, quickstart.md + +**Tests**: Not explicitly requested — test tasks omitted. Existing tests must pass (verified in validation tasks). + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Phase 1: Setup + +**Purpose**: Verify prerequisites and prepare for migration + +- [x] T001 Verify OptimizelySwiftSDK git tag format for version 5.4.2 on https://github.com/optimizely/swift-sdk (confirm tag is `5.4.2` vs `v5.4.2` and note the exact string for Package.swift) +- [x] T002 Create SPM directory structure: `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/include/optimizely_flutter_sdk/` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Move source files to SPM layout and create Package.swift — MUST complete before any user story work + +**CRITICAL**: No user story work can begin until this phase is complete + +- [x] T003 Move `ios/Classes/OptimizelyFlutterSdkPlugin.h` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h` +- [x] T004 Move `ios/Classes/OptimizelyFlutterSdkPlugin.m` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.m` and update its import path from `#import "OptimizelyFlutterSdkPlugin.h"` to `#import "./include/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.h"` +- [x] T005 [P] Move `ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/SwiftOptimizelyFlutterSdkPlugin.swift` +- [x] T006 [P] Move `ios/Classes/OptimizelyFlutterLogger.swift` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyFlutterLogger.swift` +- [x] T007 [P] Move `ios/Classes/HelperClasses/Constants.swift` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Constants.swift` +- [x] T008 [P] Move `ios/Classes/HelperClasses/OptimizelyConfig+Extension.swift` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/OptimizelyConfig+Extension.swift` +- [x] T009 [P] Move `ios/Classes/HelperClasses/Utils.swift` to `ios/optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/Utils.swift` +- [x] T010 Remove empty `ios/Classes/` directory (including `HelperClasses/` subdirectory) +- [x] T011 Remove empty `ios/Assets/` directory +- [x] T012 Create `ios/optimizely_flutter_sdk/Package.swift` with: swift-tools-version 5.9, iOS platform 10.0, FlutterFramework dependency, OptimizelySwiftSDK via `https://github.com/optimizely/swift-sdk.git` with exact version pinning, and `cSettings: [.headerSearchPath("include/optimizely_flutter_sdk")]` in target +- [x] T013 Verify `flutter pub get` succeeds from SDK repo root after file restructuring + +**Checkpoint**: SPM package structure is in place, all source files are in their new locations, Package.swift is created + +--- + +## Phase 3: User Story 1 — SDK Consumers Build iOS via SPM (Priority: P1) MVP + +**Goal**: The SDK plugin builds for iOS via SPM. Consumers on Flutter 3.44+ can run `flutter build ios` with SPM dependency resolution. + +**Independent Test**: Enable SPM (`flutter config --enable-swift-package-manager`), run `flutter build ios --simulator --no-codesign` from `example/` directory, and verify it succeeds with SPM resolving OptimizelySwiftSDK. + +### Implementation for User Story 1 + +- [x] T014 [US1] Run `flutter config --enable-swift-package-manager` to enable SPM for testing +- [x] T015 [US1] Run `flutter clean && flutter pub get` from `example/` directory +- [x] T016 [US1] Verify `flutter build ios --simulator --no-codesign` succeeds from `example/` directory with SPM resolving the swift-sdk package +- [x] T017 [US1] Run `flutter test` from SDK repo root and verify all existing unit tests pass with zero failures + +**Checkpoint**: SDK plugin builds for iOS via SPM. Example app compiles via SPM. All unit tests pass. + +--- + +## Phase 4: User Story 2 — SDK Consumers Continue Building via CocoaPods (Priority: P1) + +**Goal**: The existing podspec still works after source files moved to the new directory layout. Consumers on older Flutter can continue using CocoaPods. + +**Independent Test**: Disable SPM (`flutter config --no-enable-swift-package-manager`), run `flutter build ios --simulator --no-codesign` from `example/`, and verify it succeeds via CocoaPods with updated source paths. + +### Implementation for User Story 2 + +- [x] T018 [US2] Update `ios/optimizely_flutter_sdk.podspec`: change `s.source_files` from `'Classes/**/*'` to `'optimizely_flutter_sdk/Sources/optimizely_flutter_sdk/**/*'` +- [x] T019 [US2] Run `pod lib lint ios/optimizely_flutter_sdk.podspec --allow-warnings` to verify podspec validity with new source paths +- [x] T020 [US2] Run `flutter config --no-enable-swift-package-manager` to test CocoaPods path +- [x] T021 [US2] Run `flutter clean && flutter pub get` from `example/` directory +- [x] T022 [US2] Verify `flutter build ios --simulator --no-codesign` succeeds from `example/` directory via CocoaPods +- [x] T023 [US2] Run `flutter test` from SDK repo root and verify all existing unit tests still pass + +**Checkpoint**: SDK plugin builds for iOS via CocoaPods with updated source paths. Podspec lints clean. All unit tests pass. + +--- + +## Phase 5: User Story 3 — Test App Works with SPM (Priority: P2) + +**Goal**: The `optimizely-flutter-testapp` builds and runs on iOS via SPM. Integration tests pass. + +**Independent Test**: Build the test app with SPM enabled and run integration tests on iOS simulator. + +### Implementation for User Story 3 + +- [x] T024 [US3] Ensure test app's dependency on `optimizely_flutter_sdk` resolves the SPM-enabled version (check `/Users/muzahidul.islam/workspace/optimizely-flutter-testapp/pubspec.yaml` path dependency) +- [x] T025 [US3] Update test app CI workflow at `/Users/muzahidul.islam/workspace/optimizely-flutter-testapp/.github/workflows/ios.yml`: add `flutter config --enable-swift-package-manager` step before the build step; keep existing `brew install cocoapods` + `pod repo update` steps as fallback for CocoaPods backward compatibility +- [ ] T026 [US3] Run `flutter config --enable-swift-package-manager && flutter clean && flutter pub get` from test app repo root +- [ ] T027 [US3] Verify `flutter build ios --simulator --no-codesign` succeeds from test app repo with SPM resolving dependencies +- [ ] T028 [US3] Run integration tests on iOS simulator and verify all e2e test cases pass with zero failures + +**Checkpoint**: Test app builds and runs on iOS via SPM. CI workflow updated. All integration tests pass. + +--- + +## Phase 6: User Story 4 — CI Pipeline Validates Both Paths (Priority: P2) + +**Goal**: All CI workflows pass. Both CocoaPods and SPM build paths are validated. + +**Independent Test**: Push branch, create PR, and verify all five CI jobs pass. + +### Implementation for User Story 4 + +- [x] T029 [US4] Create version drift check script at `.github/scripts/check-version-drift.sh` (FR-014) that extracts OptimizelySwiftSDK version from both `ios/optimizely_flutter_sdk.podspec` and `ios/optimizely_flutter_sdk/Package.swift`, compares them, and exits non-zero if they diverge +- [x] T030 [US4] Integrate version drift check into SDK CI workflow `.github/workflows/flutter.yml` as a job step that runs before build jobs +- [x] T031 [US4] Review and confirm SDK CI `.github/workflows/flutter.yml` needs no other changes (no explicit pod commands exist) +- [ ] T032 [US4] Push branch and create PR to trigger all five CI jobs: `unit_test_coverage`, `build_test_android`, `build_test_ios`, `integration_android_tests`, `integration_ios_tests` +- [ ] T033 [US4] Verify all five CI jobs pass green, including the version drift check + +**Checkpoint**: All CI pipelines pass. Version drift is enforced. Both dependency paths validated. + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Purpose**: Documentation updates, version sync, and final validation + +- [x] T034 [P] Update `CLAUDE.md`: remove `cd ios && pod install` from setup commands (no longer needed), add note about dual SPM/CocoaPods support, update iOS setup instructions, add `ios/optimizely_flutter_sdk/Package.swift` to version pinning source-of-truth list in the Native SDK Version Pinning section +- [x] T035 [P] Update `README.md`: add note about SPM support alongside existing CocoaPods instructions if applicable (FR-009) +- [x] T036 Verify version consistency: confirm `ios/optimizely_flutter_sdk.podspec` and `ios/optimizely_flutter_sdk/Package.swift` both declare OptimizelySwiftSDK 5.4.2 +- [x] T037 Run full `flutter test` from SDK repo root — verify zero failures (final regression check) +- [x] T038 Run full `flutter analyze` from SDK repo root — verify zero issues +- [ ] T039 Run quickstart.md validation scenarios end-to-end (both SPM and CocoaPods paths, including version drift check) + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — can start immediately +- **Foundational (Phase 2)**: Depends on Setup (T001 tag verification informs T012 Package.swift version string) +- **User Story 1 (Phase 3)**: Depends on Foundational phase completion (Package.swift must exist) +- **User Story 2 (Phase 4)**: Depends on Foundational phase completion (files must be moved before podspec path update) +- **User Story 3 (Phase 5)**: Depends on US1 completion (SDK must have working SPM support) +- **User Story 4 (Phase 6)**: Depends on US1 and US2 completion (both paths must work before CI validation) +- **Polish (Phase 7)**: Depends on all user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Foundational (Phase 2) — no dependencies on other stories +- **User Story 2 (P1)**: Can start after Foundational (Phase 2) — can run in parallel with US1 +- **User Story 3 (P2)**: Depends on US1 — test app needs the SDK's SPM support to be working +- **User Story 4 (P2)**: Depends on US1 and US2 — CI validates both paths + +### Within Each User Story + +- Configuration before build verification +- Build verification before test verification +- Story complete before moving to next priority + +### Parallel Opportunities + +- T005, T006, T007, T008, T009 can all run in parallel (independent file moves) +- US1 and US2 can be worked on in parallel after Foundational phase completes +- T034 and T035 can run in parallel (independent documentation updates) + +--- + +## Parallel Example: Foundational Phase + +```bash +# Launch all Swift source file moves together (after T003/T004 ObjC handling): +Task: "Move SwiftOptimizelyFlutterSdkPlugin.swift to SPM sources" +Task: "Move OptimizelyFlutterLogger.swift to SPM sources" +Task: "Move Constants.swift to SPM sources" +Task: "Move OptimizelyConfig+Extension.swift to SPM sources" +Task: "Move Utils.swift to SPM sources" +``` + +## Parallel Example: User Story 1 + User Story 2 + +```bash +# After Foundational phase, both can start simultaneously: +# Developer A: User Story 1 (verify SPM path) +Task: "Enable SPM, build example app via SPM, verify tests pass" + +# Developer B: User Story 2 (verify CocoaPods path) +Task: "Update podspec paths, lint podspec, build example app via CocoaPods, verify tests pass" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (verify SPM tag) +2. Complete Phase 2: Foundational (move files + create Package.swift) +3. Complete Phase 3: User Story 1 (SDK builds via SPM) +4. **STOP and VALIDATE**: Build example app via SPM, run unit tests +5. This alone proves SPM support works + +### Incremental Delivery + +1. Setup + Foundational → Files moved, Package.swift created +2. Add User Story 1 → SDK builds via SPM (MVP!) +3. Add User Story 2 → SDK also builds via CocoaPods (backward compat confirmed) +4. Add User Story 3 → Test app works via SPM +5. Add User Story 4 → CI passes on both paths +6. Polish → Documentation updated, version sync verified + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- Commit after each phase or logical group +- Stop at any checkpoint to validate story independently +- The ObjC header placement (T003) and import path update (T004) should be done before Swift file moves to avoid build issues +- Version consistency (T036) is critical — both podspec and Package.swift must always declare the same OptimizelySwiftSDK version +- FR-014 version drift CI check is implemented in US4 (T029-T030)