Skip to content
Snippets Groups Projects
Commit 90519673 authored by Gospodin Bodurov's avatar Gospodin Bodurov
Browse files

Merge branch '5-rework-auth-cancelation-flow' into 'master'

Resolve "Rework auth flow."

Closes #5

See merge request !2
parents b67d77eb f336f4ec
No related branches found
No related tags found
1 merge request!2Resolve "Rework auth flow."
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1030"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
buildConfiguration = "Debug" buildConfiguration = "Debug-development"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
</AdditionalOptions> </AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release-development"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
...@@ -82,10 +82,10 @@ ...@@ -82,10 +82,10 @@
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug-development">
</AnalyzeAction> </AnalyzeAction>
<ArchiveAction <ArchiveAction
buildConfiguration = "Debug-development" buildConfiguration = "Release-development"
revealArchiveInOrganizer = "YES"> revealArchiveInOrganizer = "YES">
</ArchiveAction> </ArchiveAction>
</Scheme> </Scheme>
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
buildConfiguration = "Debug" buildConfiguration = "Debug-production"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
</AdditionalOptions> </AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release-production"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug-production">
</AnalyzeAction> </AnalyzeAction>
<ArchiveAction <ArchiveAction
buildConfiguration = "Release-production" buildConfiguration = "Release-production"
......
...@@ -9,6 +9,7 @@ class Config { ...@@ -9,6 +9,7 @@ class Config {
static const HOSTS = [ static const HOSTS = [
'https://demo1.vereign.com', 'https://demo1.vereign.com',
'https://demo2.vereign.com', 'https://demo2.vereign.com',
'https://app.vereign.com',
'https://rosengeorgiev.dev.vereign.com', 'https://rosengeorgiev.dev.vereign.com',
'https://borisdimitrov.dev.vereign.com', 'https://borisdimitrov.dev.vereign.com',
'https://integration.vereign.com', 'https://integration.vereign.com',
......
// app.dart // app.dart
import '../config.dart';
import 'error-alert.dart';
import 'screens/splashscreen.dart'; import 'screens/splashscreen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'screens/home.dart'; import 'screens/home.dart';
...@@ -6,9 +8,25 @@ import 'package:flutter/services.dart'; ...@@ -6,9 +8,25 @@ import 'package:flutter/services.dart';
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_app_auth_wrapper/flutter_app_auth_wrapper.dart';
import 'dart:io';
import 'package:uni_links/uni_links.dart'; import 'package:uni_links/uni_links.dart';
enum AppMode {
Default,
Authorization,
}
enum Screen {
App,
OAuth,
Dashboard
}
class App extends StatelessWidget { class App extends StatelessWidget {
Future<Widget> initApplication() async { Future<Widget> initApplication() async {
Uri initialUri; Uri initialUri;
...@@ -41,8 +59,6 @@ class App extends StatelessWidget { ...@@ -41,8 +59,6 @@ class App extends StatelessWidget {
navigateAfterFuture: initApplication, navigateAfterFuture: initApplication,
) )
); );
} }
} }
...@@ -58,15 +74,83 @@ class MainApp extends StatefulWidget { ...@@ -58,15 +74,83 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> { class _MainAppState extends State<MainApp> {
StreamSubscription _sub; StreamSubscription _sub;
String _appMode = ""; /// "Default" when you open app, "Authorization" when app initiated
/// from third party app
AppMode _appMode;
// Url of the app which invoked OAuth // Url of the app which invoked OAuth
String _invokerURL; String _invokerURL;
String _host;
String _host = Config.appFlavor == Flavor.DEVELOPMENT ? Config.HOSTS[0] : Config.DEFAULT_APP_HOST;
Screen _currentScreen = Screen.App;
bool _buttonsHidden = true;
@override @override
initState() { initState() {
super.initState(); super.initState();
// Set up app host
if (widget.initialHost != null) {
setState(() {
_host = widget.initialHost;
});
}
// Subscribe to authorization events
FlutterAppAuthWrapper.eventStream().listen((data) async {
var token = json.decode(data.toString())["access_token"];
setScreen(Screen.App);
try {
await launch("$_invokerURL?token=$token&host=$_host");
} catch (e) {
showErrorAlert(context, 'Unable to open URL $_invokerURL', openVereign);
}
revealButtons(1);
}, onError: (error) {
log("Err $error");
String errorMessage = error.message;
String errorDetails = "";
try {
errorDetails = json.decode(error.details)["errorDescription"];
} catch (e) {
errorDetails = error.details;
}
if (
// Handle cancellation for Android
errorDetails == 'User cancelled flow' ||
// Handle cancellation for iOS
errorMessage.toLowerCase().contains("the operation couldn")
) {
if (Platform.isAndroid) {
// Reveal after three seconds
revealButtons(3);
// Open only for android, because iOS will show the same alert as
// was for Auth request
openVereign();
} else {
setScreen(Screen.App);
revealButtons(0);
}
} else {
showErrorAlert(context, Platform.isAndroid ? errorDetails : errorMessage, openVereign);
setScreen(Screen.App);
revealButtons(0);
}
});
// Show buttons after timeout
revealButtons(3);
initUniLinks(); initUniLinks();
} }
...@@ -77,31 +161,100 @@ class _MainAppState extends State<MainApp> { ...@@ -77,31 +161,100 @@ class _MainAppState extends State<MainApp> {
} }
Future<Null> initUniLinks() async { Future<Null> initUniLinks() async {
updateAppMode(widget.initialUri); handleLinkChange(widget.initialUri);
_sub = getUriLinksStream().listen((Uri uri) { _sub = getUriLinksStream().listen((Uri uri) {
updateAppMode(uri); handleLinkChange(uri);
}, onError: (err) { }, onError: (err) {
log('got err: $err'); log('got err: $err');
}); });
} }
updateAppMode(Uri uri) { hideButtons() {
if (_revealTimer != null && _revealTimer.isActive) {
_revealTimer.cancel();
}
setState(() {
_buttonsHidden = true;
});
}
Timer _revealTimer;
revealButtons(delay) {
_revealTimer = Timer(
Duration(seconds: delay),
() {
setState(() {
_buttonsHidden = false;
});
}
);
}
handleLinkChange(Uri uri) {
if (uri?.path == "/authorize") { if (uri?.path == "/authorize") {
setState(() { setState(() {
_appMode = "oauth"; _appMode = AppMode.Authorization;
_invokerURL = uri.queryParameters["invokerUrl"]; _invokerURL = uri.queryParameters["invokerUrl"];
}); });
startOAuth();
} else { } else {
setState(() { setState(() {
_appMode = "app"; _appMode = AppMode.Default;
}); });
openVereign();
}
}
setScreen(Screen screen) {
setState(() {
_currentScreen = screen;
});
}
openVereign() {
setScreen(Screen.Dashboard);
FlutterWebBrowser.openWebPage(url: _host, androidToolbarColor: Color(0xFFd51d32));
}
startOAuth() {
if (_currentScreen == Screen.OAuth) {
return;
} }
// Hide buttons so they wont blink after we close or finish oauth
hideButtons();
setScreen(Screen.OAuth);
var params = Config.getOAuthParams(host: _host);
FlutterAppAuthWrapper.startAuth(
AuthConfig(
clientId: params["clientId"],
clientSecret: params["clientSecret"],
redirectUrl: params["redirectUrl"],
state: "login",
prompt: "consent",
endpoint: AuthEndpoint(
auth: params["authEndpoint"], token: params["tokenEndpoint"]),
scopes: [
"user_account_status",
"user_territory",
"user_profile"
],
),
);
} }
setMode(String mode) { setHost(String host) {
setState(() { setState(() {
_appMode = mode; _host = host;
}); });
} }
...@@ -111,9 +264,11 @@ class _MainAppState extends State<MainApp> { ...@@ -111,9 +264,11 @@ class _MainAppState extends State<MainApp> {
appBar: new AppBar(title: Text("Vereign")), appBar: new AppBar(title: Text("Vereign")),
body: Home( body: Home(
mode: _appMode, mode: _appMode,
invokerURL: _invokerURL, host: _host,
setMode: setMode, setHost: setHost,
host: widget.initialHost openDashboardClick: openVereign,
authorizeClick: startOAuth,
buttonsHidden: _buttonsHidden
) )
); );
} }
......
import 'package:flutter/material.dart';
Future<void> showErrorAlert(context, errorString, callback) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Authorization error'),
content: Text(errorString),
actions: <Widget>[
FlatButton(
child: Text('Open Dashboard'),
onPressed: () async {
Navigator.of(context).pop();
callback();
},
),
],
);
},
);
}
\ No newline at end of file
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:flutter_app_auth_wrapper/flutter_app_auth_wrapper.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:developer';
import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../config.dart'; import '../../config.dart';
import '../app.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
Home({@required this.mode, @required this.invokerURL, @required this.setMode, @required this.host}); Home({
final String mode; @required this.mode,
final String invokerURL; @required this.host,
@required this.setHost,
@required this.authorizeClick,
@required this.openDashboardClick,
@required this.buttonsHidden,
});
final AppMode mode;
final String host; final String host;
final void Function(String) setMode; final bool buttonsHidden;
final void Function(String) setHost;
final void Function() openDashboardClick;
final void Function() authorizeClick;
@override @override
_HomeState createState() => _HomeState(); _HomeState createState() => _HomeState();
} }
class _HomeState extends State<Home> { class _HomeState extends State<Home> {
String _host = Config.appFlavor == Flavor.DEVELOPMENT ? Config.HOSTS[0] : Config.DEFAULT_APP_HOST;
bool _hidden = true;
@override @override
initState() { initState() {
super.initState(); super.initState();
if (widget.host != null) {
setState(() {
_host = widget.host;
});
}
showMode(widget.mode);
FlutterAppAuthWrapper.eventStream().listen((data) {
var token = json.decode(data.toString())["access_token"];
_showAlert(token);
widget.setMode("");
}, onError: (error) {
log("Err $error");
widget.setMode("");
});
Timer(
Duration(seconds: 3),
() {
setState(() {
_hidden = false;
});
}
);
}
Future<void> _showAlert(token) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Authorization success'),
actions: <Widget>[
FlatButton(
child: Text('Go back'),
onPressed: () async {
Navigator.of(context).pop();
log("Open ${widget.invokerURL}");
try {
await launch("${widget.invokerURL}?token=$token&host=$_host");
} catch (e) {
log("Error launching url ${widget.invokerURL}");
}
},
),
],
);
},
);
}
@override
void didUpdateWidget(Home oldWidget) {
// this method IS called when parent widget passes new "props"
// unlike React, this method IS called _before_ the build
// unlike React, this method ISN'T called after setState()
if (widget.host != oldWidget.host && widget.host != null) {
setState(() {
_host = widget.host;
});
}
if (widget.mode != oldWidget.mode) {
showMode(widget.mode);
}
super.didUpdateWidget(oldWidget);
}
showMode(mode) {
if (mode == "app") {
openVereign();
} else if (mode == "oauth") {
startOAuth();
}
}
openVereign() {
FlutterWebBrowser.openWebPage(url: _host, androidToolbarColor: Color(0xFFd51d32));
}
startOAuth() {
var params = Config.getOAuthParams(host: _host);
FlutterAppAuthWrapper.startAuth(
AuthConfig(
clientId: params["clientId"],
clientSecret: params["clientSecret"],
redirectUrl: params["redirectUrl"],
state: "login",
prompt: "consent",
endpoint: AuthEndpoint(
auth: params["authEndpoint"], token: params["tokenEndpoint"]),
scopes: [
"user_account_status",
"user_territory",
"user_profile"
],
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_hidden) { if (widget.buttonsHidden) {
return Scaffold(); return Scaffold();
} }
var children = <Widget>[ var children = <Widget>[
_urlButton(context, "Open Dashboard", openVereign), _urlButton(context, "Open Dashboard", widget.openDashboardClick),
_urlButton(context, "Authorize with Vereign", startOAuth)
]; ];
if (widget.mode == AppMode.Authorization) {
children.add(_urlButton(context, "Authorize with Vereign", widget.authorizeClick));
}
if (Config.appFlavor == Flavor.DEVELOPMENT) { if (Config.appFlavor == Flavor.DEVELOPMENT) {
children.add( children.add(
wrapInContainer( wrapInContainer(
DropdownButton<String>( DropdownButton<String>(
value: _host, value: widget.host,
onChanged: (String newValue) async { onChanged: (String newValue) async {
setState(() { widget.setHost(newValue);
_host = newValue;
});
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
prefs.setString("host", newValue); prefs.setString("host", newValue);
}, },
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment