diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d6001ca..e0cccd4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,16 @@ + + + + + + + + + + + + + + diff --git a/assets/images/github-logo.svg b/assets/images/github-logo.svg new file mode 100644 index 0000000..e8dd199 --- /dev/null +++ b/assets/images/github-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/google-logo.svg b/assets/images/google-logo.svg new file mode 100644 index 0000000..585aedd --- /dev/null +++ b/assets/images/google-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/constants/api_path.dart b/lib/constants/api_path.dart deleted file mode 100644 index 879df64..0000000 --- a/lib/constants/api_path.dart +++ /dev/null @@ -1 +0,0 @@ -const thingsBoardApiEndpoint = 'http://localhost:8080'; diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index e69de29..32b8214 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -0,0 +1,4 @@ +abstract class ThingsboardAppConstants { + static final thingsBoardApiEndpoint = 'http://localhost:8080'; + static final thingsboardOAuth2CallbackUrlScheme = 'org.thingsboard.app.auth'; +} diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart index e43bbf2..a65877b 100644 --- a/lib/constants/assets_path.dart +++ b/lib/constants/assets_path.dart @@ -5,4 +5,11 @@ abstract class ThingsboardImage { static final thingsboardCenter = 'assets/images/thingsboard_center.svg'; static final dashboardPlaceholder = 'assets/images/dashboard-placeholder.png'; static final deviceProfilePlaceholder = 'assets/images/device-profile-placeholder.png'; + + static final oauth2Logos = { + 'google-logo': 'assets/images/google-logo.svg', + 'github-logo': 'assets/images/github-logo.svg', + 'facebook-logo': 'assets/images/facebook-logo.svg' + }; + } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index c4c13a9..1940d8d 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -1,14 +1,17 @@ import 'dart:ui'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_web_auth/flutter_web_auth.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/constants/assets_path.dart'; -import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; - import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; class LoginPage extends TbPageWidget { @@ -22,6 +25,7 @@ class LoginPage extends TbPageWidget { class _LoginPageState extends TbPageState { final _isLoginNotifier = ValueNotifier(false); + final _showPasswordNotifier = ValueNotifier(false); final usernameController = TextEditingController(); final passwordController = TextEditingController(); @@ -40,91 +44,199 @@ class _LoginPageState extends TbPageState { @override Widget build(BuildContext context) { + final ButtonStyle oauth2ButtonStyle = + ElevatedButton.styleFrom(primary: Theme.of(context).secondaryHeaderColor, + onPrimary: Theme.of(context).colorScheme.onSurface); return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text('Login to ThingsBoard'), - ), - body: ValueListenableBuilder( + resizeToAvoidBottomInset: false, + body: ValueListenableBuilder( valueListenable: _isLoginNotifier, builder: (BuildContext context, bool loading, child) { List children = [ - SingleChildScrollView( + LoginPageBackground(), + Padding( + padding: EdgeInsets.fromLTRB(28, 71, 28, 28), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.only(top: 60.0), - child: Center( - child: Container( - width: 300, - height: 150, - child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, - color: Theme.of(context).primaryColor, - semanticsLabel: 'ThingsBoard Logo') - ) + Row( + children: [ + SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, + height: 25, + color: Theme.of(context).primaryColor, + semanticsLabel: 'ThingsBoard Logo') + ] + ), + Container(height: 32), + Row( + children: [ + Text( + 'Login to your account', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 32, + height: 40 / 32 ) + )] ), - Padding( - //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0), - padding: EdgeInsets.symmetric(horizontal: 15), - child: TextField( - enabled: !loading, - controller: usernameController, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Username (email)', - hintText: 'Enter valid email id as abc@gmail.com'), + Container(height: tbContext.hasOAuthClients ? 24 : 48), + if (tbContext.hasOAuthClients) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: tbContext.oauth2Clients!.map((client) { + Widget? icon; + if (client.icon != null) { + if (ThingsboardImage.oauth2Logos.containsKey(client.icon)) { + icon = SvgPicture.asset(ThingsboardImage.oauth2Logos[client.icon]!, + height: 24); + } else { + String strIcon = client.icon!; + if (strIcon.startsWith('mdi:')) { + strIcon = strIcon.substring(4); + } + var iconData = MdiIcons.fromString(strIcon); + if (iconData != null) { + icon = Icon(iconData, size: 24, color: Theme.of(context).primaryColor); + } + } + } + if (icon == null) { + icon = Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); + } + return ElevatedButton.icon( + style: oauth2ButtonStyle, + onPressed: () async { + _isLoginNotifier.value = true; + var url = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + client.url); + var params = Map.from(url.queryParameters); + params['pkg'] = tbContext.packageName; + url = url.replace(queryParameters: params); + try { + final result = await FlutterWebAuth.authenticate( + url: url.toString(), + callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme); + final resultUri = Uri.parse(result); + final error = resultUri.queryParameters['error']; + if (error != null) { + _isLoginNotifier.value = false; + showErrorNotification(error); + } else { + final accessToken = resultUri.queryParameters['accessToken']; + final refreshToken = resultUri.queryParameters['refreshToken']; + if (accessToken != null && refreshToken != null) { + await tbClient.setUserFromJwtToken(accessToken, refreshToken, true); + } + } + log.debug('result = $result'); + } catch (e) { + log.error('Auth Error:', e); + _isLoginNotifier.value = false; + } + }, + icon: icon, + label: Text('Login with ${client.name}')); + }).toList(), ), + if (tbContext.hasOAuthClients) + Padding(padding: EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Flexible(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text('OR'), + ), + Flexible(child: Divider()) + ], + ) ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 15.0, top: 15, bottom: 0), - //padding: EdgeInsets.symmetric(horizontal: 15), - child: TextField( - enabled: !loading, - controller: passwordController, - obscureText: true, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Password', - hintText: 'Enter secure password'), - ), + TextField( + enabled: !loading, + controller: usernameController, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Email', + hintText: 'Enter valid email id as abc@gmail.com'), ), - TextButton( - onPressed: loading ? null : () { - //TODO FORGOT PASSWORD SCREEN GOES HERE + Container(height: 28), + ValueListenableBuilder( + valueListenable: _showPasswordNotifier, + builder: (BuildContext context, bool showPassword, child) { + return TextField( + enabled: !loading, + controller: passwordController, + obscureText: !showPassword, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off), + onPressed: loading ? null : () { + _showPasswordNotifier.value = !_showPasswordNotifier.value; + }, + ), + border: OutlineInputBorder(), + labelText: 'Password', + hintText: 'Enter secure password'), + ); + } + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: loading ? null : () { + //TODO FORGOT PASSWORD SCREEN GOES HERE + }, + child: Text( + 'Forgot Password?', + style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, + letterSpacing: 1, + fontSize: 12, + height: 16 / 12), + ), + ) + ], + ), + Spacer(), + ElevatedButton( + child: Text('Log In'), + style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), + onPressed: loading ? null : () async { + _isLoginNotifier.value = true; + try { + await tbClient.login( + LoginRequest(usernameController.text, + passwordController.text)); + } catch (e) { + _isLoginNotifier.value = false; + } }, - child: Text( - 'Forgot Password?', - style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, fontSize: 15), - ), ), Container( - height: 50, - width: 250, - decoration: BoxDecoration( - color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4)), - child: TextButton( - onPressed: loading ? null : () async { - _isLoginNotifier.value = true; - try { - await tbClient.login( - LoginRequest(usernameController.text, - passwordController.text)); - } catch (e) { - _isLoginNotifier.value = false; - } - }, - child: Text( - 'Login', - style: TextStyle(color: Colors.white, fontSize: 25), - ), - ), + height: 24, ), - SizedBox( - height: 130, - ), - Text('New User? Create Account') + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('New User?', + style: TextStyle( + fontSize: 14, + height: 14 / 20 + )), + TextButton( + onPressed: loading ? null : () { + //TODO CREATE ACCOUNT SCREEN GOES HERE + }, + child: Text( + 'Create Account', + style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, + letterSpacing: 1, + fontSize: 14, + height: 14 / 20), + ), + ) + ], + ) ] ) ) @@ -160,3 +272,47 @@ class _LoginPageState extends TbPageState { ); } } + +class LoginPageBackground extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: CustomPaint( + painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor), + ) + ); + } + +} + +class _LoginPageBackgroundPainter extends CustomPainter { + + final Color color; + + const _LoginPageBackgroundPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color.withAlpha(14); + paint.style = PaintingStyle.fill; + var topPath = Path(); + topPath.moveTo(0, 0); + topPath.lineTo(size.width / 2, 0); + topPath.lineTo(0, size.height / 10); + topPath.close(); + canvas.drawPath(topPath, paint); + var bottomPath = Path(); + bottomPath.moveTo(0, size.height * 0.98); + bottomPath.lineTo(size.width, size.height * 0.78); + bottomPath.lineTo(size.width, size.height); + bottomPath.lineTo(0, size.height); + bottomPath.close(); + canvas.drawPath(bottomPath, paint); + } + + @override + bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) { + return color != oldDelegate.color; + } +} diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 13817ef..bd73e6e 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -6,11 +6,12 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; +import 'package:package_info/package_info.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/modules/main/main_page.dart'; import 'package:thingsboard_app/utils/services/widget_action_handler.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; import 'package:thingsboard_app/utils/services/tb_secure_storage.dart'; -import 'package:thingsboard_app/constants/api_path.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; enum NotificationType { @@ -106,6 +107,7 @@ class TbContext { bool _initialized = false; bool isUserLoaded = false; final ValueNotifier _isAuthenticated = ValueNotifier(false); + List? oauth2Clients; User? userDetails; HomeDashboardInfo? homeDashboard; final _isLoadingNotifier = ValueNotifier(false); @@ -113,6 +115,7 @@ class TbContext { late final _widgetActionHandler; late final AndroidDeviceInfo? _androidInfo; late final IosDeviceInfo? _iosInfo; + late final String packageName; TbMainDashboardHolder? _mainDashboardHolder; GlobalKey messengerKey = GlobalKey(); @@ -138,7 +141,7 @@ class TbContext { return true; }()); _initialized = true; - tbClient = ThingsboardClient(thingsBoardApiEndpoint, + tbClient = ThingsboardClient(ThingsboardAppConstants.thingsBoardApiEndpoint, storage: TbSecureStorage(), onUserLoaded: onUserLoaded, onError: onError, @@ -151,6 +154,8 @@ class TbContext { } else if (Platform.isIOS) { _iosInfo = await deviceInfoPlugin.iosInfo; } + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + packageName = packageInfo.packageName; await tbClient.init(); } catch (e, s) { log.error('Failed to init tbContext: $e', e, s); @@ -253,6 +258,7 @@ class TbContext { } else { userDetails = null; homeDashboard = null; + oauth2Clients = await tbClient.getOAuth2Service().getOAuth2Clients(pkgName: packageName); } await updateRouteState(); @@ -265,6 +271,8 @@ class TbContext { bool get isAuthenticated => _isAuthenticated.value; + bool get hasOAuthClients => oauth2Clients != null && oauth2Clients!.isNotEmpty; + Future updateRouteState() async { if (currentState != null) { if (tbClient.isAuthenticated()) { @@ -314,6 +322,17 @@ class TbContext { } } + String userAgent() { + String userAgent = 'Mozilla/5.0'; + if (Platform.isAndroid) { + userAgent += ' (Linux; Android ${_androidInfo!.version.release}; ${_androidInfo!.model})'; + } else if (Platform.isIOS) { + userAgent += ' (${_iosInfo!.model})'; + } + userAgent += ' AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36'; + return userAgent; + } + bool isHomePage() { if (currentState != null) { if (currentState is TbMainState) { @@ -440,7 +459,7 @@ mixin HasTbContext { Future navigateTo(String path, {bool replace = false, bool clearStack = false}) => _tbContext.navigateTo(path, replace: replace, clearStack: clearStack); - void pop([T? result]) => _tbContext.pop(result); + void pop([T? result, BuildContext? context]) => _tbContext.pop(result, context); Future maybePop([ T? result ]) => _tbContext.maybePop(result); diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 3bb85ac..a310c76 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:thingsboard_app/constants/api_path.dart'; +import 'package:thingsboard_app/constants/app_constants.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; @@ -135,7 +135,7 @@ class _DashboardState extends TbContextState { void _onAuthenticated() async { if (tbContext.isAuthenticated) { if (!readyState.value) { - _initialUrl = Uri.parse(thingsBoardApiEndpoint + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}'); + _initialUrl = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}'); readyState.value = true; } else { var windowMessage = { diff --git a/pubspec.lock b/pubspec.lock index f40d37b..c7cdad9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -172,6 +172,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth: + dependency: "direct main" + description: + name: flutter_web_auth + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" flutter_web_plugins: dependency: transitive description: flutter @@ -225,7 +232,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.0+1" + version: "0.8.0+3" image_picker_for_web: dependency: transitive description: @@ -282,6 +289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.10" + material_design_icons_flutter: + dependency: "direct main" + description: + name: material_design_icons_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.5955-rc.1" meta: dependency: transitive description: @@ -296,6 +310,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + package_info: + dependency: "direct main" + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path: dependency: transitive description: @@ -411,7 +432,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0fbaccafa7c0b3b3a6c9ac689c5949164101c5b5" + resolved-ref: "00f08109b44b926ab6defaed12b8d4fdc44e07b0" url: "git@github.com:thingsboard/dart_thingsboard_client.git" source: git version: "1.0.0" @@ -477,7 +498,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.1.2" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 154ed6a..469bb27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,9 @@ dependencies: qr_code_scanner: ^0.5.1 device_info: ^2.0.0 geolocator: ^7.0.3 + material_design_icons_flutter: ^5.0.5955-rc.1 + flutter_web_auth: ^0.3.0 + package_info: ^2.0.2 dev_dependencies: flutter_test: