Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/scripts/run_example_integration_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ Future<bool> _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<void>.delayed(const Duration(milliseconds: 200));
Expand Down
70 changes: 66 additions & 4 deletions .github/workflows/example-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/authentication/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 47 additions & 10 deletions examples/database_crud/integration_test/tasks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand All @@ -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<void> _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.
///
Expand All @@ -122,7 +136,30 @@ Future<void> _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<String>();
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.
Expand All @@ -136,5 +173,5 @@ Future<void> _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()}');
}
75 changes: 41 additions & 34 deletions examples/database_crud/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>(
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<Priority>(
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<String>(
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<Priority>(
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(
Expand Down
2 changes: 1 addition & 1 deletion examples/database_crud/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/edge_functions/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/passkeys/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/realtime_room/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/storage_transforms/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading