diff --git a/.github/workflows/manual-prepare_release_branch.yml b/.github/workflows/manual-prepare_release_branch.yml index 20c6258..b963ae2 100644 --- a/.github/workflows/manual-prepare_release_branch.yml +++ b/.github/workflows/manual-prepare_release_branch.yml @@ -164,14 +164,27 @@ jobs: done - echo "→ Bumping Android native SDK in build.gradle" - echo " Before:" && grep "cloud.mindbox:mobile-sdk" mindbox_android/android/build.gradle || true - sed -i "s/cloud.mindbox:mobile-sdk:.*/cloud.mindbox:mobile-sdk:$AND_VER'/" mindbox_android/android/build.gradle - echo " After:" && grep "cloud.mindbox:mobile-sdk" mindbox_android/android/build.gradle || true - # Fail the release if a version substitution didn't land (stale pin). assert_pin() { grep -qE "$2" "$1" || { echo "ERROR: pattern /$2/ not found in $1 — substitution failed"; exit 1; }; } + echo "→ Bumping Android native SDK in build.gradle" + echo " Before:" && grep "cloud.mindbox:" mindbox_android/android/build.gradle || true + # Every cloud.mindbox artifact in this file ships from the same native release, so they all + # take the same version — the ones named today and any added after this was written. + sed -i -E "s/(cloud\.mindbox:[a-z0-9-]+:)[^']*'/\1$AND_VER'/" mindbox_android/android/build.gradle + echo " After:" && grep "cloud.mindbox:" mindbox_android/android/build.gradle || true + + # Named artifacts are asserted the way the iOS pins are; the sweep after them catches a + # dependency nobody thought to name here, which would otherwise ship stale without a word. + assert_pin mindbox_android/android/build.gradle "cloud\.mindbox:mobile-sdk:$AND_VER'" + assert_pin mindbox_android/android/build.gradle "cloud\.mindbox:mindbox-common:$AND_VER'" + STALE=$(grep -nE "cloud\.mindbox:[a-z0-9-]+:" mindbox_android/android/build.gradle | grep -v ":$AND_VER'" || true) + if [ -n "$STALE" ]; then + echo "ERROR: build.gradle still pins Mindbox artifacts to another version:" + echo "$STALE" + exit 1 + fi + echo "→ Bumping iOS native SDK in podspec" echo " Before s.version:" && grep -E "s\.version" mindbox_ios/ios/mindbox_ios.podspec || true sed -i -E "s/(s\.version *= *')[^']+(')/\1$IO_VER\2/" mindbox_ios/ios/mindbox_ios.podspec diff --git a/git-release-branch.sh b/git-release-branch.sh index 98aaf59..3b8de2b 100755 --- a/git-release-branch.sh +++ b/git-release-branch.sh @@ -68,17 +68,28 @@ if ! [[ $ios_sdk_version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc)?$ ]]; then exit 1 fi -android_gradle="mindbox_android/android/build.gradle" -sed -i '' "s/ api 'cloud.mindbox:mobile-sdk:.*/ api 'cloud.mindbox:mobile-sdk:$android_sdk_version\'/" $android_gradle - -echo "Bump $android_gradle to $android_sdk_version" - # Fail loudly if a version substitution didn't land (e.g. the line format # changed and sed silently matched nothing, leaving a stale pin). assert_pin() { # grep -qE "$2" "$1" || { echo "ERROR: pattern /$2/ not found in $1 — version substitution failed"; exit 1; } } +android_gradle="mindbox_android/android/build.gradle" +# Every cloud.mindbox artifact in this file ships from the same native release, so they all +# take the same version — the ones named today and any added after this was written. +sed -i '' -E "s/(cloud\.mindbox:[a-z0-9-]+:)[^']*'/\1$android_sdk_version'/" $android_gradle + +echo "Bump $android_gradle to $android_sdk_version" + +assert_pin $android_gradle "cloud\.mindbox:mobile-sdk:$android_sdk_version'" +assert_pin $android_gradle "cloud\.mindbox:mindbox-common:$android_sdk_version'" +stale_pins=$(grep -nE "cloud\.mindbox:[a-z0-9-]+:" $android_gradle | grep -v ":$android_sdk_version'" || true) +if [ -n "$stale_pins" ]; then + echo "ERROR: $android_gradle still pins Mindbox artifacts to another version:" + echo "$stale_pins" + exit 1 +fi + ios_podspec="mindbox_ios/ios/mindbox_ios.podspec" sed -i '' "s/ s.version = .*/ s.version = '$ios_sdk_version'/" $ios_podspec diff --git a/mindbox/lib/src/embedded_block.dart b/mindbox/lib/src/embedded_block.dart index 2943ea5..f84c7dd 100644 --- a/mindbox/lib/src/embedded_block.dart +++ b/mindbox/lib/src/embedded_block.dart @@ -275,11 +275,24 @@ class _EmbeddedBlockState extends State<_EmbeddedBlock> { creationParams[EmbeddedBlockParams.timeoutMs] = timeout.inMilliseconds; } + // The recognizer is built by hand rather than by a RawGestureDetector, so nothing hands it the + // touch slop of the device the way the framework hands it to every scrollable. Left to itself it + // falls back to kTouchSlop — 18 logical pixels against the 8 an Android scrollable plays with — + // and a scrollable that wants the same direction takes the drag while the block is still short + // of its own threshold: the arena closes, and the carousel is never told a finger was on it. On + // equal slop the drag goes to whoever is closest to the finger, and inside the block that is the + // block. + // + // Read whole rather than by aspect: the aspect accessors arrived in Flutter 3.10, and the plugin + // still speaks to 3.0. Watching all of MediaQuery costs nothing here — a platform view keeps the + // recognizer it was first given, so the settings are read once however they are asked for. + final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings; + final Set> gestureRecognizers = _appearance == EmbeddedBlockAppearance.content ? >{ Factory( - () => HorizontalDragGestureRecognizer(), + () => HorizontalDragGestureRecognizer()..gestureSettings = gestureSettings, ), } : const >{}; diff --git a/mindbox/pubspec.yaml b/mindbox/pubspec.yaml index cb5ac16..d09755e 100644 --- a/mindbox/pubspec.yaml +++ b/mindbox/pubspec.yaml @@ -6,8 +6,8 @@ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox documentation: https://developers.mindbox.ru/docs/flutter-sdk-integration environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/mindbox/test/embedded_block_test.dart b/mindbox/test/embedded_block_test.dart index 698b353..0230aba 100644 --- a/mindbox/test/embedded_block_test.dart +++ b/mindbox/test/embedded_block_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -482,4 +483,164 @@ void main() { expect(methods, isNot(contains(EmbeddedBlockMethods.release))); }); }); + + group('A drag that two scrollables want', () { + // The device slop an Android scrollable plays with. The block used to fall back to kTouchSlop — + // 18 — and lose every horizontal drag to a parent that crosses 8 first. + const DeviceGestureSettings settings = DeviceGestureSettings(touchSlop: 8); + + late int viewId; + + setUp(() { + viewId = -1; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform_views, (MethodCall call) async { + // 'touch' carries a list, not a map: the block wins the arena and forwards the drag, so + // this handler is asked about more than the two methods it answers. + if (call.method != 'create' && call.method != 'resize') { + return null; + } + + final Map arguments = call.arguments as Map; + + if (call.method == 'resize') { + return { + 'width': arguments['width'], + 'height': arguments['height'], + }; + } + + viewId = arguments['id']! as int; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + MethodChannel(embeddedBlockChannelName(viewId)), + (MethodCall call) async => null, + ); + return 0; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform_views, null); + }); + + /// The native block saying it has content on screen — the only state in which the block asks + /// for horizontal drags at all. + Future showContent(WidgetTester tester) async { + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + embeddedBlockChannelName(viewId), + const StandardMethodCodec().encodeMethodCall( + const MethodCall( + EmbeddedBlockMethods.report, + {'appearance': 'content'}, + ), + ), + (ByteData? _) {}, + ); + await tester.pumpAndSettle(); + } + + Future showBlockInPageView(WidgetTester tester) async { + final PageController pages = PageController(); + addTearDown(pages.dispose); + + await tester.pumpWidget(MediaQuery( + data: const MediaQueryData(gestureSettings: settings), + child: Directionality( + textDirection: TextDirection.ltr, + child: PageView( + controller: pages, + children: const [ + Column( + children: [ + SizedBox(height: 200), + MindboxEmbeddedBlock(placeSystemName: 'stories', height: 104), + ], + ), + SizedBox.expand(), + ], + ), + ), + )); + await tester.pumpAndSettle(); + await showContent(tester); + + return pages; + } + + /// A finger crossing the screen the way a finger does — in small steps, not in one jump. The + /// step matters: whoever reaches its own slop on an earlier step closes the arena, and a block + /// that waits for 18 never gets to answer a parent that is done at 8. + Future dragBy(WidgetTester tester, Offset start, double distance) async { + final TestGesture gesture = await tester.startGesture(start); + for (double moved = 0; moved < distance.abs(); moved += 4) { + await gesture.moveBy(Offset(4 * distance.sign, 0)); + await tester.pump(); + } + await gesture.up(); + await tester.pumpAndSettle(); + } + + testWidgets('A drag on the block is the block\'s, and the page stays where it is', + (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + try { + final PageController pages = await showBlockInPageView(tester); + + await dragBy(tester, tester.getCenter(find.byType(AndroidView)), -600); + + expect(pages.page, 0); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + + testWidgets('A drag beside the block still turns the page', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + try { + final PageController pages = await showBlockInPageView(tester); + + final Offset besideTheBlock = tester.getCenter(find.byType(AndroidView)) - const Offset(0, 150); + await dragBy(tester, besideTheBlock, -600); + + expect(pages.page, 1); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + + testWidgets('A block still loading leaves the drag to the page', (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + try { + final PageController pages = PageController(); + addTearDown(pages.dispose); + + await tester.pumpWidget(MediaQuery( + data: const MediaQueryData(gestureSettings: settings), + child: Directionality( + textDirection: TextDirection.ltr, + child: PageView( + controller: pages, + children: const [ + Column( + children: [ + SizedBox(height: 200), + MindboxEmbeddedBlock(placeSystemName: 'stories', height: 104), + ], + ), + SizedBox.expand(), + ], + ), + ), + )); + await tester.pumpAndSettle(); + + await dragBy(tester, tester.getCenter(find.byType(AndroidView)), -600); + + expect(pages.page, 1); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + }); } diff --git a/mindbox_android/android/build.gradle b/mindbox_android/android/build.gradle index 6a56dab..438d53b 100644 --- a/mindbox_android/android/build.gradle +++ b/mindbox_android/android/build.gradle @@ -50,5 +50,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api 'cloud.mindbox:mobile-sdk:2.15.2' + api 'cloud.mindbox:mobile-sdk:2.16.0-rc' + compileOnly 'cloud.mindbox:mindbox-common:2.16.0-rc' } diff --git a/mindbox_android/pubspec.yaml b/mindbox_android/pubspec.yaml index 6283506..fa6428e 100644 --- a/mindbox_android/pubspec.yaml +++ b/mindbox_android/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_android environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/mindbox_ios/ios/mindbox_ios.podspec b/mindbox_ios/ios/mindbox_ios.podspec index 93f5e29..980547e 100644 --- a/mindbox_ios/ios/mindbox_ios.podspec +++ b/mindbox_ios/ios/mindbox_ios.podspec @@ -15,8 +15,8 @@ The implementation of 'mindbox' plugin for the iOS platform s.source = { :path => '.' } s.source_files = 'mindbox_ios/Sources/mindbox_ios/**/*.swift', 'Classes/MindboxFlutterAppDelegate.{h,m}' s.dependency 'Flutter' - s.dependency 'Mindbox', '2.15.1' - s.dependency 'MindboxNotifications', '2.15.1' + s.dependency 'Mindbox', '2.16.0-rc' + s.dependency 'MindboxNotifications', '2.16.0-rc' s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. diff --git a/mindbox_ios/ios/mindbox_ios/Package.swift b/mindbox_ios/ios/mindbox_ios/Package.swift index 177b7ef..ab1077d 100644 --- a/mindbox_ios/ios/mindbox_ios/Package.swift +++ b/mindbox_ios/ios/mindbox_ios/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "mindbox-ios", targets: ["mindbox_ios"]) ], dependencies: [ - .package(url: "https://github.com/mindbox-cloud/ios-sdk", exact: "2.15.1"), + .package(url: "https://github.com/mindbox-cloud/ios-sdk", exact: "2.16.0-rc"), ], targets: [ .target( diff --git a/mindbox_ios/pubspec.yaml b/mindbox_ios/pubspec.yaml index 2a6541e..8b9fbad 100644 --- a/mindbox_ios/pubspec.yaml +++ b/mindbox_ios/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_ios environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/mindbox_platform_interface/pubspec.yaml b/mindbox_platform_interface/pubspec.yaml index 5e6d711..448db44 100644 --- a/mindbox_platform_interface/pubspec.yaml +++ b/mindbox_platform_interface/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_platform_interface environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: