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
12 changes: 10 additions & 2 deletions .github/workflows/supabase_flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,24 @@ jobs:
melos bootstrap

- name: flutterfmt
if: ${{ matrix.sdk == '3.x'}}
if: ${{ matrix.flutter-version == '3.x'}}
run: dart format lib test -l 80 --set-exit-if-changed

- name: analyzer
if: ${{ matrix.sdk == '3.x'}}
if: ${{ matrix.flutter-version == '3.x'}}
run: flutter analyze --fatal-warnings --fatal-infos .

- name: Run tests
run: flutter test --concurrency=1

- name: Run tests on chrome with js
if: ${{ matrix.flutter-version == '3.x'}}
run: flutter test --platform chrome

- name: Run tests on chrome with wasm
if: ${{ matrix.flutter-version == '3.x'}}
run: flutter test --platform chrome --wasm

- name: Run tests with downgraded app_links
run: |
flutter pub downgrade app_links
Expand Down
19 changes: 13 additions & 6 deletions packages/supabase_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ class _LoginFormState extends State<_LoginForm> {
setState(() {
_loading = true;
});
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
final email = _emailController.text;
final password = _passwordController.text;
Expand All @@ -106,7 +108,7 @@ class _LoginFormState extends State<_LoginForm> {
password: password,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
scaffoldMessenger.showSnackBar(const SnackBar(
content: Text('Login failed'),
backgroundColor: Colors.red,
));
Expand All @@ -123,6 +125,8 @@ class _LoginFormState extends State<_LoginForm> {
setState(() {
_loading = true;
});
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
final email = _emailController.text;
final password = _passwordController.text;
Expand All @@ -131,7 +135,7 @@ class _LoginFormState extends State<_LoginForm> {
password: password,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
scaffoldMessenger.showSnackBar(const SnackBar(
content: Text('Signup failed'),
backgroundColor: Colors.red,
));
Expand Down Expand Up @@ -173,6 +177,8 @@ class _ProfileFormState extends State<_ProfileForm> {
}

Future<void> _loadProfile() async {
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
final userId = Supabase.instance.client.auth.currentUser!.id;
final data = (await Supabase.instance.client
Expand All @@ -186,7 +192,7 @@ class _ProfileFormState extends State<_ProfileForm> {
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
scaffoldMessenger.showSnackBar(const SnackBar(
content: Text('Error occurred while getting profile'),
backgroundColor: Colors.red,
));
Expand Down Expand Up @@ -219,6 +225,8 @@ class _ProfileFormState extends State<_ProfileForm> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
setState(() {
_loading = true;
Expand All @@ -233,13 +241,12 @@ class _ProfileFormState extends State<_ProfileForm> {
'website': website,
});
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
scaffoldMessenger.showSnackBar(const SnackBar(
content: Text('Saved profile'),
));
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
scaffoldMessenger.showSnackBar(const SnackBar(
content: Text('Error saving profile'),
backgroundColor: Colors.red,
));
Expand Down
13 changes: 6 additions & 7 deletions packages/supabase_flutter/lib/src/local_storage_web.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'package:web/web.dart';

final _localStorage = html.window.localStorage;
final _localStorage = window.localStorage;

Future<bool> hasAccessToken(String persistSessionKey) async =>
_localStorage.containsKey(persistSessionKey);
_localStorage.getItem(persistSessionKey) != null;

Future<String?> accessToken(String persistSessionKey) async =>
_localStorage[persistSessionKey];
_localStorage.getItem(persistSessionKey);

Future<void> removePersistedSession(String persistSessionKey) async =>
_localStorage.remove(persistSessionKey);
_localStorage.removeItem(persistSessionKey);

Future<void> persistSession(
String persistSessionKey, persistSessionString) async =>
_localStorage[persistSessionKey] = persistSessionString;
_localStorage.setItem(persistSessionKey, persistSessionString);
1 change: 1 addition & 0 deletions packages/supabase_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies:
path_provider: ^2.0.0
shared_preferences: ^2.0.0
logging: ^1.2.0
web: '>=0.5.0 <2.0.0'

dev_dependencies:
dart_jsonwebtoken: ^2.4.1
Expand Down
72 changes: 72 additions & 0 deletions packages/supabase_flutter/test/deep_link_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@TestOn('!browser')

import 'package:app_links/app_links.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'widget_test_stubs.dart';

void main() {
const supabaseUrl = '';
const supabaseKey = '';

group('Deep Link with PKCE code', () {
late final PkceHttpClient pkceHttpClient;
late final bool mockEventChannel;

/// Check if the current version of AppLinks uses an explicit call to get
/// the initial link. This is only the case before version 6.0.0, where we
/// can find the getInitialAppLink function.
///
/// CI pipeline is set so that it tests both app_links newer and older than v6.0.0
bool appLinksExposesInitialLinkInStream() {
try {
// before app_links 6.0.0
(AppLinks() as dynamic).getInitialAppLink;
return false;
} on NoSuchMethodError catch (_) {
return true;
}
}

setUp(() async {
pkceHttpClient = PkceHttpClient();

// Add initial deep link with a `code` parameter, use method channel if
// we are in a version of AppLinks that use the explcit method for
// getting the initial link. Otherwise we want to mock the event channel
// and put the initial link there.
mockEventChannel = appLinksExposesInitialLinkInStream();
mockAppLink(
mockMethodChannel: !mockEventChannel,
mockEventChannel: mockEventChannel,
initialLink: 'com.supabase://callback/?code=my-code-verifier',
);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
httpClient: pkceHttpClient,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
pkceAsyncStorage: MockAsyncStorage()
..setItem(
key: 'supabase.auth.token-code-verifier',
value: 'raw-code-verifier'),
),
);
});

test(
'Having `code` as the query parameter triggers `getSessionFromUrl` call on initialize',
() async {
// Wait for the initial app link to be handled, as this is an async
// process when mocking the event channel.
if (mockEventChannel) {
await Future.delayed(const Duration(milliseconds: 500));
}
expect(pkceHttpClient.requestCount, 1);
expect(pkceHttpClient.lastRequestBody['auth_code'], 'my-code-verifier');
});
});
}
65 changes: 3 additions & 62 deletions packages/supabase_flutter/test/supabase_flutter_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:app_links/app_links.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Expand Down Expand Up @@ -79,13 +78,14 @@ void main() {
authOptions: FlutterAuthClientOptions(
localStorage: MockExpiredStorage(),
pkceAsyncStorage: MockAsyncStorage(),
autoRefreshToken: false,
),
);
});

test('initial session contains the error', () async {
test('emits exception when no auto refresh', () async {
// Give it a delay to wait for recoverSession to throw
await Future.delayed(const Duration(milliseconds: 10));
await Future.delayed(const Duration(milliseconds: 100));

await expectLater(Supabase.instance.client.auth.onAuthStateChange,
emitsError(isA<AuthException>()));
Expand Down Expand Up @@ -113,65 +113,6 @@ void main() {
});
});

group('Deep Link with PKCE code', () {
late final PkceHttpClient pkceHttpClient;
late final bool mockEventChannel;

/// Check if the current version of AppLinks uses an explicit call to get
/// the initial link. This is only the case before version 6.0.0, where we
/// can find the getInitialAppLink function.
///
/// CI pipeline is set so that it tests both app_links newer and older than v6.0.0
bool appLinksExposesInitialLinkInStream() {
try {
// before app_links 6.0.0
(AppLinks() as dynamic).getInitialAppLink;
return false;
} on NoSuchMethodError catch (_) {
return true;
}
}

setUp(() async {
pkceHttpClient = PkceHttpClient();

// Add initial deep link with a `code` parameter, use method channel if
// we are in a version of AppLinks that use the explcit method for
// getting the initial link. Otherwise we want to mock the event channel
// and put the initial link there.
mockEventChannel = appLinksExposesInitialLinkInStream();
mockAppLink(
mockMethodChannel: !mockEventChannel,
mockEventChannel: mockEventChannel,
initialLink: 'com.supabase://callback/?code=my-code-verifier',
);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
httpClient: pkceHttpClient,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
pkceAsyncStorage: MockAsyncStorage()
..setItem(
key: 'supabase.auth.token-code-verifier',
value: 'raw-code-verifier'),
),
);
});

test(
'Having `code` as the query parameter triggers `getSessionFromUrl` call on initialize',
() async {
// Wait for the initial app link to be handled, as this is an async
// process when mocking the event channel.
if (mockEventChannel) {
await Future.delayed(const Duration(milliseconds: 500));
}
expect(pkceHttpClient.requestCount, 1);
expect(pkceHttpClient.lastRequestBody['auth_code'], 'my-code-verifier');
});
});
group('EmptyLocalStorage', () {
late EmptyLocalStorage localStorage;

Expand Down