diff --git a/data/lib/core/exceptions.dart b/data/lib/core/exceptions.dart new file mode 100644 index 0000000..96454c2 --- /dev/null +++ b/data/lib/core/exceptions.dart @@ -0,0 +1,17 @@ +class UnauthenticatedException implements Exception { + final String message; + + UnauthenticatedException(this.message); + + @override + String toString() => 'UnauthenticatedException: $message'; +} + +class ServerException implements Exception { + final String message; + + ServerException(this.message); + + @override + String toString() => 'ServerException: $message'; +} diff --git a/data/lib/data_sources/authenticated_http_client.dart b/data/lib/data_sources/authenticated_http_client.dart new file mode 100644 index 0000000..4c0689b --- /dev/null +++ b/data/lib/data_sources/authenticated_http_client.dart @@ -0,0 +1,33 @@ +// lib/data/datasources/authenticated_http_client.dart + +import 'package:domain/repositories_abstract/token_provider.dart'; +import 'package:http/http.dart' as http; + +import '../core/exceptions.dart'; + +class AuthenticatedHttpClient extends http.BaseClient { + final http.Client _inner; + final TokenProvider _tokenProvider; + + AuthenticatedHttpClient(this._inner, this._tokenProvider); + + @override + Future send(http.BaseRequest request) async { + final token = await _tokenProvider.getToken(); + if (token != null) { + request.headers['Authorization'] = 'Bearer $token'; + } + + var response = await _inner.send(request); + + if (response.statusCode == 401) { + // Token is invalid or expired, delete it + await _tokenProvider.deleteToken(); + + // Notify the app or handle as needed + throw UnauthenticatedException('Token expired'); + } + + return response; + } +} diff --git a/data/lib/providers/token_provider_impl.dart b/data/lib/providers/token_provider_impl.dart new file mode 100644 index 0000000..50a8fb2 --- /dev/null +++ b/data/lib/providers/token_provider_impl.dart @@ -0,0 +1,24 @@ +// lib/data/providers/token_provider_impl.dart + +import 'package:domain/repositories_abstract/token_provider.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class TokenProviderImpl implements TokenProvider { + final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + static const _tokenKey = 'accessToken'; + + @override + Future saveToken(String token) async { + await _secureStorage.write(key: _tokenKey, value: token); + } + + @override + Future getToken() async { + return await _secureStorage.read(key: _tokenKey); + } + + @override + Future deleteToken() async { + await _secureStorage.delete(key: _tokenKey); + } +} diff --git a/data/lib/repositories/auth_repository.dart b/data/lib/repositories/auth_repository.dart index b635519..ebb735b 100644 --- a/data/lib/repositories/auth_repository.dart +++ b/data/lib/repositories/auth_repository.dart @@ -6,10 +6,14 @@ import 'package:domain/entities/user.dart'; import 'package:domain/repositories_abstract/auth_repository.dart'; import 'package:http/http.dart' as http; +import '../core/exceptions.dart'; +import '../data_sources/authenticated_http_client.dart'; + class AuthRepositoryImpl implements AuthRepository { final http.Client client; + final AuthenticatedHttpClient authClient; - AuthRepositoryImpl({required this.client}); + AuthRepositoryImpl({required this.client, required this.authClient}); @override Future signup(String email, String password) async { @@ -43,4 +47,22 @@ class AuthRepositoryImpl implements AuthRepository { throw Exception('Login failed'); } } + + @override + Future logout() async { + final url = Uri.parse('http://localhost:3000/api/auth/logout'); + + final response = await authClient.post( + url, + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + return; + } else { + // Handle error responses + throw ServerException( + 'Logout failed with status code: ${response.statusCode}'); + } + } } diff --git a/data/pubspec.yaml b/data/pubspec.yaml index 1b09686..952819e 100644 --- a/data/pubspec.yaml +++ b/data/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: domain: # Add dependency on the domain module path: ../domain # Local path to your domain module uuid: ^4.5.1 + flutter_secure_storage: ^9.2.2 dev_dependencies: flutter_test: diff --git a/domain/lib/repositories_abstract/auth_repository.dart b/domain/lib/repositories_abstract/auth_repository.dart index 76e6fcf..5b871b2 100644 --- a/domain/lib/repositories_abstract/auth_repository.dart +++ b/domain/lib/repositories_abstract/auth_repository.dart @@ -4,5 +4,6 @@ import '../entities/user.dart'; abstract class AuthRepository { Future signup(String email, String password); - Future login(String email, String password); // New login method + Future login(String email, String password); + Future logout(); } diff --git a/domain/lib/repositories_abstract/token_provider.dart b/domain/lib/repositories_abstract/token_provider.dart new file mode 100644 index 0000000..e0bb74b --- /dev/null +++ b/domain/lib/repositories_abstract/token_provider.dart @@ -0,0 +1,7 @@ +// lib/domain/providers/token_provider.dart + +abstract class TokenProvider { + Future saveToken(String token); + Future getToken(); + Future deleteToken(); +} diff --git a/domain/lib/usecases/auth_usecase.dart b/domain/lib/usecases/auth_usecase.dart index 9961741..3796fc6 100644 --- a/domain/lib/usecases/auth_usecase.dart +++ b/domain/lib/usecases/auth_usecase.dart @@ -2,11 +2,13 @@ import '../entities/user.dart'; import '../repositories_abstract/auth_repository.dart'; +import '../repositories_abstract/token_provider.dart'; class AuthUseCase { final AuthRepository repository; + final TokenProvider tokenProvider; - AuthUseCase({required this.repository}); + AuthUseCase({required this.repository, required this.tokenProvider}); Future signup(String email, String password) async { // Add any business logic or validation here if needed @@ -15,6 +17,20 @@ class AuthUseCase { Future login(String email, String password) async { // Add any business logic or validation here if needed - return await repository.login(email, password); + String accessToken = await repository.login(email, password); + await tokenProvider.saveToken(accessToken); + return accessToken; + } + + Future logout() async { + await repository.logout(); + tokenProvider.deleteToken(); + return; + } + + @override + Future isLoggedIn() async { + final token = await tokenProvider.getToken(); + return token != null; } } diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 180109e..32b64fb 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -1,16 +1,22 @@ +import 'package:data/data_sources/authenticated_http_client.dart'; import 'package:data/data_sources/function_tools_data_source.dart'; import 'package:data/data_sources/open_ai_data_source.dart'; +import 'package:data/providers/token_provider_impl.dart'; import 'package:data/repositories/auth_repository.dart'; import 'package:data/repositories/chat_repository_impl.dart'; import 'package:data/repositories/chat_session_repository_imp.dart'; import 'package:domain/domain.dart'; import 'package:domain/repositories_abstract/auth_repository.dart'; +import 'package:domain/repositories_abstract/token_provider.dart'; import 'package:domain/usecases/auth_usecase.dart'; import 'package:domain/usecases/function_tools_usecase.dart'; import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; -import 'package:swiftcomp/presentation/more/NewLogin/viewModels/login_view_model.dart'; -import 'package:swiftcomp/presentation/more/NewLogin/viewModels/signup_view_model.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; +import 'package:swiftcomp/presentation/more/viewModels/feature_flag_view_model.dart'; +import 'package:swiftcomp/presentation/more/viewModels/login_view_model.dart'; +import 'package:swiftcomp/presentation/more/viewModels/more_view_model.dart'; +import 'package:swiftcomp/presentation/more/viewModels/signup_view_model.dart'; import 'presentation/chat/viewModels/chat_view_model.dart'; final sl = GetIt.instance; @@ -20,9 +26,15 @@ void initInjection() { sl.registerFactory(() => ChatViewModel( chatUseCase: sl(), chatSessionUseCase: sl(), functionToolsUseCase: sl())); sl.registerFactory(() => LoginViewModel(authUseCase: sl())); - sl.registerFactory(() => SignupViewModel( - authUseCase: sl())); + sl.registerFactory(() => SignupViewModel(authUseCase: sl())); + sl.registerFactory( + () => MoreViewModel(authUseCase: sl(), featureFlagProvider: sl())); + sl.registerFactory( + () => FeatureFlagViewModel(featureFlagProvider: sl())); + // Providers + sl.registerLazySingleton(() => TokenProviderImpl()); + sl.registerLazySingleton(() => FeatureFlagProvider()); // Use Cases sl.registerLazySingleton( @@ -33,7 +45,7 @@ void initInjection() { sl.registerLazySingleton(() => FunctionToolsUseCase()); - sl.registerLazySingleton(() => AuthUseCase(repository: sl())); + sl.registerLazySingleton(() => AuthUseCase(repository: sl(), tokenProvider: sl())); // Repositories sl.registerLazySingleton(() => @@ -41,7 +53,7 @@ void initInjection() { sl.registerLazySingleton( () => ChatSessionRepositoryImpl()); sl.registerLazySingleton( - () => AuthRepositoryImpl(client: sl())); + () => AuthRepositoryImpl(client: sl(), authClient: sl())); // Data Sources sl.registerLazySingleton( @@ -51,4 +63,6 @@ void initInjection() { // External sl.registerLazySingleton(() => http.Client()); + sl.registerLazySingleton(() => AuthenticatedHttpClient(sl(), sl()));; + } diff --git a/lib/main.dart b/lib/main.dart index 7ad5454..49e3c8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:swiftcomp/generated/l10n.dart'; -import 'package:swiftcomp/presentation/more/feature_flag_provider.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; +import 'package:swiftcomp/presentation/more/viewModels/more_view_model.dart'; import 'package:swiftcomp/util/NumberPrecisionHelper.dart'; import 'package:swiftcomp/util/in_app_reviewer_helper.dart'; import 'package:swiftcomp/util/others.dart'; @@ -67,6 +68,7 @@ class _MyAppState extends State { providers: [ ChangeNotifierProvider(create: (context) => NumberPrecisionHelper()), ChangeNotifierProvider(create: (context) => FeatureFlagProvider()), + ChangeNotifierProvider(create: (context) => sl()), ], child: MaterialApp( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/bottom_navigator.dart b/lib/presentation/bottom_navigator.dart index 7388c6b..5d05515 100644 --- a/lib/presentation/bottom_navigator.dart +++ b/lib/presentation/bottom_navigator.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:swiftcomp/presentation/more/feature_flag_provider.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; import 'package:swiftcomp/presentation/tools/page/tool_page.dart'; import 'chat/views/chat_screen.dart'; -import 'more/more_page.dart'; +import 'more/views/more_page.dart'; class BottomNavigator extends StatefulWidget { const BottomNavigator({Key? key}) : super(key: key); diff --git a/lib/presentation/more/feature_flag_page.dart b/lib/presentation/more/feature_flag_page.dart deleted file mode 100644 index 9222343..0000000 --- a/lib/presentation/more/feature_flag_page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:swiftcomp/presentation/more/feature_flag_provider.dart'; - -class FeatureFlagPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Feature Flags'), - ), - body: Consumer( - builder: (context, featureFlagProvider, _) { - return ListView( - children: featureFlagProvider.allFeatureFlags().keys.map((feature) { - return SwitchListTile( - title: Text(feature), - value: featureFlagProvider.getFeatureFlag(feature), - onChanged: (bool value) { - featureFlagProvider.toggleFeatureFlag(feature); - }, - ); - }).toList(), - ); - }, - ), - ); - } -} \ No newline at end of file diff --git a/lib/presentation/more/more_page.dart b/lib/presentation/more/more_page.dart deleted file mode 100644 index 58f970a..0000000 --- a/lib/presentation/more/more_page.dart +++ /dev/null @@ -1,404 +0,0 @@ -import 'dart:io'; - -import 'package:amplify_flutter/amplify_flutter.dart'; -import 'package:device_info/device_info.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_progress_hud/flutter_progress_hud.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:launch_review/launch_review.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:provider/provider.dart'; -import 'package:share/share.dart'; -import 'package:swiftcomp/generated/l10n.dart'; -import 'package:swiftcomp/presentation/more/login/login_page.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -import 'NewLogin/views/new_login.dart'; -import 'feature_flag_page.dart'; -import 'feature_flag_provider.dart'; - -class MorePage extends StatefulWidget { - const MorePage({Key? key}) : super(key: key); - - @override - _MorePageState createState() => _MorePageState(); -} - -class _MorePageState extends State - with AutomaticKeepAliveClientMixin { - bool isSignedIn = false; - String _version = ''; - - int _tapCount = 0; - final int _maxTaps = 5; - final int _tapTimeout = 1000; // Timeout in milliseconds - DateTime _lastTapTime = DateTime.now(); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - fetchAuthSession(); - _initPackageInfo(); - } - - Future fetchAuthSession() async { - AuthSession authResult = await Amplify.Auth.fetchAuthSession(); - setState(() { - isSignedIn = authResult.isSignedIn; - }); - } - - Future _initPackageInfo() async { - final PackageInfo info = await PackageInfo.fromPlatform(); - setState(() { - _version = info.version; - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Consumer( - builder: (context, featureFlagProvider, _) { - bool isNewLoginEnabled = featureFlagProvider.getFeatureFlag('NewLogin'); - return Scaffold( - appBar: AppBar( - title: const Text("More"), - ), - body: ProgressHUD( - child: Builder( - builder: (context) => ListView( - children: [ - if (isNewLoginEnabled) - MoreRow( - title: "New Login", - leadingIcon: Icons.person_rounded, - onTap: () async { - String received = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const NewLoginPage())); - if (received == "Log in Success") { - setState(() { - isSignedIn = true; - }); - } - }), - isSignedIn - ? MoreRow( - title: "Logout", - leadingIcon: Icons.person_rounded, - onTap: () async { - showDialog( - context: context, - builder: (BuildContext context1) { - return AlertDialog( - title: const Text( - 'Do you want to sign out?'), - content: null, - actions: [ - TextButton( - onPressed: () => Navigator.pop( - context1, 'Cancel'), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - final progress = - ProgressHUD.of(context); - progress?.show(); - try { - await Amplify.Auth.signOut(); - - progress?.dismiss(); - - setState(() { - isSignedIn = false; - }); - - Fluttertoast.showToast( - msg: "Logged out", - toastLength: - Toast.LENGTH_SHORT, - gravity: - ToastGravity.CENTER, - timeInSecForIosWeb: 2, - backgroundColor: - Colors.black, - textColor: Colors.white, - fontSize: 16.0); - } on AuthException catch (e) { - progress?.dismiss(); - print(e.message); - } - Navigator.pop(context1, 'OK'); - }, - child: const Text('OK'), - ), - ], - ); - }, - ); - }) - : MoreRow( - title: "Login", - leadingIcon: Icons.person_rounded, - onTap: () async { - String received = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const LoginPage())); - if (received == "Log in Success") { - setState(() { - isSignedIn = true; - }); - } - }), - MoreRow( - title: S.of(context).Settings, - leadingIcon: Icons.settings_rounded, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const ToolSettingPage())); - }), - MoreRow( - title: "Feedback", - leadingIcon: Icons.chat_rounded, - onTap: () async { - final DeviceInfoPlugin deviceInfo = - DeviceInfoPlugin(); - - String device; - String systemVersion; - if (Platform.isAndroid) { - AndroidDeviceInfo androidInfo = - await deviceInfo.androidInfo; - device = androidInfo.model; - systemVersion = - androidInfo.version.sdkInt.toString(); - } else if (Platform.isIOS) { - IosDeviceInfo iosInfo = - await deviceInfo.iosInfo; - device = iosInfo.model; - systemVersion = iosInfo.systemVersion; - } else { - device = ""; - systemVersion = ""; - } - - var packageInfo = - await PackageInfo.fromPlatform(); - - String appName = packageInfo.appName; - String version = packageInfo.version; - - final Uri params = Uri( - scheme: 'mailto', - path: 'appsbayarea@gmail.com', - query: - 'subject=$appName Feedback&body=\n\n\nVersion=$version\nDevice=$device\nSystem Version=$systemVersion', //add subject and body here - ); - - var url = params.toString(); - if (await canLaunchUrlString(url)) { - await launchUrlString(url); - } else { - throw 'Could not launch $url'; - } - }, - ), - MoreRow( - title: "Rate this App", - leadingIcon: Icons.thumb_up_rounded, - onTap: () { - LaunchReview.launch( - androidAppId: "com.banghuazhao.swiftcomp", - iOSAppId: "1297825946"); - }, - ), - MoreRow( - title: "Share this App", - leadingIcon: Icons.share_rounded, - onTap: () async { - final Size size = MediaQuery.of(context).size; - var packageInfo = - await PackageInfo.fromPlatform(); - String appName = packageInfo.appName; - if (Platform.isIOS) { - Share.share( - "http://itunes.apple.com/app/id${"1297825946"}", - subject: appName, - sharePositionOrigin: Rect.fromLTRB( - 0, 0, size.width, size.height / 2)); - } else { - Share.share( - "https://play.google.com/store/apps/details?id=" + - "com.banghuazhao.swiftcomp", - subject: appName); - } - }, - ), - if (isSignedIn) - MoreRow( - title: "Delete Current Account", - leadingIcon: Icons.delete_outlined, - onTap: () async { - showDialog( - context: context, - builder: (BuildContext context1) { - return Expanded( - child: AlertDialog( - title: Text('Delete Current Account'), - content: Text( - 'Do you want to delete the current account?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context1).pop(); - }, - child: Text( - 'No', - style: TextStyle( - color: Colors.black), - ), - ), - TextButton( - onPressed: () async { - Navigator.of(context1).pop(); - final progress = - ProgressHUD.of(context); - progress?.show(); - try { - await Amplify.Auth.deleteUser(); - print('Delete user succeeded'); - progress?.dismiss(); - Fluttertoast.showToast( - msg: - "The account is deleted successfully", - toastLength: - Toast.LENGTH_SHORT, - gravity: - ToastGravity.CENTER, - timeInSecForIosWeb: 2, - backgroundColor: - Colors.black, - textColor: Colors.white, - fontSize: 16.0); - setState(() { - isSignedIn = false; - }); - } on Exception catch (e) { - progress?.dismiss(); - Fluttertoast.showToast( - msg: - "Delete account failed", - toastLength: - Toast.LENGTH_SHORT, - gravity: - ToastGravity.CENTER, - timeInSecForIosWeb: 2, - backgroundColor: - Colors.black, - textColor: Colors.white, - fontSize: 16.0); - print( - 'Delete user failed with error: $e'); - } - }, - child: Text( - 'Yes', - style: - TextStyle(color: Colors.red), - ), - ), - ], - ), - ); - }, - ); - }, - ), - GestureDetector( - onTap: _handleTap, - child: Container( - margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), - child: Text("Version $_version"), - ), - ), - ], - )))); - }); - } - - void _handleTap() { - final now = DateTime.now(); - if (now.difference(_lastTapTime).inMilliseconds > _tapTimeout) { - _tapCount = 0; // Reset tap count if taps are not continuous - } - _tapCount++; - _lastTapTime = now; - - if (_tapCount == _maxTaps) { - _tapCount = 0; // Reset tap count after navigating - _navigateToFeatureFlagPage(); - } - } - - void _navigateToFeatureFlagPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => FeatureFlagPage()), - ); - } -} - -class MoreRow extends StatelessWidget { - final IconData leadingIcon; - final IconData trailingIcon; - final String title; - final void Function() onTap; - - MoreRow( - {Key? key, - this.trailingIcon = Icons.chevron_right_rounded, - required this.leadingIcon, - required this.title, - required this.onTap}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), - child: Ink( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(24)), - ), - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - onTap: onTap, - child: ListTile( - leading: Icon(leadingIcon), - trailing: Icon(trailingIcon), - title: Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - ), - ); - } -} diff --git a/lib/presentation/more/feature_flag_provider.dart b/lib/presentation/more/providers/feature_flag_provider.dart similarity index 100% rename from lib/presentation/more/feature_flag_provider.dart rename to lib/presentation/more/providers/feature_flag_provider.dart diff --git a/lib/presentation/more/viewModels/feature_flag_view_model.dart b/lib/presentation/more/viewModels/feature_flag_view_model.dart new file mode 100644 index 0000000..bf5c1ce --- /dev/null +++ b/lib/presentation/more/viewModels/feature_flag_view_model.dart @@ -0,0 +1,21 @@ +// lib/presentation/viewmodels/feature_flag_view_model.dart + +import 'package:flutter/material.dart'; +import '../providers/feature_flag_provider.dart'; + +class FeatureFlagViewModel extends ChangeNotifier { + final FeatureFlagProvider featureFlagProvider; + + FeatureFlagViewModel({required this.featureFlagProvider}); + + Map get featureFlags => featureFlagProvider.allFeatureFlags(); + + bool getFeatureFlag(String key) { + return featureFlagProvider.getFeatureFlag(key); + } + + void toggleFeatureFlag(String key) { + featureFlagProvider.toggleFeatureFlag(key); + notifyListeners(); + } +} diff --git a/lib/presentation/more/NewLogin/viewModels/login_view_model.dart b/lib/presentation/more/viewModels/login_view_model.dart similarity index 100% rename from lib/presentation/more/NewLogin/viewModels/login_view_model.dart rename to lib/presentation/more/viewModels/login_view_model.dart diff --git a/lib/presentation/more/viewModels/more_view_model.dart b/lib/presentation/more/viewModels/more_view_model.dart new file mode 100644 index 0000000..b41b9e7 --- /dev/null +++ b/lib/presentation/more/viewModels/more_view_model.dart @@ -0,0 +1,229 @@ +// lib/presentation/viewmodels/more_view_model.dart + +import 'dart:io'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:device_info/device_info.dart'; +import 'package:domain/entities/user.dart'; +import 'package:domain/usecases/auth_usecase.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:launch_review/launch_review.dart'; +import 'package:share/share.dart'; + +class MoreViewModel extends ChangeNotifier { + final AuthUseCase authUseCase; + final FeatureFlagProvider featureFlagProvider; + + bool isNewLoginEnabled = false; + bool isLoggedIn = false; + bool isSignedIn = false; + String version = ''; + User? user; + + int _tapCount = 0; + final int _maxTaps = 5; + final int _tapTimeout = 1000; // Timeout in milliseconds + DateTime _lastTapTime = DateTime.now(); + + MoreViewModel( + {required this.authUseCase, required this.featureFlagProvider}) { + fetchAuthSession(); + initPackageInfo(); + fetchFeatureFlags(); + } + + Future fetchAuthSession() async { + try { + AuthSession authResult = await Amplify.Auth.fetchAuthSession(); + isSignedIn = authResult.isSignedIn; + + notifyListeners(); + } catch (e) { + print("Failed to fetch auth session: $e"); + } + } + + Future fetchAuthSessionNew() async { + try { + isLoggedIn = await authUseCase.isLoggedIn(); + notifyListeners(); + if (isLoggedIn) { + fetchUser(); + } + } catch (e) { + if (kDebugMode) { + print(e); + } + isLoggedIn = false; + } + } + + Future fetchUser() async { + user = User(email: "email"); + notifyListeners(); + } + + void fetchFeatureFlags() { + isNewLoginEnabled = featureFlagProvider.getFeatureFlag('NewLogin'); + notifyListeners(); + + featureFlagProvider.addListener(() { + final newFlagStatus = featureFlagProvider.getFeatureFlag('NewLogin'); + if (newFlagStatus != isNewLoginEnabled) { + isNewLoginEnabled = newFlagStatus; + notifyListeners(); + } + }); + } + + Future initPackageInfo() async { + try { + final PackageInfo info = await PackageInfo.fromPlatform(); + version = info.version; + notifyListeners(); + } catch (e) { + print("Failed to fetch package info: $e"); + } + } + + Future newLogout(BuildContext context) async { + try { + await authUseCase.logout(); + Fluttertoast.showToast( + msg: "Logged out", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + isLoggedIn = false; + notifyListeners(); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + } + + Future logout(BuildContext context) async { + try { + await Amplify.Auth.signOut(); + isSignedIn = false; + Fluttertoast.showToast( + msg: "Logged out", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + notifyListeners(); + } catch (e) { + print("Logout failed: $e"); + } + } + + Future deleteAccount(BuildContext context) async { + try { + await Amplify.Auth.deleteUser(); + isSignedIn = false; + Fluttertoast.showToast( + msg: "Account deleted successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + notifyListeners(); + } catch (e) { + print("Account deletion failed: $e"); + Fluttertoast.showToast( + msg: "Delete account failed", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } + } + + Future openFeedback() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + String device; + String systemVersion; + + if (Platform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + device = androidInfo.model; + systemVersion = androidInfo.version.sdkInt.toString(); + } else if (Platform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + device = iosInfo.model; + systemVersion = iosInfo.systemVersion; + } else { + device = ""; + systemVersion = ""; + } + + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final String appName = packageInfo.appName; + final String version = packageInfo.version; + + final Uri params = Uri( + scheme: 'mailto', + path: 'appsbayarea@gmail.com', + query: + 'subject=$appName Feedback&body=\n\n\nVersion=$version\nDevice=$device\nSystem Version=$systemVersion', + ); + + final url = params.toString(); + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + } else { + print("Could not launch feedback URL"); + } + } + + void rateApp() { + LaunchReview.launch( + androidAppId: "com.banghuazhao.swiftcomp", iOSAppId: "1297825946"); + } + + Future shareApp(BuildContext context) async { + final packageInfo = await PackageInfo.fromPlatform(); + final String appName = packageInfo.appName; + final Size size = MediaQuery.of(context).size; + + if (Platform.isIOS) { + Share.share("http://itunes.apple.com/app/id1297825946", + subject: appName, + sharePositionOrigin: + Rect.fromLTRB(0, 0, size.width, size.height / 2)); + } else { + Share.share( + "https://play.google.com/store/apps/details?id=com.banghuazhao.swiftcomp", + subject: appName); + } + } + + void handleTap(Function navigateToFeatureFlagPage) { + final now = DateTime.now(); + if (now.difference(_lastTapTime).inMilliseconds > _tapTimeout) { + _tapCount = 0; // Reset tap count if taps are not continuous + } + _tapCount++; + _lastTapTime = now; + + if (_tapCount == _maxTaps) { + _tapCount = 0; // Reset tap count after navigating + navigateToFeatureFlagPage(); + } + } +} diff --git a/lib/presentation/more/NewLogin/viewModels/signup_view_model.dart b/lib/presentation/more/viewModels/signup_view_model.dart similarity index 100% rename from lib/presentation/more/NewLogin/viewModels/signup_view_model.dart rename to lib/presentation/more/viewModels/signup_view_model.dart diff --git a/lib/presentation/more/views/feature_flag_page.dart b/lib/presentation/more/views/feature_flag_page.dart new file mode 100644 index 0000000..b82f42f --- /dev/null +++ b/lib/presentation/more/views/feature_flag_page.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; + +import '../../../injection_container.dart'; +import '../viewModels/feature_flag_view_model.dart'; + +class FeatureFlagPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => sl(), + child: Consumer(builder: (context, viewModel, _) { + return Scaffold( + appBar: AppBar( + title: Text('Feature Flags'), + ), + body: ListView( + children: viewModel.featureFlags.keys.map((feature) { + return SwitchListTile( + title: Text(feature), + value: viewModel.getFeatureFlag(feature), + onChanged: (bool value) { + viewModel.toggleFeatureFlag(feature); + }, + ); + }).toList(), + )); + })); + } +} diff --git a/lib/presentation/more/views/more_page.dart b/lib/presentation/more/views/more_page.dart new file mode 100644 index 0000000..0a7c7f1 --- /dev/null +++ b/lib/presentation/more/views/more_page.dart @@ -0,0 +1,179 @@ +// lib/presentation/pages/more_page.dart + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_progress_hud/flutter_progress_hud.dart'; +import 'package:swiftcomp/presentation/more/login/login_page.dart'; +import '../../../injection_container.dart'; +import '../viewModels/more_view_model.dart'; +import 'feature_flag_page.dart'; +import 'new_login.dart'; +import 'tool_setting_page.dart'; + +class MorePage extends StatefulWidget { + const MorePage({Key? key}) : super(key: key); + + @override + _MorePageState createState() => _MorePageState(); +} + +class _MorePageState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + print("didChangeDependencies"); + final viewModel = Provider.of(context, listen: false); + viewModel.fetchAuthSessionNew(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, _) { + return Scaffold( + appBar: AppBar(title: const Text("More")), + body: ProgressHUD( + child: Builder( + builder: (context) => ListView( + children: [ + if (viewModel.isNewLoginEnabled && !viewModel.isLoggedIn) + MoreRow( + title: "New Login", + leadingIcon: Icons.person_rounded, + onTap: () async { + String result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NewLoginPage())); + if (result == "Log in Success") { + viewModel.fetchAuthSessionNew(); + } + }, + ), + if (viewModel.isNewLoginEnabled && viewModel.isLoggedIn) + ListTile( + leading: Icon(Icons.account_circle, size: 40), + title: Text( + viewModel.user?.email ?? "", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.bold), + ), + subtitle: Text("View Profile"), + onTap: () { + // Optional: Navigate to Profile Page or Show Profile Options + }, + ), + if (viewModel.isNewLoginEnabled && viewModel.isLoggedIn) + MoreRow( + title: "New Logout", + leadingIcon: Icons.person_rounded, + onTap: () async => viewModel.newLogout(context)), + MoreRow( + title: viewModel.isSignedIn ? "Logout" : "Login", + leadingIcon: Icons.person_rounded, + onTap: viewModel.isSignedIn + ? () => viewModel.logout(context) + : () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LoginPage())); + if (result == "Log in Success") { + viewModel.fetchAuthSession(); + } + }, + ), + MoreRow( + title: "Settings", + leadingIcon: Icons.settings_rounded, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ToolSettingPage()), + ), + ), + MoreRow( + title: "Feedback", + leadingIcon: Icons.chat_rounded, + onTap: viewModel.openFeedback, + ), + MoreRow( + title: "Rate this App", + leadingIcon: Icons.thumb_up_rounded, + onTap: viewModel.rateApp, + ), + MoreRow( + title: "Share this App", + leadingIcon: Icons.share_rounded, + onTap: () => viewModel.shareApp(context), + ), + if (viewModel.isSignedIn) + MoreRow( + title: "Delete Current Account", + leadingIcon: Icons.delete_outlined, + onTap: () => viewModel.deleteAccount(context), + ), + GestureDetector( + onTap: () => viewModel.handleTap( + () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FeatureFlagPage()), + ), + ), + child: Container( + margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Text("Version ${viewModel.version}"), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class MoreRow extends StatelessWidget { + final IconData leadingIcon; + final IconData trailingIcon; + final String title; + final void Function() onTap; + + MoreRow( + {Key? key, + this.trailingIcon = Icons.chevron_right_rounded, + required this.leadingIcon, + required this.title, + required this.onTap}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Ink( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(24)), + ), + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + onTap: onTap, + child: ListTile( + leading: Icon(leadingIcon), + trailing: Icon(trailingIcon), + title: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/more/NewLogin/views/new_login.dart b/lib/presentation/more/views/new_login.dart similarity index 98% rename from lib/presentation/more/NewLogin/views/new_login.dart rename to lib/presentation/more/views/new_login.dart index 98f498e..0487ddd 100644 --- a/lib/presentation/more/NewLogin/views/new_login.dart +++ b/lib/presentation/more/views/new_login.dart @@ -2,7 +2,7 @@ import 'package:domain/entities/user.dart'; import 'package:domain/usecases/auth_usecase.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:swiftcomp/presentation/more/NewLogin/views/sigup_page.dart'; +import 'package:swiftcomp/presentation/more/views/sigup_page.dart'; import '../../../../injection_container.dart'; import '../viewModels/login_view_model.dart'; diff --git a/lib/presentation/more/NewLogin/views/sigup_page.dart b/lib/presentation/more/views/sigup_page.dart similarity index 100% rename from lib/presentation/more/NewLogin/views/sigup_page.dart rename to lib/presentation/more/views/sigup_page.dart diff --git a/lib/presentation/more/tool_setting_page.dart b/lib/presentation/more/views/tool_setting_page.dart similarity index 100% rename from lib/presentation/more/tool_setting_page.dart rename to lib/presentation/more/views/tool_setting_page.dart diff --git a/lib/presentation/tools/page/UDFRC_rules_of_mixture_result_page.dart b/lib/presentation/tools/page/UDFRC_rules_of_mixture_result_page.dart index a304927..3db2a52 100644 --- a/lib/presentation/tools/page/UDFRC_rules_of_mixture_result_page.dart +++ b/lib/presentation/tools/page/UDFRC_rules_of_mixture_result_page.dart @@ -5,7 +5,7 @@ import 'package:swiftcomp/generated/l10n.dart'; import 'package:swiftcomp/presentation/tools/model/material_model.dart'; import 'package:swiftcomp/presentation/tools/widget/orthotropic_properties_widget.dart'; import 'package:swiftcomp/presentation/tools/widget/result_6by6_matrix.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; class RulesOfMixtureResultPage extends StatefulWidget { final UDFRCRulesOfMixtureOutput output; diff --git a/lib/presentation/tools/page/lamina_engineering_constants_result_page.dart b/lib/presentation/tools/page/lamina_engineering_constants_result_page.dart index 1d4a2d7..323aba4 100644 --- a/lib/presentation/tools/page/lamina_engineering_constants_result_page.dart +++ b/lib/presentation/tools/page/lamina_engineering_constants_result_page.dart @@ -10,7 +10,7 @@ import 'package:provider/provider.dart'; import 'package:swiftcomp/generated/l10n.dart'; import 'package:swiftcomp/presentation/tools/model/material_model.dart'; import 'package:swiftcomp/presentation/tools/widget/result_3by3_matrix.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; import 'package:swiftcomp/util/NumberPrecisionHelper.dart'; import 'package:swiftcomp/util/number.dart'; diff --git a/lib/presentation/tools/page/lamina_stress_strain_result_page.dart b/lib/presentation/tools/page/lamina_stress_strain_result_page.dart index fc29245..b82557a 100644 --- a/lib/presentation/tools/page/lamina_stress_strain_result_page.dart +++ b/lib/presentation/tools/page/lamina_stress_strain_result_page.dart @@ -3,7 +3,7 @@ import 'package:composite_calculator/models/tensor_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:swiftcomp/generated/l10n.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; import '../widget/result_3by3_matrix.dart'; diff --git a/lib/presentation/tools/page/laminate_3d_properties_result_page.dart b/lib/presentation/tools/page/laminate_3d_properties_result_page.dart index e3d4fd9..2869130 100644 --- a/lib/presentation/tools/page/laminate_3d_properties_result_page.dart +++ b/lib/presentation/tools/page/laminate_3d_properties_result_page.dart @@ -5,7 +5,7 @@ import 'package:swiftcomp/generated/l10n.dart'; import 'package:swiftcomp/presentation/tools/model/material_model.dart'; import 'package:swiftcomp/presentation/tools/widget/orthotropic_properties_widget.dart'; import 'package:swiftcomp/presentation/tools/widget/result_6by6_matrix.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; class Laminate3DPropertiesResultPage extends StatefulWidget { final Laminate3DPropertiesOutput output; diff --git a/lib/presentation/tools/page/laminate_plate_properties_result_page.dart b/lib/presentation/tools/page/laminate_plate_properties_result_page.dart index a6ee556..37bfe5f 100644 --- a/lib/presentation/tools/page/laminate_plate_properties_result_page.dart +++ b/lib/presentation/tools/page/laminate_plate_properties_result_page.dart @@ -5,7 +5,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:provider/provider.dart'; import 'package:swiftcomp/generated/l10n.dart'; import 'package:swiftcomp/presentation/tools/widget/result_3by3_matrix.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; import 'package:swiftcomp/util/NumberPrecisionHelper.dart'; class LaminatePlatePropertiesResultPage extends StatefulWidget { diff --git a/lib/presentation/tools/page/laminate_stress_strain_result_page.dart b/lib/presentation/tools/page/laminate_stress_strain_result_page.dart index d4d982d..6810a5e 100644 --- a/lib/presentation/tools/page/laminate_stress_strain_result_page.dart +++ b/lib/presentation/tools/page/laminate_stress_strain_result_page.dart @@ -8,7 +8,7 @@ import 'package:linalg/matrix.dart'; import 'package:provider/provider.dart'; import 'package:swiftcomp/generated/l10n.dart'; import 'package:swiftcomp/presentation/tools/model/mechanical_tensor_model.dart'; -import 'package:swiftcomp/presentation/more/tool_setting_page.dart'; +import 'package:swiftcomp/presentation/more/views/tool_setting_page.dart'; import 'package:swiftcomp/util/NumberPrecisionHelper.dart'; class LaminateStressStrainResultPage extends StatefulWidget { diff --git a/test/feature_flag_provider_test.dart b/test/feature_flag_provider_test.dart index aeb30b3..a7573d2 100644 --- a/test/feature_flag_provider_test.dart +++ b/test/feature_flag_provider_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:swiftcomp/presentation/more/feature_flag_provider.dart'; +import 'package:swiftcomp/presentation/more/providers/feature_flag_provider.dart'; void main() { group('FeatureFlagProvider Tests', () {