From 6cae66b48e638df87401f068d4bf71429762ec26 Mon Sep 17 00:00:00 2001 From: Alexander Simson Date: Tue, 11 Aug 2026 14:33:51 +0200 Subject: [PATCH 1/2] Prepare 4.0.0 release --- CHANGELOG.md | 32 +++++++ README.md | 31 +++++-- android/build.gradle | 12 ++- .../castle_flutter/CastleFlutterPlugin.kt | 73 ++++----------- example/android/app/build.gradle | 8 +- example/android/settings.gradle | 2 +- example/ios/.gitignore | 5 ++ example/ios/Flutter/Debug.xcconfig | 2 - example/ios/Flutter/Release.xcconfig | 2 - .../Flutter/ephemeral/flutter_lldb_helper.py | 32 ------- .../ios/Flutter/ephemeral/flutter_lldbinit | 5 -- example/ios/Podfile | 41 --------- example/ios/Podfile.lock | 29 ------ example/ios/Runner.xcodeproj/project.pbxproj | 89 +++++-------------- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++++ example/lib/main.dart | 16 +--- example/pubspec.lock | 4 +- example/pubspec.yaml | 2 +- ios/castle_flutter.podspec | 22 ----- ios/castle_flutter/Package.swift | 6 +- .../castle_flutter/CastleFlutterPlugin.swift | 82 +++++++---------- lib/castle.dart | 17 ---- pubspec.lock | 2 +- pubspec.yaml | 7 +- test/castle_flutter_test.dart | 22 +---- 25 files changed, 177 insertions(+), 384 deletions(-) delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Podfile.lock delete mode 100644 ios/castle_flutter.podspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4ce2b..c14c21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 4.0.0 (2026-08-11) + +This release follows 2.2.1. There is no 3.x release — the version number was moved straight to +4.0.0 so that it matches the major version of the native SDKs the plugin wraps. + +- Updated Castle iOS SDK to 4.2.0 +- Updated Castle Android SDK to 4.0.2 + +**Breaking changes** + +- Minimum Android API level raised to 26 (Android 8.0), up from 21. +- iOS integration is now **Swift Package Manager only**. The `castle_flutter.podspec` has + been removed. Enable SPM in your app with `flutter config --enable-swift-package-manager` + (on by default from Flutter 3.44). +- Minimum iOS version is 13.0. +- Minimum Flutter version is 3.29.0, minimum Dart SDK 3.4.0. +- Removed methods that no longer exist in the native SDKs. + - Removed `flushIfNeeded(url)` + - Removed `userAgent` + - Removed `queueSize` + +**Fixes and enhancements** + +- `advertisingIdentifier(id)` is now applied on Android as well, via the new `adIdProvider` + configuration option. It was previously a no-op on Android. +- Fixed `baseURLAllowList` never being applied on iOS. The values are sent from Dart as + strings but were cast to `URL`, so the cast always failed and the option was dropped. + +See the [Android](https://docs.castle.io/docs/android-sdk-migrating-from-v3-to-v4) and +[iOS](https://docs.castle.io/docs/ios-sdk-migrating-from-v3-to-v4) native migration guides +for details on the underlying SDK changes. + ## 2.2.1 (2026-05-27) - Fix Android issues ([#60](https://github.com/castle/castle-flutter/issues/57)) diff --git a/README.md b/README.md index 1f37a9e..d5d56ec 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,41 @@ [![Pub Version](https://img.shields.io/pub/v/castle_flutter)](https://pub.dev/packages/castle_flutter) [![CircleCI](https://img.shields.io/circleci/build/github/castle/castle-flutter/master?token=1ac87baf6b68cf1b425e8156ffc0c7497432026f)](https://circleci.com/gh/castle/castle-flutter) -## Usage +## Installation To use this plugin, add `castle_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +This plugin requires **Swift Package Manager**. CocoaPods is not supported, because the Castle +iOS SDK is not published to CocoaPods from version 4.0.0 onwards. SPM is enabled by default from +Flutter 3.44; on earlier versions enable it before running `flutter pub get`: + +```sh +flutter config --enable-swift-package-manager +``` + +Note that `flutter pub get` fails for every platform, not just iOS, while SPM is disabled. + ## Requirements ### Flutter - - Flutter 1.20.0+ + - Flutter 3.29.0+ (3.44+ recommended) + - Dart 3.4.0+ ### iOS - - iOS 12.0+ - - Xcode 10.0+ + - iOS 13.0+ + - Swift Package Manager ### Android - - Android 5.0 + - Android 8.0 (API 26) + +## Upgrading from 2.x to 4.0.0 + +There is no 3.x release. The version number was moved straight to 4.0.0 so that it matches the +major version of the native SDKs the plugin wraps. + +Version 4.0.0 updates to Castle Android SDK 4.x and Castle iOS SDK 4.x, which contain breaking +changes of their own. See the [CHANGELOG](CHANGELOG.md), and the +[Android](https://docs.castle.io/docs/android-sdk-migrating-from-v3-to-v4) and +[iOS](https://docs.castle.io/docs/ios-sdk-migrating-from-v3-to-v4) migration guides. ## Usage diff --git a/android/build.gradle b/android/build.gradle index 4f81162..a476e48 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,11 +1,10 @@ group 'io.castle.flutter.castle_flutter' -version '2.0.14' +version '4.0.0' buildscript { - ext.kotlin_version = '2.1.20' + ext.kotlin_version = '2.2.20' repositories { google() - jcenter() mavenCentral() } @@ -18,7 +17,6 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() mavenCentral() } } @@ -29,13 +27,13 @@ apply plugin: 'kotlin-android' android { namespace 'io.castle.flutter.castle_flutter' - compileSdkVersion 35 + compileSdkVersion 36 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 21 + minSdkVersion 26 } lintOptions { disable 'InvalidPackage' @@ -59,5 +57,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api 'io.castle.android:castle:3.1.8' + api 'io.castle.android:castle:4.0.2' } diff --git a/android/src/main/kotlin/io/castle/flutter/castle_flutter/CastleFlutterPlugin.kt b/android/src/main/kotlin/io/castle/flutter/castle_flutter/CastleFlutterPlugin.kt index 6866b25..f31614a 100644 --- a/android/src/main/kotlin/io/castle/flutter/castle_flutter/CastleFlutterPlugin.kt +++ b/android/src/main/kotlin/io/castle/flutter/castle_flutter/CastleFlutterPlugin.kt @@ -1,10 +1,9 @@ package io.castle.flutter.castle_flutter import android.app.Application -import android.content.Context import androidx.annotation.NonNull -import io.castle.android.Castle -import io.castle.android.CastleConfiguration +import io.castle.Castle +import io.castle.Configuration import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -21,6 +20,11 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { private lateinit var application: Application + /// Advertising identifier supplied from Dart. Read lazily by the adIdProvider + /// installed at configure time, so it can be set before or after configure. + @Volatile + private var advertisingIdentifier: String? = null + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "castle") channel.setMethodCallHandler(this) @@ -41,15 +45,9 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { call.method.equals("flush") -> { this.flush(call, result) } - call.method.equals("flushIfNeeded") -> { - this.flushIfNeeded(call, result) - } call.method.equals("reset") -> { this.reset(result) } - call.method.equals("baseUrl") -> { - this.baseUrl(call, result) - } call.method.equals("createRequestToken") -> { this.createRequestToken(call, result) } @@ -59,12 +57,6 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { call.method.equals("userJwt") -> { this.userJwt(call, result) } - call.method.equals("userAgent") -> { - this.userAgent(call, result) - } - call.method.equals("queueSize") -> { - this.queueSize(call, result) - } call.method.equals("advertisingIdentifier") -> { this.advertisingIdentifier(call, result) } @@ -76,7 +68,7 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { private fun configure(call: MethodCall, result: Result) { try { - val builder = CastleConfiguration.Builder() + val builder = Configuration.Builder() call.argument("publishableKey")?.let { builder.publishableKey(it) @@ -96,6 +88,8 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { call.argument>("baseURLAllowList")?.let { builder.baseURLAllowList(it) } + builder.adIdProvider { advertisingIdentifier ?: "" } + val configuration = builder.build() Castle.configure(application, configuration) @@ -107,7 +101,7 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { private fun screen(call: MethodCall, result: Result) { try { - val name = call.argument("name") + val name = call.argument("name") ?: "" Castle.screen(name) result.success(true) } catch (e: Exception) { @@ -117,7 +111,7 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { private fun custom(call: MethodCall, result: Result) { try { - val name = call.argument("name") + val name = call.argument("name") ?: "" val properties = call.argument>("properties") if (properties == null) { Castle.custom(name) @@ -139,16 +133,6 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { } } - private fun flushIfNeeded(call: MethodCall, result: Result) { - try { - val url = call.argument("url") - Castle.flushIfNeeded(url) - result.success(true) - } catch (e: Exception) { - result.error("CastleException", e.localizedMessage, null) - } - } - private fun reset(result: Result) { try { Castle.reset() @@ -158,18 +142,9 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { } } - private fun baseUrl(call: MethodCall, result: Result) { - try { - val baseUrl: String = Castle.baseUrl() - result.success(baseUrl) - } catch (e: Exception) { - result.error("CastleException", e.localizedMessage, null) - } - } - private fun createRequestToken(call: MethodCall, result: Result) { try { - val token: String = Castle.createRequestToken() + val token: String? = Castle.createRequestToken() result.success(token) } catch (e: Exception) { result.error("CastleException", e.localizedMessage, null) @@ -187,32 +162,16 @@ class CastleFlutterPlugin: FlutterPlugin, MethodCallHandler { private fun userJwt(call: MethodCall, result: Result) { try { - val userJwt = call.argument("userJwt") - Castle.userJwt(userJwt) + val userJwt = call.argument("userJwt") ?: "" + Castle.setUserJwt(userJwt) result.success(true) } catch (e: Exception) { result.error("CastleException", e.localizedMessage, null) } } - private fun userAgent(call: MethodCall, result: Result) { - try { - val userAgent: String = Castle.userAgent() - result.success(userAgent) - } catch (e: Exception) { - result.error("CastleException", e.localizedMessage, null) - } - } - - private fun queueSize(call: MethodCall, result: Result) { - try { - val queueSize: Int = Castle.queueSize() - result.success(queueSize) - } catch (e: Exception) { - result.error("CastleException", e.localizedMessage, null) - } - } private fun advertisingIdentifier(call: MethodCall, result: Result) { + advertisingIdentifier = call.argument("identifier") result.success(true) } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 44ecb3e..f0b8856 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -22,10 +22,10 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -ext.kotlin_version = '2.1.20' +ext.kotlin_version = '2.2.20' android { - compileSdkVersion 35 + compileSdkVersion 36 namespace 'io.castle.flutter.castle_flutter_example' @@ -40,8 +40,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.castle.flutter.castle_flutter_example" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 33 + minSdkVersion 26 + targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/settings.gradle b/example/android/settings.gradle index a194ff9..560ce60 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.9.2" apply false - id "org.jetbrains.kotlin.android" version "2.1.20" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" \ No newline at end of file diff --git a/example/ios/.gitignore b/example/ios/.gitignore index e96ef60..73531ab 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -22,9 +22,14 @@ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh +Flutter/ephemeral/ ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* +# Swift Package Manager. The plugin pins castle-ios with `exact:`, so +# Package.resolved carries no information that Package.swift doesn't. +**/xcshareddata/swiftpm/ + # Exceptions to above rules. !default.mode1v3 !default.mode2v3 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index b2f5fae..592ceee 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,3 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#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 88c2914..592ceee 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,3 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf9..0000000 --- a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fb..0000000 --- a/example/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index e72e0b4..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# 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! - - 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) - end -end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index 2d643fe..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,29 +0,0 @@ -PODS: - - Castle (3.2.0) - - castle_flutter (2.2.0): - - Castle (= 3.2.0) - - Flutter - - Flutter (1.0.0) - -DEPENDENCIES: - - castle_flutter (from `.symlinks/plugins/castle_flutter/ios`) - - Flutter (from `Flutter`) - -SPEC REPOS: - trunk: - - Castle - -EXTERNAL SOURCES: - castle_flutter: - :path: ".symlinks/plugins/castle_flutter/ios" - Flutter: - :path: Flutter - -SPEC CHECKSUMS: - Castle: de26fa2c40a8bb11bd87c917a1cc7b6310cf6ab8 - castle_flutter: fe5ebd007ee566e7d826af0ff8c36f6c37893cbb - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - -PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d - -COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 04cf6a2..380855e 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ 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 */; }; - D7B11958C5365AFDFF87BBBA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACC9272F4FEEEEB82CCEFC30 /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -30,7 +30,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 11191DD11EC6C619A7678BEE /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -44,9 +43,7 @@ 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 = ""; }; - A247FADB8EFF69D1B9078126 /* 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 = ""; }; - ACC9272F4FEEEEB82CCEFC30 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E6E8D12FA9C7277BAE390D29 /* 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,34 +51,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D7B11958C5365AFDFF87BBBA /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 8C298C512A4C770A36647E4C /* Pods */ = { - isa = PBXGroup; - children = ( - 11191DD11EC6C619A7678BEE /* Pods-Runner.debug.xcconfig */, - A247FADB8EFF69D1B9078126 /* Pods-Runner.release.xcconfig */, - E6E8D12FA9C7277BAE390D29 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 901AB2D0A6F1C93DA6C10ED5 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ACC9272F4FEEEEB82CCEFC30 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -96,8 +76,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 8C298C512A4C770A36647E4C /* Pods */, - 901AB2D0A6F1C93DA6C10ED5 /* Frameworks */, ); sourceTree = ""; }; @@ -128,17 +106,18 @@ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 66277A23DF8751BABBC2ADE0 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 5CACE1A69429B3250A0C0F1A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -153,6 +132,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { LastUpgradeCheck = 1510; @@ -213,45 +195,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 5CACE1A69429B3250A0C0F1A /* [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; - }; - 66277A23DF8751BABBC2ADE0 /* [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; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -575,6 +518,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/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 @@ + + + + + + + + + + { String _token = ''; - int _queueSize = 0; - String _userAgent = ''; String _requestTokenHeaderName = ''; @override @@ -27,7 +25,7 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initCastle() async { - var token, userId, baseUrl, queueSize, userAgent, clientIdHeaderName, requestTokenHeaderName; + var token, requestTokenHeaderName; try { await Castle.configure( publishableKey: "pk_CTsfAeRTqxGgA7HHxqpEESvjfPp4QAKA", @@ -36,11 +34,9 @@ class _MyAppState extends State { flushLimit: 20, baseURLAllowList: ["http://google.com"], ); - // Set mock IDFA + // Set mock advertising identifier await Castle.advertisingIdentifier("00000000-0000-0000-0000-000000000001"); token = await Castle.createRequestToken; - queueSize = await Castle.queueSize; - userAgent = await Castle.userAgent; requestTokenHeaderName = await Castle.requestTokenHeaderName; await Castle.userJwt( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVjMjQ0ZjMwLTM0MzItNGJiYy04OGYxLTFlM2ZjMDFiYzFmZSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJlZ2lzdGVyZWRfYXQiOiIyMDIyLTAxLTAxVDA5OjA2OjE0LjgwM1oifQ.eAwehcXZDBBrJClaE0bkO9XAr4U3vqKUpyZ-d3SxnH0" @@ -56,8 +52,6 @@ class _MyAppState extends State { setState(() { _token = token.toString(); - _queueSize = queueSize; - _userAgent = userAgent.toString(); _requestTokenHeaderName = requestTokenHeaderName.toString(); }); } @@ -79,12 +73,6 @@ class _MyAppState extends State { Text( _token ), - Text( - _queueSize.toString() - ), - Text( - _userAgent - ), Text( _requestTokenHeaderName ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 88dca6f..211676c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -23,7 +23,7 @@ packages: path: ".." relative: true source: path - version: "2.2.0" + version: "4.0.0" characters: dependency: transitive description: @@ -201,4 +201,4 @@ packages: version: "15.0.0" sdks: dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 96975f4..4b116af 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the castle_flutter plugin. publish_to: 'none' environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.4.0 <4.0.0" dependencies: flutter: diff --git a/ios/castle_flutter.podspec b/ios/castle_flutter.podspec deleted file mode 100644 index a351bed..0000000 --- a/ios/castle_flutter.podspec +++ /dev/null @@ -1,22 +0,0 @@ -require 'yaml' - -package = YAML.load_file(File.join('..', 'pubspec.yaml')) - -Pod::Spec.new do |s| - s.name = 'castle_flutter' - s.version = package['version'] - s.summary = package['description'] - s.homepage = package['homepage'] - s.license = package['license'] - s.authors = package['author'] - s.source = { :path => '.' } - s.source_files = 'castle_flutter/Sources/castle_flutter/**/*.swift' - s.dependency 'Flutter' - s.platform = :ios, '12.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.2' - - s.dependency 'Castle', '3.2.0' -end diff --git a/ios/castle_flutter/Package.swift b/ios/castle_flutter/Package.swift index bf050cd..e177ca4 100644 --- a/ios/castle_flutter/Package.swift +++ b/ios/castle_flutter/Package.swift @@ -12,15 +12,13 @@ let package = Package( .library(name: "castle-flutter", targets: ["castle_flutter"]) ], dependencies: [ - .package(name: "FlutterFramework", path: "../FlutterFramework"), - .package(url: "https://github.com/castle/castle-ios.git", exact: "3.2.0") + .package(url: "https://github.com/castle/castle-ios.git", exact: "4.2.0") ], targets: [ .target( name: "castle_flutter", dependencies: [ - .product(name: "FlutterFramework", package: "FlutterFramework"), - .product(name: "Castle", package: "castle-ios") + .product(name: "CastleSDK", package: "castle-ios") ] ) ] diff --git a/ios/castle_flutter/Sources/castle_flutter/CastleFlutterPlugin.swift b/ios/castle_flutter/Sources/castle_flutter/CastleFlutterPlugin.swift index 76c906a..307bfee 100644 --- a/ios/castle_flutter/Sources/castle_flutter/CastleFlutterPlugin.swift +++ b/ios/castle_flutter/Sources/castle_flutter/CastleFlutterPlugin.swift @@ -1,6 +1,6 @@ import Flutter import UIKit -import Castle +import CastleSDK @objc(CastleFlutterPlugin) public class CastleFlutterPlugin: NSObject, FlutterPlugin { @@ -22,8 +22,6 @@ public class CastleFlutterPlugin: NSObject, FlutterPlugin { custom(call, result: result) case "flush": flush(call, result: result) - case "flushIfNeeded": - flushIfNeeded(call, result: result) case "reset": reset(call, result: result) case "createRequestToken": @@ -32,10 +30,6 @@ public class CastleFlutterPlugin: NSObject, FlutterPlugin { requestTokenHeaderName(call, result: result) case "userJwt": userJwt(call, result: result) - case "userAgent": - userAgent(call, result: result) - case "queueSize": - queueSize(call, result: result) case "advertisingIdentifier": setAdvertisingIdentifier(call, result: result) default: @@ -43,38 +37,46 @@ public class CastleFlutterPlugin: NSObject, FlutterPlugin { } } + // Castle.configure(_:) must be called on the main thread, otherwise it throws + // CastleConfigurationError.mustConfigureOnMainThread. Method channel handlers + // are already invoked on the platform (main) thread, so no dispatch is needed. private func configure(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - if let args = call.arguments as? Dictionary { - let configuration = CastleConfiguration(publishableKey: (args["publishableKey"] as? String)!) - configuration.adSupportBlock = { () -> String in - return self.idfa ?? "" - } + guard let args = call.arguments as? Dictionary, + let publishableKey = args["publishableKey"] as? String else { + result(FlutterError.init(code: "bad args", message: nil, details: nil)) + return + } - if let debugLoggingEnabled = args["debugLoggingEnabled"] as? Bool { - configuration.isDebugLoggingEnabled = debugLoggingEnabled - } + let configuration = CastleConfiguration(publishableKey: publishableKey) + configuration.adSupportBlock = { () -> String in + return self.idfa ?? "" + } - if let lifeCycleEventsEnabled = args["lifeCycleEventsEnabled"] as? Bool { - configuration.isApplicationLifecycleTrackingEnabled = lifeCycleEventsEnabled - } + if let debugLoggingEnabled = args["debugLoggingEnabled"] as? Bool { + configuration.isDebugLoggingEnabled = debugLoggingEnabled + } - if let flushLimit = args["flushLimit"] as? UInt { - configuration.flushLimit = flushLimit - } + if let lifeCycleEventsEnabled = args["lifeCycleEventsEnabled"] as? Bool { + configuration.isApplicationLifecycleTrackingEnabled = lifeCycleEventsEnabled + } - if let maxQueueLimit = args["maxQueueLimit"] as? UInt { - configuration.maxQueueLimit = maxQueueLimit - } + if let flushLimit = args["flushLimit"] as? Int { + configuration.flushLimit = flushLimit + } - if let baseURLAllowList = args["baseURLAllowList"] as? Array { - configuration.baseURLAllowList = baseURLAllowList - } + if let maxQueueLimit = args["maxQueueLimit"] as? Int { + configuration.maxQueueLimit = maxQueueLimit + } - Castle.configure(configuration) + if let baseURLAllowList = args["baseURLAllowList"] as? Array { + configuration.baseURLAllowList = baseURLAllowList.compactMap { URL(string: $0) } + } + do { + try Castle.configure(configuration) result(true) - } else { - result(FlutterError.init(code: "bad args", message: nil, details: nil)) + } catch { + result(FlutterError.init(code: "CastleException", message: error.localizedDescription, details: nil)) } } @@ -116,16 +118,6 @@ public class CastleFlutterPlugin: NSObject, FlutterPlugin { result(true) } - private func flushIfNeeded(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - if let args = call.arguments as? Dictionary { - Castle.flushIfNeeded(URL(string: (args["url"] as? String)!)!) - - result(true) - } else { - result(FlutterError.init(code: "bad args", message: nil, details: nil)) - } - } - private func reset(_ call: FlutterMethodCall, result: @escaping FlutterResult) { Castle.reset() @@ -137,15 +129,7 @@ public class CastleFlutterPlugin: NSObject, FlutterPlugin { } private func requestTokenHeaderName(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result(CastleRequestTokenHeaderName) - } - - private func userAgent(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result(Castle.userAgent()) - } - - private func queueSize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result(Castle.queueSize()) + result(Castle.requestTokenHeaderName) } private func setAdvertisingIdentifier(_ call: FlutterMethodCall, result: @escaping FlutterResult) { diff --git a/lib/castle.dart b/lib/castle.dart index b6ef65e..0540f12 100644 --- a/lib/castle.dart +++ b/lib/castle.dart @@ -41,13 +41,6 @@ class Castle { await _channel.invokeMethod('flush'); } - static Future flushIfNeeded(String url) async { - final bool? flushIfNeeded = await _channel.invokeMethod('flushIfNeeded', { - 'url': url, - }); - return flushIfNeeded; - } - static Future reset() async { await _channel.invokeMethod('reset'); } @@ -67,14 +60,4 @@ class Castle { final String? requestTokenHeaderName = await _channel.invokeMethod('requestTokenHeaderName'); return requestTokenHeaderName; } - - static Future get userAgent async { - final String? userAgent = await _channel.invokeMethod('userAgent'); - return userAgent; - } - - static Future get queueSize async { - final int? queueSize = await _channel.invokeMethod('queueSize'); - return queueSize; - } } diff --git a/pubspec.lock b/pubspec.lock index df3c012..f8b1377 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -258,4 +258,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index d98b54b..90061cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,11 @@ description: Castle Flutter plugin. name: castle_flutter -version: 2.2.1 -author: Castle Intelligence, Inc +version: 4.0.0 homepage: https://github.com/castle/castle-flutter environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.20.0" + sdk: '>=3.4.0 <4.0.0' + flutter: ">=3.29.0" dependencies: flutter: diff --git a/test/castle_flutter_test.dart b/test/castle_flutter_test.dart index 530bedf..2530f82 100644 --- a/test/castle_flutter_test.dart +++ b/test/castle_flutter_test.dart @@ -8,20 +8,13 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { switch (methodCall.method) { case 'createRequestToken': return "abc123"; case 'requestTokenHeaderName': return 'X-Castle-Request-Token'; - case 'userId': - return 'thisisatestuser1'; - case 'userSignature': - return '944d7d6c5187cafac297785bbf6de0136a2e10f31788e92b2822f5cfd407fa52'; - case 'userAgent': - return 'castle_flutter_example/1.0 (1)(Castle 1.2.2; Android 11; Google sdk_gphone_x86_arm)'; - case 'queueSize': - return 100; default: return null; } @@ -29,7 +22,8 @@ void main() { }); tearDown(() { - channel.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); }); test('createRequestToken', () async { @@ -39,12 +33,4 @@ void main() { test('requestTokenHeaderName', () async { expect(await Castle.requestTokenHeaderName, 'X-Castle-Request-Token'); }); - - test('userAgent', () async { - expect(await Castle.userAgent, 'castle_flutter_example/1.0 (1)(Castle 1.2.2; Android 11; Google sdk_gphone_x86_arm)'); - }); - - test('queueSize', () async { - expect(await Castle.queueSize, 100); - }); } From a2cd474f403db2a0ce85ab004e0d4b4ef661e904 Mon Sep 17 00:00:00 2001 From: Alexander Simson Date: Tue, 11 Aug 2026 14:33:52 +0200 Subject: [PATCH 2/2] Update CircleCI Flutter image and drop CocoaPods build --- .circleci/config.yml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 88e324a..0562430 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: test: docker: - - image: cirrusci/flutter + - image: ghcr.io/cirruslabs/flutter:stable steps: - checkout - run: flutter doctor @@ -41,6 +41,9 @@ jobs: fi echo 'export PATH="$HOME/flutter/bin:$PATH"' >> "$BASH_ENV" - run: flutter --version + # castle_flutter is Swift Package Manager only, so SPM has to be enabled + # before `flutter pub get` — it fails to resolve the plugin otherwise. + - run: flutter config --enable-swift-package-manager - run: flutter precache --ios - save_cache: name: Save Flutter SDK cache @@ -63,21 +66,6 @@ jobs: - ~/.pub-cache key: pub-cache-v1-{{ checksum "example/pubspec.lock" }} - - run: - name: Build iOS with CocoaPods - working_directory: example - command: | - flutter config --no-enable-swift-package-manager - flutter build ios --no-codesign --debug - - - run: - name: Reset example for SPM build - working_directory: example - command: | - flutter clean - rm -rf ios/Pods ios/.symlinks ios/Podfile.lock - flutter pub get - - restore_cache: name: Restore SPM cache keys: @@ -86,9 +74,7 @@ jobs: - run: name: Build iOS with Swift Package Manager working_directory: example - command: | - flutter config --enable-swift-package-manager - flutter build ios --no-codesign --debug + command: flutter build ios --no-codesign --debug - save_cache: name: Save SPM cache paths: