Add platform type filter to oauth2 clients request. Improve login page.
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user