diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfdb3ff9b7a11a9497b84546d615db2afa..12f056a58e55a3eab7dc15a86eed72a1d6f4576b 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1020" + LastUpgradeVersion = "1030" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/development.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/development.xcscheme index 070dcc01088fafcb8456d97746866c29febf81b4..2e31fa212bc008b370f1068d3bb072bb8e09f625 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/development.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/development.xcscheme @@ -23,7 +23,7 @@ </BuildActionEntries> </BuildAction> <TestAction - buildConfiguration = "Debug" + buildConfiguration = "Debug-development" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -65,7 +65,7 @@ </AdditionalOptions> </LaunchAction> <ProfileAction - buildConfiguration = "Release" + buildConfiguration = "Release-development" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" @@ -82,10 +82,10 @@ </BuildableProductRunnable> </ProfileAction> <AnalyzeAction - buildConfiguration = "Debug"> + buildConfiguration = "Debug-development"> </AnalyzeAction> <ArchiveAction - buildConfiguration = "Debug-development" + buildConfiguration = "Release-development" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/production.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/production.xcscheme index 9bacab5a653ddf4d641628abf214ab299a3be56a..041708fdd109c4df750e65bd03151facc85f8c26 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/production.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/production.xcscheme @@ -23,7 +23,7 @@ </BuildActionEntries> </BuildAction> <TestAction - buildConfiguration = "Debug" + buildConfiguration = "Debug-production" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -65,7 +65,7 @@ </AdditionalOptions> </LaunchAction> <ProfileAction - buildConfiguration = "Release" + buildConfiguration = "Release-production" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" @@ -82,7 +82,7 @@ </BuildableProductRunnable> </ProfileAction> <AnalyzeAction - buildConfiguration = "Debug"> + buildConfiguration = "Debug-production"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release-production" diff --git a/lib/config.dart b/lib/config.dart index cbbe9229109a57999eabcdd92f20e05d7fd3a5f3..2194fde776d6cc4645101506dbdaedaaa5053f08 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -9,6 +9,7 @@ class Config { static const HOSTS = [ 'https://demo1.vereign.com', 'https://demo2.vereign.com', + 'https://app.vereign.com', 'https://rosengeorgiev.dev.vereign.com', 'https://borisdimitrov.dev.vereign.com', 'https://integration.vereign.com', diff --git a/lib/src/app.dart b/lib/src/app.dart index 3e2b29d60263127e11faa00ddc88778a5d9eabd1..d9ea905180a204cffb1468c50d31d1ef66ac2420 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,4 +1,6 @@ // app.dart +import '../config.dart'; +import 'error-alert.dart'; import 'screens/splashscreen.dart'; import 'package:flutter/material.dart'; import 'screens/home.dart'; @@ -6,9 +8,25 @@ import 'package:flutter/services.dart'; import 'dart:async'; import 'dart:developer'; 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'; +enum AppMode { + Default, + Authorization, +} + +enum Screen { + App, + OAuth, + Dashboard +} + class App extends StatelessWidget { Future<Widget> initApplication() async { Uri initialUri; @@ -41,8 +59,6 @@ class App extends StatelessWidget { navigateAfterFuture: initApplication, ) ); - - } } @@ -58,15 +74,83 @@ class MainApp extends StatefulWidget { class _MainAppState extends State<MainApp> { 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 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 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(); } @@ -77,31 +161,100 @@ class _MainAppState extends State<MainApp> { } Future<Null> initUniLinks() async { - updateAppMode(widget.initialUri); + handleLinkChange(widget.initialUri); _sub = getUriLinksStream().listen((Uri uri) { - updateAppMode(uri); + handleLinkChange(uri); }, onError: (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") { setState(() { - _appMode = "oauth"; + _appMode = AppMode.Authorization; _invokerURL = uri.queryParameters["invokerUrl"]; }); + + startOAuth(); } else { 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(() { - _appMode = mode; + _host = host; }); } @@ -111,9 +264,11 @@ class _MainAppState extends State<MainApp> { appBar: new AppBar(title: Text("Vereign")), body: Home( mode: _appMode, - invokerURL: _invokerURL, - setMode: setMode, - host: widget.initialHost + host: _host, + setHost: setHost, + openDashboardClick: openVereign, + authorizeClick: startOAuth, + buttonsHidden: _buttonsHidden ) ); } diff --git a/lib/src/error-alert.dart b/lib/src/error-alert.dart new file mode 100644 index 0000000000000000000000000000000000000000..a3fd300440bb05e7f45e2219fc518cf05668bb17 --- /dev/null +++ b/lib/src/error-alert.dart @@ -0,0 +1,22 @@ +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 diff --git a/lib/src/screens/home.dart b/lib/src/screens/home.dart index a336e144994788c2e649399288ebdb6afc92cf78..c5e8e88309250d99b2cff53ade6c8efb6f5073dd 100644 --- a/lib/src/screens/home.dart +++ b/lib/src/screens/home.dart @@ -1,160 +1,58 @@ 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 'package:shared_preferences/shared_preferences.dart'; import '../../config.dart'; +import '../app.dart'; class Home extends StatefulWidget { - Home({@required this.mode, @required this.invokerURL, @required this.setMode, @required this.host}); - final String mode; - final String invokerURL; + Home({ + @required this.mode, + @required this.host, + @required this.setHost, + @required this.authorizeClick, + @required this.openDashboardClick, + @required this.buttonsHidden, + }); + + final AppMode mode; 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 _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { - String _host = Config.appFlavor == Flavor.DEVELOPMENT ? Config.HOSTS[0] : Config.DEFAULT_APP_HOST; - bool _hidden = true; - @override 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 Widget build(BuildContext context) { - if (_hidden) { + if (widget.buttonsHidden) { return Scaffold(); } var children = <Widget>[ - _urlButton(context, "Open Dashboard", openVereign), - _urlButton(context, "Authorize with Vereign", startOAuth) + _urlButton(context, "Open Dashboard", widget.openDashboardClick), ]; + if (widget.mode == AppMode.Authorization) { + children.add(_urlButton(context, "Authorize with Vereign", widget.authorizeClick)); + } + if (Config.appFlavor == Flavor.DEVELOPMENT) { children.add( wrapInContainer( DropdownButton<String>( - value: _host, + value: widget.host, onChanged: (String newValue) async { - setState(() { - _host = newValue; - }); - + widget.setHost(newValue); final prefs = await SharedPreferences.getInstance(); prefs.setString("host", newValue); },