From 459846ed3d3de3310b8c642a14b15c35a92d208c Mon Sep 17 00:00:00 2001 From: Anh Date: Tue, 8 Aug 2023 12:59:07 +0700 Subject: [PATCH] Add user sign up flow --- flutter/flutter.mk | 6 +- flutter/ios/Podfile.lock | 100 +++++++----- flutter/lib/benchmark/state.dart | 14 -- .../lib/firebase/firebase_auth_service.dart | 50 ++++-- flutter/lib/firebase/firebase_manager.dart | 63 ++++++-- flutter/lib/l10n/app_en.arb | 8 + flutter/lib/main.dart | 5 +- flutter/lib/resources/result_manager.dart | 29 +++- .../lib/ui/history/result_list_screen.dart | 4 +- flutter/lib/ui/home/app_drawer.dart | 150 ++++++++++-------- flutter/lib/ui/home/share_button.dart | 46 +++++- flutter/lib/ui/home/user_profile.dart | 135 ++++++++++++++++ flutter/pubspec.lock | 134 ++++++++++++---- flutter/pubspec.yaml | 14 +- .../flutter/generated_plugin_registrant.cc | 6 + .../windows/flutter/generated_plugins.cmake | 2 + 16 files changed, 566 insertions(+), 200 deletions(-) create mode 100644 flutter/lib/ui/home/user_profile.dart diff --git a/flutter/flutter.mk b/flutter/flutter.mk index 0a0e4038b..55543d7e2 100644 --- a/flutter/flutter.mk +++ b/flutter/flutter.mk @@ -174,8 +174,8 @@ output/flutter/pub/%.stamp: %/pubspec.yaml .PHONY: flutter/test/unit flutter/test/unit: - cd flutter && ${_start_args} flutter --no-version-check test test -r expanded - cd flutter_common && ${_start_args} flutter --no-version-check test test -r expanded + cd flutter && ${_start_args} flutter --no-version-check test --no-pub test -r expanded + cd flutter_common && ${_start_args} flutter --no-version-check test --no-pub test -r expanded ifneq (${FLUTTER_TEST_DEVICE},) flutter_test_device_arg=--device-id "${FLUTTER_TEST_DEVICE}" @@ -186,7 +186,7 @@ flutter_perf_test_arg=--dart-define=enable-perf-test=${PERF_TEST} .PHONY: flutter/test/integration flutter/test/integration: cd flutter && ${_start_args} \ - flutter --no-version-check test \ + flutter --no-version-check test --no-pub \ integration_test \ ${flutter_test_device_arg} \ ${flutter_official_build_arg} \ diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index 25005476d..5dfc0a46c 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -1,63 +1,75 @@ PODS: + - desktop_webview_auth (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - file_picker (0.0.1): - Flutter - - Firebase/Auth (10.7.0): + - Firebase/Auth (10.12.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 10.12.0) + - Firebase/CoreOnly (10.12.0): + - FirebaseCore (= 10.12.0) + - Firebase/DynamicLinks (10.12.0): - Firebase/CoreOnly - - FirebaseAuth (~> 10.7.0) - - Firebase/CoreOnly (10.7.0): - - FirebaseCore (= 10.7.0) - - Firebase/Storage (10.7.0): + - FirebaseDynamicLinks (~> 10.12.0) + - Firebase/Storage (10.12.0): - Firebase/CoreOnly - - FirebaseStorage (~> 10.7.0) - - firebase_auth (4.4.2): - - Firebase/Auth (= 10.7.0) + - FirebaseStorage (~> 10.12.0) + - firebase_auth (4.7.2): + - Firebase/Auth (= 10.12.0) - firebase_core - Flutter - - firebase_core (2.10.0): - - Firebase/CoreOnly (= 10.7.0) + - firebase_core (2.15.0): + - Firebase/CoreOnly (= 10.12.0) + - Flutter + - firebase_dynamic_links (5.3.4): + - Firebase/DynamicLinks (= 10.12.0) + - firebase_core - Flutter - - firebase_storage (11.1.1): - - Firebase/Storage (= 10.7.0) + - firebase_storage (11.2.5): + - Firebase/Storage (= 10.12.0) - firebase_core - Flutter - - FirebaseAppCheckInterop (10.9.0) - - FirebaseAuth (10.7.0): + - FirebaseAppCheckInterop (10.13.0) + - FirebaseAuth (10.12.0): + - FirebaseAppCheckInterop (~> 10.0) - FirebaseCore (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/Environment (~> 7.8) - GTMSessionFetcher/Core (< 4.0, >= 2.1) - - FirebaseAuthInterop (10.9.0) - - FirebaseCore (10.7.0): + - FirebaseAuthInterop (10.13.0) + - FirebaseCore (10.12.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.9.0): + - FirebaseCoreExtension (10.13.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.9.0): + - FirebaseCoreInternal (10.13.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseStorage (10.7.0): + - FirebaseDynamicLinks (10.12.0): + - FirebaseCore (~> 10.0) + - FirebaseStorage (10.12.0): - FirebaseAppCheckInterop (~> 10.0) - FirebaseAuthInterop (~> 10.0) - FirebaseCore (~> 10.0) - FirebaseCoreExtension (~> 10.0) - GTMSessionFetcher/Core (< 4.0, >= 2.1) - Flutter (1.0.0) - - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - GoogleUtilities/AppDelegateSwizzler (7.11.5): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): + - GoogleUtilities/Environment (7.11.5): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Logger (7.11.5): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Network (7.11.5): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): - GoogleUtilities/Logger - GTMSessionFetcher/Core (3.1.1) - integration_test (0.0.1): @@ -69,7 +81,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - PromisesObjC (2.2.0) + - PromisesObjC (2.3.1) - share (0.0.1): - Flutter - share_plus (0.0.1): @@ -83,10 +95,12 @@ PODS: - Flutter DEPENDENCIES: + - desktop_webview_auth (from `.symlinks/plugins/desktop_webview_auth/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_dynamic_links (from `.symlinks/plugins/firebase_dynamic_links/ios`) - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) - Flutter (from `Flutter`) - integration_test (from `.symlinks/plugins/integration_test/ios`) @@ -108,12 +122,15 @@ SPEC REPOS: - FirebaseCore - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseDynamicLinks - FirebaseStorage - GoogleUtilities - GTMSessionFetcher - PromisesObjC EXTERNAL SOURCES: + desktop_webview_auth: + :path: ".symlinks/plugins/desktop_webview_auth/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -122,6 +139,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_dynamic_links: + :path: ".symlinks/plugins/firebase_dynamic_links/ios" firebase_storage: :path: ".symlinks/plugins/firebase_storage/ios" Flutter: @@ -146,27 +165,30 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock/ios" SPEC CHECKSUMS: + desktop_webview_auth: d645139460ef203d50bd0cdb33356785dd939cce device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed file_picker: ec55172937ec2d774460eb4538bad01df0171d81 - Firebase: 0219acf760880eeec8ce479895bd7767466d9f81 - firebase_auth: 98778cde4127b34d2d006f8bd049d3a8131f37bd - firebase_core: 18d44f087248303a4a8f05d0099a000c46e3c77a - firebase_storage: caf39437ad5db21ea75fd3dca77a21cd9178d158 - FirebaseAppCheckInterop: e69dde5cd51b88ee1b4339d6766b691272256f9b - FirebaseAuth: dd64c01631df724b09f33e584625775c52f7d71f - FirebaseAuthInterop: e53c08e60a02de17d1ab77c5032db8ae22d3a799 - FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 - FirebaseCoreExtension: d3e9bba2930a8033042112397cd9f006a1bb203d - FirebaseCoreInternal: d2b4acb827908e72eca47a9fd896767c3053921e - FirebaseStorage: 4841efa304543e1f9e4ca116c559c7a1ea2a9d0f + Firebase: 07150e75d142fb9399f6777fa56a187b17f833a0 + firebase_auth: 3f7820b22557dd4a1b024f4d86947d1a0ff8a10f + firebase_core: e477125798fc37cd4ab43ca6a8536bf7e0929c00 + firebase_dynamic_links: d85cf455646322fd101c8a5a5942c3d47132fe80 + firebase_storage: d5c1b95383db1230d9fed88c76cb257d8d1ec1d6 + FirebaseAppCheckInterop: 5e12dc623d443dedffcde9c6f3ed41510125d8ef + FirebaseAuth: a66c1e14ec58f41d154a4b41ce1a23ea00ad4805 + FirebaseAuthInterop: 74875bde5d15636522a8fe98beb561df7a54db58 + FirebaseCore: f86a1394906b97ac445ae49c92552a9425831bed + FirebaseCoreExtension: ce60f9db46d83944cf444664d6d587474128eeca + FirebaseCoreInternal: b342e37cd4f5b4454ec34308f073420e7920858e + FirebaseDynamicLinks: 1a387da899779e5ef34f4d6f8bdba882f90d0e67 + FirebaseStorage: 1d7ca8c8953fc61ccacaa7c612696b5402968a0d Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 diff --git a/flutter/lib/benchmark/state.dart b/flutter/lib/benchmark/state.dart index 3449d9cb2..8e39ddb7e 100644 --- a/flutter/lib/benchmark/state.dart +++ b/flutter/lib/benchmark/state.dart @@ -15,7 +15,6 @@ import 'package:mlperfbench/backend/list.dart'; import 'package:mlperfbench/benchmark/benchmark.dart'; import 'package:mlperfbench/board_decoder.dart'; import 'package:mlperfbench/build_info.dart'; -import 'package:mlperfbench/firebase/firebase_manager.dart'; import 'package:mlperfbench/resources/config_manager.dart'; import 'package:mlperfbench/resources/resource_manager.dart'; import 'package:mlperfbench/resources/validation_helper.dart'; @@ -99,10 +98,6 @@ class BenchmarkState extends ChangeNotifier { ); } - Future uploadLastResult() async { - // TODO: implement uploadLastResult - } - Future clearCache() async { await resourceManager.cacheManager.deleteLoadedResources([], 0); notifyListeners(); @@ -150,15 +145,6 @@ class BenchmarkState extends ChangeNotifier { ), needToPurgeCache, ); - if (FirebaseManager.enabled) { - await FirebaseManager.instance.initialize(); - final excluded = resourceManager.resultManager.results - .map((e) => e.meta.uuid) - .toList(); - final onlineResults = - await FirebaseManager.instance.downloadResults(excluded); - resourceManager.resultManager.results.addAll(onlineResults); - } print('finished loading resources'); error = null; stackTrace = null; diff --git a/flutter/lib/firebase/firebase_auth_service.dart b/flutter/lib/firebase/firebase_auth_service.dart index 88edcdeab..9033aef1f 100644 --- a/flutter/lib/firebase/firebase_auth_service.dart +++ b/flutter/lib/firebase/firebase_auth_service.dart @@ -1,23 +1,34 @@ -import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart' hide EmailAuthProvider; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; class FirebaseAuthService { + static final List providers = [ + EmailAuthProvider(), + ]; + late final FirebaseAuth firebaseAuth; - late final User user; + User get currentUser { + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + throw 'FirebaseAuth.instance.currentUser is null'; + } + print('currentUser: $user'); + return user; + } - signInAnonymously() async { + Future signInAnonymously() async { final userCredential = await firebaseAuth.signInAnonymously(); - user = await _getUserFromCredential(userCredential); + return _getUserFromCredential(userCredential); } - signIn({required String email, required String password}) async { + Future signIn({required String email, required String password}) async { try { - UserCredential userCredential = - await firebaseAuth.signInWithEmailAndPassword( + final userCredential = await firebaseAuth.signInWithEmailAndPassword( email: email, password: password, ); - user = await _getUserFromCredential(userCredential); + return _getUserFromCredential(userCredential); } on FirebaseAuthException catch (e) { if (e.code == 'user-not-found') { print('No user found for email $email'); @@ -28,10 +39,31 @@ class FirebaseAuthService { } } + Future link(AuthCredential authCred) async { + try { + final userCredential = await currentUser.linkWithCredential(authCred); + print('userCredential: $userCredential'); + return _getUserFromCredential(userCredential); + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'provider-already-linked': + throw ('The provider has already been linked to the user.'); + case 'invalid-credential': + throw ("The provider's credential is not valid."); + case 'credential-already-in-use': + throw ('The account corresponding to the credential already exists, ' + 'or is already linked to a Firebase User.'); + // See the API reference for the full list of error codes. + default: + rethrow; + } + } + } + Future _getUserFromCredential(UserCredential? credential) async { final user = credential?.user; if (user == null) { - throw Exception('User is not signed in'); + throw 'User is not signed in'; } return user; } diff --git a/flutter/lib/firebase/firebase_manager.dart b/flutter/lib/firebase/firebase_manager.dart index fc0ca56e9..86b0e988d 100644 --- a/flutter/lib/firebase/firebase_manager.dart +++ b/flutter/lib/firebase/firebase_manager.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_storage/firebase_storage.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:intl/intl.dart'; import 'package:mlperfbench_common/data/extended_result.dart'; @@ -18,8 +19,8 @@ class FirebaseManager { static final enabled = DefaultFirebaseOptions.available(); static final instance = FirebaseManager._(); - final auth = FirebaseAuthService(); - final storage = FirebaseStorageService(); + final _authService = FirebaseAuthService(); + final _storageService = FirebaseStorageService(); bool _isInitialized = false; @@ -29,35 +30,67 @@ class FirebaseManager { } final currentPlatform = DefaultFirebaseOptions.currentPlatform; final app = await Firebase.initializeApp(options: currentPlatform); - auth.firebaseAuth = FirebaseAuth.instance; - storage.firebaseStorage = FirebaseStorage.instance; print('Firebase initialized using projectId: ${app.options.projectId}'); + + await _initAuthentication(); + _initStorage(); + + _isInitialized = true; + return instance; + } + + Future _initAuthentication() async { + FirebaseUIAuth.configureProviders(FirebaseAuthService.providers); + _authService.firebaseAuth = FirebaseAuth.instance; if (DefaultFirebaseOptions.ciUserEmail.isNotEmpty) { - await auth.signIn( + final user = await _authService.signIn( email: DefaultFirebaseOptions.ciUserEmail, password: DefaultFirebaseOptions.ciUserPassword, ); - } else { - await auth.signInAnonymously(); + print('Signed in as CI user with email: ${user.email}'); } - print('User has uid: ${auth.user.uid} and email: ${auth.user.email}'); - _isInitialized = true; - return instance; + FirebaseAuth.instance.userChanges().listen((User? user) { + print('User did change uid: ${user?.uid} | email: ${user?.email}'); + }); + } + + void _initStorage() { + _storageService.firebaseStorage = FirebaseStorage.instance; } +} + +extension Authentication on FirebaseManager { + List get authProviders { + return FirebaseAuthService.providers; + } + + bool get isSignedIn { + return FirebaseAuth.instance.currentUser != null; + } + + Future signInAnonymously() async { + return _authService.signInAnonymously(); + } + + Future link(AuthCredential authCred) async { + return _authService.link(authCred); + } +} +extension Storage on FirebaseManager { Future uploadResult(ExtendedResult result) async { final DateFormat formatter = DateFormat('yyyy-MM-ddTHH-mm-ss'); final String datetime = formatter.format(result.meta.creationDate); // Example fileName: 2023-06-06T13-38-01_125ef847-ca9a-45e0-bf36-8fd22f493b8d.json final fileName = '${datetime}_${result.meta.uuid}.json'; - final uid = auth.user.uid; final jsonString = jsonToStringIndented(result); - await storage.upload(jsonString, uid, fileName); + await _storageService.upload( + jsonString, _authService.currentUser.uid, fileName); } Future> downloadResults(List excluded) async { - final uid = auth.user.uid; - final fileNames = await storage.list(uid); + final uid = _authService.currentUser.uid; + final fileNames = await _storageService.list(uid); List results = []; for (final fileName in fileNames) { // Example fileName: 2023-06-06T13-38-01_125ef847-ca9a-45e0-bf36-8fd22f493b8d.json @@ -67,7 +100,7 @@ class FirebaseManager { continue; } print('Download online result [$fileName]'); - final content = await storage.download(uid, fileName); + final content = await _storageService.download(uid, fileName); final json = jsonDecode(content) as Map; final result = ExtendedResult.fromJson(json); results.add(result); diff --git a/flutter/lib/l10n/app_en.arb b/flutter/lib/l10n/app_en.arb index 24a10767f..ab6f85ec5 100644 --- a/flutter/lib/l10n/app_en.arb +++ b/flutter/lib/l10n/app_en.arb @@ -9,6 +9,13 @@ "menuBenchmarkConfiguration": "Benchmark Configuration", "menuSettings": "Settings", "menuAbout": "About", + "menuProfile": "Profile", + "menuSignIn": "Sign In", + + "userAnonymousUser": "Anonymous User", + "userSignInAnonymously": "Sign in anonymously", + "userSignInEmailPassword": "Sign in with email/password", + "userProfile": "User Profile", "unsupportedMainMessage": "This device is not yet supported.", "unsupportedBackendError": "Error message", @@ -41,6 +48,7 @@ "uploadSuccess": "Results uploaded successfully", "uploadFail": "Error uploading results", + "uploadRequiredSignedIn": "An account is required to upload result.", "runFail": "Error while running benchmarks", "settingsOffline": "Offline mode", diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 4bdf6a62e..e9589bd60 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -10,6 +10,7 @@ import 'package:mlperfbench/benchmark/run_mode.dart'; import 'package:mlperfbench/benchmark/state.dart'; import 'package:mlperfbench/build_info.dart'; import 'package:mlperfbench/device_info.dart'; +import 'package:mlperfbench/firebase/firebase_manager.dart'; import 'package:mlperfbench/resources/utils.dart'; import 'package:mlperfbench/store.dart'; import 'package:mlperfbench/ui/root/app.dart'; @@ -36,7 +37,9 @@ Future launchUi() async { await BuildInfoHelper.staticInit(); final store = await Store.create(); final benchmarkState = await BenchmarkState.create(store); - + if (FirebaseManager.enabled) { + await FirebaseManager.instance.initialize(); + } if (const bool.fromEnvironment('autostart', defaultValue: false)) { assert(const bool.hasEnvironment('resultsStringMark')); assert(const bool.hasEnvironment('terminalStringMark')); diff --git a/flutter/lib/resources/result_manager.dart b/flutter/lib/resources/result_manager.dart index b0bcb8dda..bf6b2c372 100644 --- a/flutter/lib/resources/result_manager.dart +++ b/flutter/lib/resources/result_manager.dart @@ -8,6 +8,7 @@ import 'package:mlperfbench_common/data/result_sort.dart'; import 'package:mlperfbench_common/data/results/benchmark_result.dart'; import 'package:mlperfbench/benchmark/benchmark.dart'; +import 'package:mlperfbench/firebase/firebase_manager.dart'; import 'package:mlperfbench/resources/utils.dart'; class ResultManager { @@ -23,9 +24,10 @@ class ResultManager { return resultManager; } - final List results = []; ResultFilter resultFilter = ResultFilter(); ResultSort resultSort = ResultSort(); + final List localResults = []; + final List remoteResults = []; final List _resultsFiles = []; late final Directory _resultsDir; @@ -49,7 +51,7 @@ class ResultManager { final json = jsonDecode(await file.readAsString()) as Map; try { - results.add(ExtendedResult.fromJson(json)); + localResults.add(ExtendedResult.fromJson(json)); _resultsFiles.add(file); } catch (e, trace) { print('Unable to parse result from [$file]: $e'); @@ -59,13 +61,13 @@ class ResultManager { } Future deleteResult(ExtendedResult result) async { - final idx = results.indexOf(result); - results.removeAt(idx); + final idx = localResults.indexOf(result); + localResults.removeAt(idx); await _resultsFiles[idx].delete(); } Future saveResult(ExtendedResult result) async { - results.add(result); + localResults.add(result); final DateFormat formatter = DateFormat('yyyy-MM-ddTHH-mm-ss'); final String datetime = formatter.format(result.meta.creationDate); final resultFile = @@ -81,7 +83,7 @@ class ResultManager { } ExtendedResult getLastResult() { - return results.last; + return localResults.last; } void restoreResults( @@ -114,4 +116,19 @@ class ResultManager { validity: runResult.loadgenInfo?.validity ?? false, ); } + + Future uploadLastResult() async { + await FirebaseManager.instance.uploadResult(localResults.last); + } + + Future downloadRemoteResults() async { + final excluded = localResults.map((e) => e.meta.uuid).toList(); + final downloaded = await FirebaseManager.instance.downloadResults(excluded); + remoteResults.clear(); + remoteResults.addAll(downloaded); + } + + Future clearRemoteResult() async { + remoteResults.clear(); + } } diff --git a/flutter/lib/ui/history/result_list_screen.dart b/flutter/lib/ui/history/result_list_screen.dart index 725d03b12..e438b6c4a 100644 --- a/flutter/lib/ui/history/result_list_screen.dart +++ b/flutter/lib/ui/history/result_list_screen.dart @@ -26,9 +26,11 @@ class _ResultListScreenState extends State { Widget build(BuildContext context) { final state = context.watch(); final l10n = AppLocalizations.of(context); - final results = state.resourceManager.resultManager.results; + final localResults = state.resourceManager.resultManager.localResults; + final remoteResults = state.resourceManager.resultManager.remoteResults; final filter = state.resourceManager.resultManager.resultFilter; final sort = state.resourceManager.resultManager.resultSort; + final results = localResults + remoteResults; final resultsDataProvider = BenchmarksDataProvider(results); List resultItems = diff --git a/flutter/lib/ui/home/app_drawer.dart b/flutter/lib/ui/home/app_drawer.dart index 0fa27fde7..e99c26d75 100644 --- a/flutter/lib/ui/home/app_drawer.dart +++ b/flutter/lib/ui/home/app_drawer.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:mlperfbench/firebase/firebase_manager.dart'; import 'package:mlperfbench/localizations/app_localizations.dart'; import 'package:mlperfbench/ui/config/config_screen.dart'; import 'package:mlperfbench/ui/history/result_list_screen.dart'; +import 'package:mlperfbench/ui/home/user_profile.dart'; import 'package:mlperfbench/ui/settings/about_screen.dart'; import 'package:mlperfbench/ui/settings/settings_screen.dart'; @@ -11,7 +13,8 @@ class AppDrawer extends StatelessWidget { @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context); + final header = buildHeader(context); + final menuList = buildMenuList(context); return Drawer( // Add a ListView to the drawer. This ensures the user can scroll // through the options in the drawer if there isn't enough vertical @@ -19,70 +22,91 @@ class AppDrawer extends StatelessWidget { child: ListView( // Important: Remove any padding from the ListView. padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 80.0, - child: DrawerHeader( - // decoration: BoxDecoration(color: Colors.black), - // margin: EdgeInsets.all(0.0), - // padding: EdgeInsets.all(0.0), + children: [header] + menuList, + ), + ); + } - child: Text('MLPerf Mobile'), - ), - ), - ListTile( - leading: const Icon(Icons.access_time), - title: Text(l10n.menuHistory), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ResultListScreen(), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.tune), - title: Text(l10n.menuBenchmarkConfiguration), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ConfigScreen(), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.settings), - title: Text(l10n.menuSettings), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.info), - title: Text(l10n.menuAbout), - onTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AboutScreen(), - )); - }, - ), - ], + Widget buildHeader(BuildContext context) { + final l10n = AppLocalizations.of(context); + final appTitle = Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + l10n.menuHome, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ); + if (FirebaseManager.enabled) { + return DrawerHeader( + child: ListView( + children: [ + appTitle, + const UserProfile(), + ], + ), + ); + } else { + return SizedBox( + height: 80, + child: DrawerHeader(child: appTitle), + ); + } + } + + List buildMenuList(BuildContext context) { + final l10n = AppLocalizations.of(context); + return [ + ListTile( + leading: const Icon(Icons.access_time), + title: Text(l10n.menuHistory), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ResultListScreen(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.tune), + title: Text(l10n.menuBenchmarkConfiguration), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ConfigScreen(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.settings), + title: Text(l10n.menuSettings), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.info), + title: Text(l10n.menuAbout), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AboutScreen(), + )); + }, + ), + ]; } } diff --git a/flutter/lib/ui/home/share_button.dart b/flutter/lib/ui/home/share_button.dart index 72dc255b2..5db45fdb6 100644 --- a/flutter/lib/ui/home/share_button.dart +++ b/flutter/lib/ui/home/share_button.dart @@ -7,6 +7,7 @@ import 'package:mlperfbench/app_constants.dart'; import 'package:mlperfbench/benchmark/state.dart'; import 'package:mlperfbench/firebase/firebase_manager.dart'; import 'package:mlperfbench/localizations/app_localizations.dart'; +import 'package:mlperfbench/ui/home/user_profile.dart'; enum _ShareDestination { local, cloud } @@ -55,8 +56,7 @@ class _ShareButton extends State { setState(() { _isSharing = true; }); - final result = resultManager.getLastResult(); - await FirebaseManager.instance.uploadResult(result); + await resultManager.uploadLastResult(); setState(() { _isSharing = false; _shareStatus = l10n.uploadSuccess; @@ -98,12 +98,17 @@ class _ShareButton extends State { ), ), TextButton( - onPressed: !FirebaseManager.enabled - ? null - : () { - Navigator.of(context).pop(); - _handleSharing(_ShareDestination.cloud); - }, + onPressed: () { + if (!FirebaseManager.enabled) { + return; + } + if (!FirebaseManager.instance.isSignedIn) { + _buildProfileModal(context); + return; + } + Navigator.of(context).pop(); + _handleSharing(_ShareDestination.cloud); + }, child: Row( children: [ const Icon(Icons.cloud_upload), @@ -139,4 +144,29 @@ class _ShareButton extends State { valueColor: AlwaysStoppedAnimation(Colors.blue), ); } + + Future _buildProfileModal(BuildContext context) { + final l10n = AppLocalizations.of(context); + return showModalBottomSheet( + context: context, + builder: (context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.shareButtonMLCommons, + style: + TextStyle(color: AppColors.shareTextButton, fontSize: 18), + ), + const SizedBox(height: 20), + Text(l10n.uploadRequiredSignedIn), + const SizedBox(height: 20), + const UserProfile(), + ], + ), + ); + }); + } } diff --git a/flutter/lib/ui/home/user_profile.dart b/flutter/lib/ui/home/user_profile.dart new file mode 100644 index 000000000..06c43dc08 --- /dev/null +++ b/flutter/lib/ui/home/user_profile.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:provider/provider.dart'; + +import 'package:mlperfbench/benchmark/state.dart'; +import 'package:mlperfbench/firebase/firebase_manager.dart'; +import 'package:mlperfbench/localizations/app_localizations.dart'; + +class UserProfile extends StatefulWidget { + const UserProfile({Key? key}) : super(key: key); + + @override + State createState() { + return _UserProfileState(); + } +} + +class _UserProfileState extends State { + late BenchmarkState state; + late AppLocalizations l10n; + + @override + Widget build(BuildContext context) { + state = context.watch(); + l10n = AppLocalizations.of(context); + + final currentUser = FirebaseAuth.instance.currentUser; + final signInWithEmailButton = _buildSignInWithEmailButton(context); + final signInAnonymouslyButton = _buildSignInAnonymouslyButton(context); + final profileButton = _buildProfileButton(context); + + List children = []; + if (currentUser == null) { + children.add(signInAnonymouslyButton); + children.add(signInWithEmailButton); + } else { + final email = currentUser.email; + if (email != null) { + children.add(Text(email)); + } + if (currentUser.isAnonymous) { + children.add(Text(l10n.userAnonymousUser)); + } + children.add(const SizedBox(height: 8)); + children.add(profileButton); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + Widget _buildSignInAnonymouslyButton(BuildContext context) { + return ElevatedButton( + onPressed: () { + FirebaseManager.instance.signInAnonymously(); + Navigator.pop(context); + }, + child: Text(l10n.userSignInAnonymously), + ); + } + + Widget _buildSignInWithEmailButton(BuildContext context) { + final resultManager = state.resourceManager.resultManager; + final signInScreenActions = [ + AuthStateChangeAction((context, state) { + resultManager.downloadRemoteResults(); + Navigator.pop(context); + }), + AuthStateChangeAction((context, state) { + final authCred = state.credential.credential; + if (authCred != null) { + FirebaseManager.instance.link(authCred); + } + Navigator.pop(context); + }), + AuthStateChangeAction((context, state) { + Navigator.pop(context); + }), + ]; + + final signInButton = ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return Scaffold( + appBar: AppBar(title: Text(l10n.menuSignIn)), + body: SignInScreen( + providers: FirebaseManager.instance.authProviders, + actions: signInScreenActions, + ), + ); + }), + ); + }, + child: Text(l10n.userSignInEmailPassword), + ); + return signInButton; + } + + Widget _buildProfileButton(BuildContext context) { + final resultManager = state.resourceManager.resultManager; + var profileScreenActions = [ + SignedOutAction((context) { + resultManager.clearRemoteResult(); + Navigator.pop(context); + }) + ]; + final profileButton = ElevatedButton( + onPressed: () { + // FirebaseManager.instance.auth.signOut(); + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return Scaffold( + appBar: AppBar(title: Text(l10n.menuProfile)), + body: ProfileScreen( + providers: FirebaseManager.instance.authProviders, + actions: profileScreenActions, + )); + }, + ), + ); + }, + child: Text(l10n.userProfile), + ); + return profileButton; + } +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 10b8e69a3..69abca9cc 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "6a0ad72b2bcdb461749e40c01c478212a78db848dfcb2f10f2a461988bc5fb29" + sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.4" archive: dependency: "direct main" description: @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + desktop_webview_auth: + dependency: transitive + description: + name: desktop_webview_auth + sha256: a277d3ee920325560c06970bb817825d9ae369cf9ddf870b3eb704094d02a44c + url: "https://pub.dev" + source: hosted + version: "0.0.12" device_info_plus: dependency: "direct main" description: @@ -145,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + email_validator: + dependency: transitive + description: + name: email_validator + sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b + url: "https://pub.dev" + source: hosted + version: "2.1.17" fake_async: dependency: transitive description: @@ -181,74 +197,122 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: "8c97619ed2633148c41c344a59461f23c73bf8aa477ae48296703f06d9621fb0" + sha256: "49fd35ce06f2530dd460e5dc123235731cb61dd7c76b0af4b6e190404880d04d" url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.7.2" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: f7db119be795d79533c503887075a0315fc3ae1da6f9a9cd4f9100a62c68859c + sha256: "817f3ceb84ef5e9adaaf50cf7a19255f6ffcdd12c6f9e9aa4cf00fc7f2eb3cfb" url: "https://pub.dev" source: hosted - version: "6.13.1" + version: "6.16.1" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "45687246f5be811baf3810652460acd0f3d8c98f9d8f8b1961d7cc5c15c1e803" + sha256: e9044778287f1ff8f9f4cee7e247b03ec87bb8977e0e65ad27dc337e196132e8 url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.6.2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "239e4ac688674a7e7b5476fd16b0d8e2b5a453d464f32091af3ce1df4ebb7316" + sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.15.0" firebase_core_platform_interface: - dependency: "direct main" + dependency: transitive description: name: firebase_core_platform_interface - sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" + sha256: "0fd5c4b228de29b55fac38aed0d9e42514b3d3bd47675de52bf7f8fccaf922fa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.6.0" + firebase_dynamic_links: + dependency: transitive + description: + name: firebase_dynamic_links + sha256: "4872f4d7e94736041398bc3490c2ddd87ee159d6b051ba01ca2708e5260a7ebe" + url: "https://pub.dev" + source: hosted + version: "5.3.4" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + sha256: "946fccfefb67e26bf63e392f1b3917d79ea031d3071488f0c5e8ab72de8219ab" + url: "https://pub.dev" + source: hosted + version: "0.2.6+4" firebase_storage: dependency: "direct main" description: name: firebase_storage - sha256: "50f0916a04e351ae549582e66bd33d9eb39c2ccb426e8e8c601a17471259d06f" + sha256: "4b747005aee0c611242cdd553f58795f51e1567d2dfd4f75692fac3f67c8c336" url: "https://pub.dev" source: hosted - version: "11.1.1" + version: "11.2.5" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface - sha256: "2711e985fd61242914dd1253b496286b03d292c2ff0f8863cc17ed14cf1bb3da" + sha256: c77c7b6b7d283280993c81ea8ac95552b2ae521a7bb46a95181c1482e62d1633 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.4.4" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: "16622540f424c871e71150140da6a02e9df60c9536e46d3243300f18c206cbc3" + sha256: "6906245579f1af225e43df0395c9d9631cb3135cbfa3521a839196d3383bb89a" + url: "https://pub.dev" + source: hosted + version: "3.6.5" + firebase_ui_auth: + dependency: "direct main" + description: + name: firebase_ui_auth + sha256: e439571fcad7ed48450eed8d64c70b93765526b876327055469806a51101eff0 + url: "https://pub.dev" + source: hosted + version: "1.6.2" + firebase_ui_localizations: + dependency: transitive + description: + name: firebase_ui_localizations + sha256: b13be7432af3eed2ff6f2ed1c55a9afc32ffa7376fcada9e16be055fad6415ed + url: "https://pub.dev" + source: hosted + version: "1.5.0" + firebase_ui_oauth: + dependency: transitive + description: + name: firebase_ui_oauth + sha256: "2cbe5a8996134f1a57205a5ebfa5863c5d599c03a07aae70867de2ecdfbbde0e" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "1.4.7" + firebase_ui_shared: + dependency: transitive + description: + name: firebase_ui_shared + sha256: "6f36f067d955d41591aacf68aafbaec7053571f2f6ed495da8bfa803f7c633b7" + url: "https://pub.dev" + source: hosted + version: "1.3.0" fixnum: dependency: transitive description: @@ -487,34 +551,34 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.0" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.1.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.0" path_provider_platform_interface: dependency: transitive description: @@ -527,10 +591,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.0" pedantic: dependency: "direct dev" description: @@ -679,10 +743,10 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.3.0" shared_preferences: dependency: "direct main" description: @@ -1020,10 +1084,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" xml: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f8dbd6055..8c63469cb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -47,12 +47,10 @@ dependencies: intl: ^0.17.0 permission_handler: ^10.4.3 flutter_markdown: ^0.6.15 - # Use specific versions for Firebase to avoid build error on Windows - firebase_core: 2.10.0 - firebase_storage: 11.1.1 - firebase_auth: 4.4.2 - # Fix https://github.com/firebase/flutterfire/issues/10909 - firebase_core_platform_interface: 4.6.0 + firebase_storage: ^11.2.5 + firebase_core: ^2.15.0 + firebase_auth: ^4.7.2 + firebase_ui_auth: ^1.6.2 dev_dependencies: flutter_test: @@ -81,6 +79,10 @@ flutter: - assets/android-boards/database.json generate: true uses-material-design: true + fonts: + - family: SocialIcons + fonts: + - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf import_sorter: comments: false diff --git a/flutter/windows/flutter/generated_plugin_registrant.cc b/flutter/windows/flutter/generated_plugin_registrant.cc index d5013ba11..42fec2ff9 100644 --- a/flutter/windows/flutter/generated_plugin_registrant.cc +++ b/flutter/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,17 @@ #include "generated_plugin_registrant.h" +#include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopWebviewAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewAuthPlugin")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/flutter/windows/flutter/generated_plugins.cmake b/flutter/windows/flutter/generated_plugins.cmake index a0d138869..f24e192f4 100644 --- a/flutter/windows/flutter/generated_plugins.cmake +++ b/flutter/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_auth + firebase_core permission_handler_windows share_plus url_launcher_windows