Add platform type filter to oauth2 clients request. Improve login page.

This commit is contained in:
Igor Kulikov
2021-06-10 19:03:30 +03:00
parent 27013f88e7
commit c3c5b7f0c2
3 changed files with 255 additions and 189 deletions

View File

@@ -24,6 +24,12 @@ class LoginPage extends TbPageWidget<LoginPage, _LoginPageState> {
class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> { class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
final ButtonStyle _oauth2ButtonWithTextStyle =
OutlinedButton.styleFrom(alignment: Alignment.centerLeft, primary: Colors.black87);
final ButtonStyle _oauth2IconButtonStyle =
OutlinedButton.styleFrom(alignment: Alignment.center);
final _isLoginNotifier = ValueNotifier<bool>(false); final _isLoginNotifier = ValueNotifier<bool>(false);
final _showPasswordNotifier = ValueNotifier<bool>(false); final _showPasswordNotifier = ValueNotifier<bool>(false);
@@ -44,9 +50,6 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ButtonStyle oauth2ButtonStyle =
ElevatedButton.styleFrom(primary: Theme.of(context).secondaryHeaderColor,
onPrimary: Theme.of(context).colorScheme.onSurface);
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@@ -55,190 +58,148 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
builder: (BuildContext context, bool loading, child) { builder: (BuildContext context, bool loading, child) {
List<Widget> children = [ List<Widget> children = [
LoginPageBackground(), LoginPageBackground(),
Padding( Positioned.fill(
padding: EdgeInsets.fromLTRB(28, 71, 28, 28), child: LayoutBuilder(
child: Column( builder: (context, constraints) {
crossAxisAlignment: CrossAxisAlignment.stretch, return SingleChildScrollView(
children: [ padding: EdgeInsets.fromLTRB(28, 71, 28, 28),
Row( child: ConstrainedBox(
children: [ constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 28)),
SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, child: IntrinsicHeight(
height: 25, child: Column(
color: Theme.of(context).primaryColor, crossAxisAlignment: CrossAxisAlignment.stretch,
semanticsLabel: 'ThingsBoard Logo') children: [
] Row(
), children: [
Container(height: 32), SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
Row( height: 25,
children: [ color: Theme.of(context).primaryColor,
Text( semanticsLabel: 'ThingsBoard Logo')
'Login to your account', ]
style: TextStyle( ),
fontWeight: FontWeight.bold, Container(height: 32),
fontSize: 32, Row(
height: 40 / 32 children: [
) Text(
)] 'Login to your account',
), style: TextStyle(
Container(height: tbContext.hasOAuthClients ? 24 : 48), fontWeight: FontWeight.bold,
if (tbContext.hasOAuthClients) fontSize: 28,
Column( height: 36 / 28
crossAxisAlignment: CrossAxisAlignment.stretch, )
children: tbContext.oauth2Clients!.map((client) { )]
Widget? icon; ),
if (client.icon != null) { Container(height: 48),
if (ThingsboardImage.oauth2Logos.containsKey(client.icon)) { if (tbContext.hasOAuthClients)
icon = SvgPicture.asset(ThingsboardImage.oauth2Logos[client.icon]!, _buildOAuth2Buttons(tbContext.oauth2Clients!),
height: 24); if (tbContext.hasOAuthClients)
} else { Padding(padding: EdgeInsets.only(top: 10, bottom: 16),
String strIcon = client.icon!; child: Row(
if (strIcon.startsWith('mdi:')) { children: [
strIcon = strIcon.substring(4); Flexible(child: Divider()),
} Padding(
var iconData = MdiIcons.fromString(strIcon); padding: EdgeInsets.symmetric(horizontal: 16),
if (iconData != null) { child: Text('OR'),
icon = Icon(iconData, size: 24, color: Theme.of(context).primaryColor); ),
} Flexible(child: Divider())
} ],
} )
if (icon == null) { ),
icon = Icon(Icons.login, size: 24, color: Theme.of(context).primaryColor); TextField(
} enabled: !loading,
return ElevatedButton.icon( controller: usernameController,
style: oauth2ButtonStyle, decoration: InputDecoration(
onPressed: () async { border: OutlineInputBorder(),
_isLoginNotifier.value = true; labelText: 'Email',
var url = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + client.url); hintText: 'Enter valid email id as abc@gmail.com'),
var params = Map<String,String>.from(url.queryParameters); ),
params['pkg'] = tbContext.packageName; Container(height: 28),
url = url.replace(queryParameters: params); ValueListenableBuilder(
try { valueListenable: _showPasswordNotifier,
final result = await FlutterWebAuth.authenticate( builder: (BuildContext context, bool showPassword, child) {
url: url.toString(), return TextField(
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme); enabled: !loading,
final resultUri = Uri.parse(result); controller: passwordController,
final error = resultUri.queryParameters['error']; obscureText: !showPassword,
if (error != null) { decoration: InputDecoration(
_isLoginNotifier.value = false; suffixIcon: IconButton(
showErrorNotification(error); icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
} else { onPressed: loading ? null : () {
final accessToken = resultUri.queryParameters['accessToken']; _showPasswordNotifier.value = !_showPasswordNotifier.value;
final refreshToken = resultUri.queryParameters['refreshToken']; },
if (accessToken != null && refreshToken != null) { ),
await tbClient.setUserFromJwtToken(accessToken, refreshToken, true); border: OutlineInputBorder(),
} labelText: 'Password',
} hintText: 'Enter secure password'),
log.debug('result = $result'); );
} catch (e) { }
log.error('Auth Error:', e); ),
_isLoginNotifier.value = false; Row(
} mainAxisAlignment: MainAxisAlignment.end,
}, children: [
icon: icon, TextButton(
label: Text('Login with ${client.name}')); onPressed: loading ? null : () {
}).toList(), //TODO FORGOT PASSWORD SCREEN GOES HERE
), },
if (tbContext.hasOAuthClients) child: Text(
Padding(padding: EdgeInsets.symmetric(vertical: 16), 'Forgot Password?',
child: Row( style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary,
children: [ letterSpacing: 1,
Flexible(child: Divider()), fontSize: 12,
Padding( height: 16 / 12),
padding: EdgeInsets.symmetric(horizontal: 16), ),
child: Text('OR'), )
), ],
Flexible(child: Divider()) ),
], Spacer(),
) ElevatedButton(
), child: Text('Log In'),
TextField( style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
enabled: !loading, onPressed: loading ? null : () async {
controller: usernameController, _isLoginNotifier.value = true;
decoration: InputDecoration( try {
border: OutlineInputBorder(), await tbClient.login(
labelText: 'Email', LoginRequest(usernameController.text,
hintText: 'Enter valid email id as abc@gmail.com'), passwordController.text));
), } catch (e) {
Container(height: 28), _isLoginNotifier.value = false;
ValueListenableBuilder( }
valueListenable: _showPasswordNotifier, },
builder: (BuildContext context, bool showPassword, child) { ),
return TextField( Container(
enabled: !loading, height: 24,
controller: passwordController, ),
obscureText: !showPassword, Row(
decoration: InputDecoration( mainAxisAlignment: MainAxisAlignment.center,
suffixIcon: IconButton( children: [
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off), Text('New User?',
onPressed: loading ? null : () { style: TextStyle(
_showPasswordNotifier.value = !_showPasswordNotifier.value; fontSize: 14,
}, height: 14 / 20
), )),
border: OutlineInputBorder(), TextButton(
labelText: 'Password', onPressed: loading ? null : () {
hintText: 'Enter secure password'), //TODO CREATE ACCOUNT SCREEN GOES HERE
); },
} child: Text(
), 'Create Account',
Row( style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary,
mainAxisAlignment: MainAxisAlignment.end, letterSpacing: 1,
children: [ fontSize: 14,
TextButton( height: 14 / 20),
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;
}
},
),
Container(
height: 24,
),
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),
),
)
],
)
]
)
) )
]; ];
if (loading) { if (loading) {
@@ -271,6 +232,108 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
}) })
); );
} }
Widget _buildOAuth2Buttons(List<OAuth2ClientInfo> clients) {
if (clients.length == 1 || clients.length > 6) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: clients.asMap().map((index, client) =>
MapEntry(index, _buildOAuth2Button(client, 'Login with ${client.name}', false, index == clients.length - 1))).values.toList()
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: Text('LOGIN WITH')),
),
Row(
children: clients.asMap().map((index, client) =>
MapEntry(index, _buildOAuth2Button(client, clients.length == 2 ? client.name : null, true, index == clients.length - 1))).values.toList()
)
],
);
}
}
Widget _buildOAuth2Button(OAuth2ClientInfo client,
String? text,
bool expand,
bool isLast) {
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);
}
Widget button;
bool iconOnly = text == null;
if (iconOnly) {
button = OutlinedButton(
style: _oauth2IconButtonStyle,
onPressed: () => _oauth2ButtonPressed(client),
child: icon);
} else {
button = OutlinedButton.icon(
style: _oauth2ButtonWithTextStyle,
onPressed: () => _oauth2ButtonPressed(client),
icon: icon,
label: Expanded(child: Text(text, textAlign: TextAlign.center)));
}
if (expand) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: isLast ? 0 : 8),
child: button,
)
);
} else {
return button;
}
}
void _oauth2ButtonPressed(OAuth2ClientInfo client) async {
_isLoginNotifier.value = true;
var url = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + client.url);
var params = Map<String,String>.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;
}
}
} }
class LoginPageBackground extends StatelessWidget { class LoginPageBackground extends StatelessWidget {

View File

@@ -107,6 +107,7 @@ class TbContext {
bool _initialized = false; bool _initialized = false;
bool isUserLoaded = false; bool isUserLoaded = false;
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false); final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
PlatformType? _oauth2PlatformType;
List<OAuth2ClientInfo>? oauth2Clients; List<OAuth2ClientInfo>? oauth2Clients;
User? userDetails; User? userDetails;
HomeDashboardInfo? homeDashboard; HomeDashboardInfo? homeDashboard;
@@ -151,8 +152,10 @@ class TbContext {
try { try {
if (Platform.isAndroid) { if (Platform.isAndroid) {
_androidInfo = await deviceInfoPlugin.androidInfo; _androidInfo = await deviceInfoPlugin.androidInfo;
_oauth2PlatformType = PlatformType.ANDROID;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
_iosInfo = await deviceInfoPlugin.iosInfo; _iosInfo = await deviceInfoPlugin.iosInfo;
_oauth2PlatformType = PlatformType.IOS;
} }
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
packageName = packageInfo.packageName; packageName = packageInfo.packageName;
@@ -258,7 +261,7 @@ class TbContext {
} else { } else {
userDetails = null; userDetails = null;
homeDashboard = null; homeDashboard = null;
oauth2Clients = await tbClient.getOAuth2Service().getOAuth2Clients(pkgName: packageName); oauth2Clients = await tbClient.getOAuth2Service().getOAuth2Clients(pkgName: packageName, platform: _oauth2PlatformType);
} }
await updateRouteState(); await updateRouteState();

View File

@@ -432,7 +432,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "00f08109b44b926ab6defaed12b8d4fdc44e07b0" resolved-ref: ee77e14d156129c75e54fff105c63189d8a81b8e
url: "git@github.com:thingsboard/dart_thingsboard_client.git" url: "git@github.com:thingsboard/dart_thingsboard_client.git"
source: git source: git
version: "1.0.0" version: "1.0.0"