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: