diff --git a/.github/workflows/supabase_flutter.yml b/.github/workflows/supabase_flutter.yml index b46614ef4..3c66a6b92 100644 --- a/.github/workflows/supabase_flutter.yml +++ b/.github/workflows/supabase_flutter.yml @@ -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 diff --git a/packages/supabase_flutter/example/lib/main.dart b/packages/supabase_flutter/example/lib/main.dart index a4809f8ec..eb60a4749 100644 --- a/packages/supabase_flutter/example/lib/main.dart +++ b/packages/supabase_flutter/example/lib/main.dart @@ -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; @@ -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, )); @@ -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; @@ -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, )); @@ -173,6 +177,8 @@ class _ProfileFormState extends State<_ProfileForm> { } Future _loadProfile() async { + final ScaffoldMessengerState scaffoldMessenger = + ScaffoldMessenger.of(context); try { final userId = Supabase.instance.client.auth.currentUser!.id; final data = (await Supabase.instance.client @@ -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, )); @@ -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; @@ -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, )); diff --git a/packages/supabase_flutter/lib/src/local_storage_web.dart b/packages/supabase_flutter/lib/src/local_storage_web.dart index f27a737ac..4e70784c9 100644 --- a/packages/supabase_flutter/lib/src/local_storage_web.dart +++ b/packages/supabase_flutter/lib/src/local_storage_web.dart @@ -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 hasAccessToken(String persistSessionKey) async => - _localStorage.containsKey(persistSessionKey); + _localStorage.getItem(persistSessionKey) != null; Future accessToken(String persistSessionKey) async => - _localStorage[persistSessionKey]; + _localStorage.getItem(persistSessionKey); Future removePersistedSession(String persistSessionKey) async => - _localStorage.remove(persistSessionKey); + _localStorage.removeItem(persistSessionKey); Future persistSession( String persistSessionKey, persistSessionString) async => - _localStorage[persistSessionKey] = persistSessionString; + _localStorage.setItem(persistSessionKey, persistSessionString); diff --git a/packages/supabase_flutter/pubspec.yaml b/packages/supabase_flutter/pubspec.yaml index d0cfde90d..e732912be 100644 --- a/packages/supabase_flutter/pubspec.yaml +++ b/packages/supabase_flutter/pubspec.yaml @@ -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 diff --git a/packages/supabase_flutter/test/deep_link_test.dart b/packages/supabase_flutter/test/deep_link_test.dart new file mode 100644 index 000000000..7bfadf84d --- /dev/null +++ b/packages/supabase_flutter/test/deep_link_test.dart @@ -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'); + }); + }); +} diff --git a/packages/supabase_flutter/test/supabase_flutter_test.dart b/packages/supabase_flutter/test/supabase_flutter_test.dart index 9205e77ba..d487032e6 100644 --- a/packages/supabase_flutter/test/supabase_flutter_test.dart +++ b/packages/supabase_flutter/test/supabase_flutter_test.dart @@ -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'; @@ -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())); @@ -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;