diff --git a/.github/scripts/run_example_integration_tests.dart b/.github/scripts/run_example_integration_tests.dart index b02952600..dc9cf2f7b 100644 --- a/.github/scripts/run_example_integration_tests.dart +++ b/.github/scripts/run_example_integration_tests.dart @@ -61,7 +61,9 @@ Future _waitForPort( while (DateTime.now().isBefore(deadline)) { try { final socket = await Socket.connect('localhost', port); - await socket.close(); + // Destroys rather than closes: close() only shuts the sending side down, + // and nothing reads this socket, so it would keep the process alive. + socket.destroy(); return true; } on SocketException { await Future.delayed(const Duration(milliseconds: 200)); diff --git a/.github/workflows/example-integration-tests.yml b/.github/workflows/example-integration-tests.yml index a5a901672..f3883e7fb 100644 --- a/.github/workflows/example-integration-tests.yml +++ b/.github/workflows/example-integration-tests.yml @@ -1,8 +1,13 @@ +# Runs on release pull requests, and on any pull request labelled +# "integration tests" so the suite can be exercised on demand. name: Example integration tests on: pull_request: - types: [opened, reopened, synchronize, edited] + # No "edited": a bot rewriting the description would otherwise cancel and + # restart the whole matrix. Release pull requests are opened with their + # title, and any other one can opt in with the label. + types: [opened, reopened, synchronize, labeled] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,7 +23,9 @@ env: jobs: backend: name: Host shared Supabase - if: startsWith(github.event.pull_request.title, 'chore(release)') + if: >- + startsWith(github.event.pull_request.title, 'chore(release)') + || contains(github.event.pull_request.labels.*.name, 'integration tests') timeout-minutes: 60 runs-on: ubuntu-latest env: @@ -105,7 +112,9 @@ jobs: test: name: ${{ matrix.id }} - if: startsWith(github.event.pull_request.title, 'chore(release)') + if: >- + startsWith(github.event.pull_request.title, 'chore(release)') + || contains(github.event.pull_request.labels.*.name, 'integration tests') timeout-minutes: 45 runs-on: ${{ matrix.os }} strategy: @@ -136,13 +145,29 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build libgtk-3-dev xvfb + - name: Enable KVM + if: matrix.target == 'android' + run: | + # Without access to /dev/kvm the emulator falls back to software + # emulation, which takes minutes to boot and then stops responding. + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Boot iOS simulator if: matrix.target == 'ios' run: | set -euo pipefail udid=$(xcrun simctl list devices available -j \ | jq -r '[.devices[][] | select(.name | test("iPhone"))][0].udid') - xcrun simctl boot "$udid" || true + # Erases first: the tests hang waiting for the app's VM service on a + # simulator carrying state from the runner image. Then boots and waits + # for the boot to finish, so the tests cannot launch into a simulator + # that is still coming up. + xcrun simctl shutdown all || true + xcrun simctl erase "$udid" + xcrun simctl bootstatus "$udid" -b echo "IOS_DEVICE=$udid" >> "$GITHUB_ENV" - name: Wait for the Supabase endpoint @@ -172,6 +197,36 @@ jobs: && flutter pub get) done + - name: Prepare the scaffolded macOS apps + if: matrix.target == 'macos' + shell: bash + run: | + set -euo pipefail + # The scaffolding only grants the sandbox the network server + # entitlement, so without the client one every request to Supabase + # fails with "Operation not permitted". + for example in database_crud passkeys; do + for config in DebugProfile Release; do + /usr/libexec/PlistBuddy \ + -c 'Add :com.apple.security.network.client bool true' \ + "examples/$example/macos/Runner/$config.entitlements" + done + done + # The passkeys plugin is only available from macOS 13.5, so the + # scaffolded 10.15 target fails to compile the plugin registrant. + sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [0-9.]*;/MACOSX_DEPLOYMENT_TARGET = 13.5;/g' \ + examples/passkeys/macos/Runner.xcodeproj/project.pbxproj + + - name: Prepare the scaffolded iOS apps + if: matrix.target == 'ios' + shell: bash + run: | + set -euo pipefail + # As above, the passkeys plugin needs iOS 16 rather than the + # scaffolded 13.0. + sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = 16.0;/g' \ + examples/passkeys/ios/Runner.xcodeproj/project.pbxproj + - name: Run integration tests if: matrix.target != 'android' shell: bash @@ -186,6 +241,13 @@ jobs: target: google_apis script: dart .github/scripts/run_example_integration_tests.dart android + - name: Dump the simulator log + if: always() && matrix.target == 'ios' + continue-on-error: true + run: | + xcrun simctl spawn "$IOS_DEVICE" log show --last 20m --style compact \ + --predicate 'processImagePath ENDSWITH "Runner"' | tail -n 300 + - name: Signal completion if: always() shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 28151b485..8fe5cca61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,36 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2026-08-14 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`gotrue` - `v2.27.2`](#gotrue---v2272) + - [`supabase` - `v2.16.1`](#supabase---v2161) + - [`supabase_flutter` - `v2.17.2`](#supabase_flutter---v2172) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `supabase` - `v2.16.1` + - `supabase_flutter` - `v2.17.2` + +--- + +#### `gotrue` - `v2.27.2` + + - **FIX**(gotrue): parse expires_in as num in Session.fromJson ([#1719](https://github.com/supabase/supabase-flutter/issues/1719)). ([6c8047ee](https://github.com/supabase/supabase-flutter/commit/6c8047eefb82c76936a14eebe281276526e3037c)) + + ## 2026-08-05 ### Changes diff --git a/examples/authentication/pubspec.yaml b/examples/authentication/pubspec.yaml index 0e938c337..c2989d5b5 100644 --- a/examples/authentication/pubspec.yaml +++ b/examples/authentication/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/examples/database_crud/integration_test/tasks_test.dart b/examples/database_crud/integration_test/tasks_test.dart index 4f81eefe9..3c6762943 100644 --- a/examples/database_crud/integration_test/tasks_test.dart +++ b/examples/database_crud/integration_test/tasks_test.dart @@ -70,7 +70,13 @@ void main() { createdTitle, ); await tester.tap(find.widgetWithText(FilledButton, 'Create')); - await _pumpUntil(tester, find.text(createdTitle)); + // Searches for the new task rather than looking for it in the full list: the + // dialog leaves the keyboard up, and on a phone that leaves room for only a + // couple of tiles, so it would otherwise end up below the fold. Waits for the + // tile rather than the title text, which also matches the text field of the + // dialog that is still animating away. + await _searchFor(tester, createdTitle); + await _pumpUntil(tester, _tile(createdTitle)); // Complete it by ticking the checkbox in its tile (update). await tester.tap(_inTile(createdTitle, find.byType(Checkbox))); @@ -93,20 +99,28 @@ void main() { renamedTitle, ); await tester.tap(find.widgetWithText(FilledButton, 'Save')); - await _pumpUntil(tester, find.text(renamedTitle)); - expect(find.text(createdTitle), findsNothing); + // The row stops matching the search for the old title, which is the filter + // still in the field, so it leaves the list. + await _pumpUntilGone(tester, _tile(createdTitle)); + await _searchFor(tester, renamedTitle); + await _pumpUntil(tester, _tile(renamedTitle)); // Delete it (delete). await tester.tap(_inTile(renamedTitle, find.byIcon(Icons.delete))); - await _pumpUntilGone(tester, find.text(renamedTitle)); + await _pumpUntilGone(tester, _tile(renamedTitle)); }); } +/// Finds the [ListTile] of the task called [title]. +Finder _tile(String title) => find.widgetWithText(ListTile, title); + /// Finds [target] within the [ListTile] that contains [title]. -Finder _inTile(String title, Finder target) => find.descendant( - of: find.ancestor(of: find.text(title), matching: find.byType(ListTile)), - matching: target, -); +Finder _inTile(String title, Finder target) => + find.descendant(of: _tile(title), matching: target); + +/// Filters the list down to [query] through the search field. +Future _searchFor(WidgetTester tester, String query) => + tester.enterText(find.widgetWithText(TextField, 'Search title'), query); /// Pumps frames until [finder] matches at least one widget or [timeout] elapses. /// @@ -122,7 +136,30 @@ Future _pumpUntil( await tester.pump(const Duration(milliseconds: 100)); if (finder.evaluate().isNotEmpty) return; } - fail('Timed out waiting for: $finder'); + fail('Timed out waiting for: $finder\n${_screen()}'); +} + +/// What is on screen, so a timeout says whether the app got somewhere else +/// rather than only which finder came up empty. +String _screen() { + final labels = find + .byType(Text) + .evaluate() + .map((element) => (element.widget as Text).data) + .whereType(); + final fields = find + .byType(EditableText) + .evaluate() + .map( + (element) => '"${(element.widget as EditableText).controller.text}"', + ); + final boxes = find + .byType(Checkbox) + .evaluate() + .map((element) => '${(element.widget as Checkbox).value}'); + return 'Labels: ${labels.join(' | ')}\n' + 'Text fields: ${fields.join(' | ')}\n' + 'Checkboxes: ${boxes.join(' | ')}'; } /// The inverse of [_pumpUntil]: pumps until [finder] matches nothing. @@ -136,5 +173,5 @@ Future _pumpUntilGone( await tester.pump(const Duration(milliseconds: 100)); if (finder.evaluate().isEmpty) return; } - fail('Timed out waiting for it to disappear: $finder'); + fail('Timed out waiting for it to disappear: $finder\n${_screen()}'); } diff --git a/examples/database_crud/lib/main.dart b/examples/database_crud/lib/main.dart index d7ae282d3..4d50b82fa 100644 --- a/examples/database_crud/lib/main.dart +++ b/examples/database_crud/lib/main.dart @@ -411,40 +411,47 @@ class _TaskDialogState extends State<_TaskDialog> { Widget build(BuildContext context) { return AlertDialog( title: const Text('New task'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: _title, - autofocus: true, - decoration: const InputDecoration(labelText: 'Title'), - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: _projectId, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Project'), - items: [ - for (final project in widget.projects) - DropdownMenuItem( - value: project.id, - child: Text(project.name), - ), - ], - onChanged: (value) => setState(() => _projectId = value!), - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: _priority, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Priority'), - items: [ - for (final priority in Priority.values) - DropdownMenuItem(value: priority, child: Text(priority.label)), - ], - onChanged: (value) => setState(() => _priority = value!), - ), - ], + // Scrolls so the fields still fit, and the actions stay reachable, when + // the keyboard leaves the dialog little room on a phone. + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _title, + autofocus: true, + decoration: const InputDecoration(labelText: 'Title'), + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _projectId, + isExpanded: true, + decoration: const InputDecoration(labelText: 'Project'), + items: [ + for (final project in widget.projects) + DropdownMenuItem( + value: project.id, + child: Text(project.name), + ), + ], + onChanged: (value) => setState(() => _projectId = value!), + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: _priority, + isExpanded: true, + decoration: const InputDecoration(labelText: 'Priority'), + items: [ + for (final priority in Priority.values) + DropdownMenuItem( + value: priority, + child: Text(priority.label), + ), + ], + onChanged: (value) => setState(() => _priority = value!), + ), + ], + ), ), actions: [ TextButton( diff --git a/examples/database_crud/pubspec.yaml b/examples/database_crud/pubspec.yaml index e4accb5b1..5d4371500 100644 --- a/examples/database_crud/pubspec.yaml +++ b/examples/database_crud/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/examples/edge_functions/pubspec.yaml b/examples/edge_functions/pubspec.yaml index cd544f49b..fce0c7c7f 100644 --- a/examples/edge_functions/pubspec.yaml +++ b/examples/edge_functions/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/examples/passkeys/pubspec.yaml b/examples/passkeys/pubspec.yaml index c8d70dd2e..8cd6447d5 100644 --- a/examples/passkeys/pubspec.yaml +++ b/examples/passkeys/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: flutter: sdk: flutter passkeys: ^2.21.1 - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/examples/realtime_room/pubspec.yaml b/examples/realtime_room/pubspec.yaml index edf88b9ae..7bfb52311 100644 --- a/examples/realtime_room/pubspec.yaml +++ b/examples/realtime_room/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/examples/storage_transforms/pubspec.yaml b/examples/storage_transforms/pubspec.yaml index 342c178d0..43b9fe4e2 100644 --- a/examples/storage_transforms/pubspec.yaml +++ b/examples/storage_transforms/pubspec.yaml @@ -14,7 +14,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: supabase_lints: ^0.1.1 diff --git a/packages/gotrue/CHANGELOG.md b/packages/gotrue/CHANGELOG.md index 565fb1db6..d111df015 100644 --- a/packages/gotrue/CHANGELOG.md +++ b/packages/gotrue/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.27.2 + + - **FIX**(gotrue): parse expires_in as num in Session.fromJson ([#1719](https://github.com/supabase/supabase-flutter/issues/1719)). ([6c8047ee](https://github.com/supabase/supabase-flutter/commit/6c8047eefb82c76936a14eebe281276526e3037c)) + ## 2.27.1 - **REFACTOR**(supabase_common): share the local stack test configuration ([#1640](https://github.com/supabase/supabase-flutter/issues/1640)). ([a08f06d3](https://github.com/supabase/supabase-flutter/commit/a08f06d3b746d1fa5e3cd17c3370fe10466cb69b)) diff --git a/packages/gotrue/lib/src/version.dart b/packages/gotrue/lib/src/version.dart index 616497824..b2194915a 100644 --- a/packages/gotrue/lib/src/version.dart +++ b/packages/gotrue/lib/src/version.dart @@ -1 +1 @@ -const version = '2.27.1'; +const version = '2.27.2'; diff --git a/packages/gotrue/pubspec.yaml b/packages/gotrue/pubspec.yaml index 48d2d532c..8d32d7d20 100644 --- a/packages/gotrue/pubspec.yaml +++ b/packages/gotrue/pubspec.yaml @@ -1,6 +1,6 @@ name: gotrue description: A dart client library for the GoTrue API. -version: 2.27.1 +version: 2.27.2 homepage: "https://supabase.com" repository: "https://github.com/supabase/supabase-flutter/tree/main/packages/gotrue" issue_tracker: "https://github.com/supabase/supabase-flutter/issues" diff --git a/packages/supabase/CHANGELOG.md b/packages/supabase/CHANGELOG.md index cf49c42db..812b27a54 100644 --- a/packages/supabase/CHANGELOG.md +++ b/packages/supabase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.16.1 + + - Update a dependency to the latest release. + ## 2.16.0 - **REFACTOR**(supabase_common): share the local stack test configuration ([#1640](https://github.com/supabase/supabase-flutter/issues/1640)). ([a08f06d3](https://github.com/supabase/supabase-flutter/commit/a08f06d3b746d1fa5e3cd17c3370fe10466cb69b)) diff --git a/packages/supabase/example/pubspec.yaml b/packages/supabase/example/pubspec.yaml index f3db7ea3c..b3441cbf3 100644 --- a/packages/supabase/example/pubspec.yaml +++ b/packages/supabase/example/pubspec.yaml @@ -10,7 +10,7 @@ resolution: workspace dependencies: web: '>=1.0.0 <2.0.0' - supabase: ^2.16.0 + supabase: ^2.16.1 dev_dependencies: build_runner: any diff --git a/packages/supabase/lib/src/version.dart b/packages/supabase/lib/src/version.dart index b3b2b8ce9..f73964887 100644 --- a/packages/supabase/lib/src/version.dart +++ b/packages/supabase/lib/src/version.dart @@ -1 +1 @@ -const version = '2.16.0'; +const version = '2.16.1'; diff --git a/packages/supabase/pubspec.yaml b/packages/supabase/pubspec.yaml index d1fc5fdb5..9bebca213 100644 --- a/packages/supabase/pubspec.yaml +++ b/packages/supabase/pubspec.yaml @@ -1,6 +1,6 @@ name: supabase description: A dart client for Supabase. This client makes it simple for developers to build secure and scalable products. -version: 2.16.0 +version: 2.16.1 homepage: 'https://supabase.com' repository: 'https://github.com/supabase/supabase-flutter/tree/main/packages/supabase' issue_tracker: 'https://github.com/supabase/supabase-flutter/issues' @@ -19,7 +19,7 @@ resolution: workspace dependencies: functions_client: 2.7.1 - gotrue: 2.27.1 + gotrue: 2.27.2 http: ^1.6.0 meta: ^1.7.0 postgrest: 2.9.1 diff --git a/packages/supabase_flutter/CHANGELOG.md b/packages/supabase_flutter/CHANGELOG.md index 25e9151a5..ea42aa22b 100644 --- a/packages/supabase_flutter/CHANGELOG.md +++ b/packages/supabase_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.17.2 + + - Update a dependency to the latest release. + ## 2.17.1 - Update a dependency to the latest release. diff --git a/packages/supabase_flutter/example/pubspec.yaml b/packages/supabase_flutter/example/pubspec.yaml index c7b41424a..49e194fc8 100644 --- a/packages/supabase_flutter/example/pubspec.yaml +++ b/packages/supabase_flutter/example/pubspec.yaml @@ -13,7 +13,7 @@ resolution: workspace dependencies: flutter: sdk: flutter - supabase_flutter: ^2.17.1 + supabase_flutter: ^2.17.2 dev_dependencies: flutter_test: diff --git a/packages/supabase_flutter/lib/src/version.dart b/packages/supabase_flutter/lib/src/version.dart index 62236708c..e163269ee 100644 --- a/packages/supabase_flutter/lib/src/version.dart +++ b/packages/supabase_flutter/lib/src/version.dart @@ -1 +1 @@ -const version = '2.17.1'; +const version = '2.17.2'; diff --git a/packages/supabase_flutter/pubspec.yaml b/packages/supabase_flutter/pubspec.yaml index 8de1930d4..39c294d88 100644 --- a/packages/supabase_flutter/pubspec.yaml +++ b/packages/supabase_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: supabase_flutter description: Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products. -version: 2.17.1 +version: 2.17.2 homepage: 'https://supabase.com' repository: 'https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter' issue_tracker: 'https://github.com/supabase/supabase-flutter/issues' @@ -26,7 +26,7 @@ dependencies: http: ^1.6.0 meta: ^1.16.0 passkeys_platform_interface: ^2.8.0 - supabase: 2.16.0 + supabase: 2.16.1 supabase_common: 0.1.2 url_launcher: ^6.3.2 shared_preferences: ^2.5.5