diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart index f33a477..a6f10e3 100644 --- a/lib/providers/settings.dart +++ b/lib/providers/settings.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:uni_links/uni_links.dart'; import 'package:nanoid/nanoid.dart'; +import 'package:url_launcher/url_launcher.dart'; // import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -104,6 +105,11 @@ class _SettingsProviderState extends State { // https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow void _onSchemeDetected(Uri uri) async { + try { + // FIXME: + await closeWebView(); + } catch (err) {} + setState(() { loading = true; }); diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 874359a..267beb5 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -76,7 +76,6 @@ class _LoginScreenState extends State { var state = settings.generateRandomString(); launch( 'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=user%20repo&state=$state', - forceSafariVC: false, // this makes URL Scheme work ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index 6996702..837d7e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,8 @@ dependencies: rxdart: ^0.20.0 uri: ^0.11.3 intl: ^0.15.7 - url_launcher: ^4.2.0 + url_launcher: + path: vendor/url_launcher uni_links: ^0.1.4 flutter_markdown: ^0.2.0 shared_preferences: ^0.5.0 diff --git a/vendor/url_launcher/android/build.gradle b/vendor/url_launcher/android/build.gradle new file mode 100644 index 0000000..1c82a15 --- /dev/null +++ b/vendor/url_launcher/android/build.gradle @@ -0,0 +1,34 @@ +group 'io.flutter.plugins.urllauncher' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 27 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/vendor/url_launcher/android/gradle.properties b/vendor/url_launcher/android/gradle.properties new file mode 100644 index 0000000..8bd86f6 --- /dev/null +++ b/vendor/url_launcher/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/vendor/url_launcher/android/settings.gradle b/vendor/url_launcher/android/settings.gradle new file mode 100644 index 0000000..6620cd7 --- /dev/null +++ b/vendor/url_launcher/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'url_launcher' diff --git a/vendor/url_launcher/android/src/main/AndroidManifest.xml b/vendor/url_launcher/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3b455ea --- /dev/null +++ b/vendor/url_launcher/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/vendor/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java b/vendor/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java new file mode 100644 index 0000000..851b9b5 --- /dev/null +++ b/vendor/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java @@ -0,0 +1,143 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.urllauncher; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Bundle; +import android.view.KeyEvent; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** UrlLauncherPlugin */ +public class UrlLauncherPlugin implements MethodCallHandler { + private final Registrar mRegistrar; + + public static void registerWith(Registrar registrar) { + MethodChannel channel = + new MethodChannel(registrar.messenger(), "plugins.flutter.io/url_launcher"); + UrlLauncherPlugin instance = new UrlLauncherPlugin(registrar); + channel.setMethodCallHandler(instance); + } + + private UrlLauncherPlugin(Registrar registrar) { + this.mRegistrar = registrar; + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + String url = call.argument("url"); + if (call.method.equals("canLaunch")) { + canLaunch(url, result); + } else if (call.method.equals("launch")) { + Intent launchIntent; + boolean useWebView = call.argument("useWebView"); + boolean enableJavaScript = call.argument("enableJavaScript"); + Activity activity = mRegistrar.activity(); + if (activity == null) { + result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); + return; + } + if (useWebView) { + launchIntent = new Intent(activity, WebViewActivity.class); + launchIntent.putExtra("url", url); + launchIntent.putExtra("enableJavaScript", enableJavaScript); + } else { + launchIntent = new Intent(Intent.ACTION_VIEW); + launchIntent.setData(Uri.parse(url)); + } + activity.startActivity(launchIntent); + result.success(null); + } else if (call.method.equals("closeWebView")) { + Intent intent = new Intent("close"); + mRegistrar.context().sendBroadcast(intent); + result.success(null); + } else { + result.notImplemented(); + } + } + + private void canLaunch(String url, Result result) { + Intent launchIntent = new Intent(Intent.ACTION_VIEW); + launchIntent.setData(Uri.parse(url)); + ComponentName componentName = + launchIntent.resolveActivity(mRegistrar.context().getPackageManager()); + + boolean canLaunch = + componentName != null + && !"{com.android.fallback/com.android.fallback.Fallback}" + .equals(componentName.toShortString()); + result.success(canLaunch); + } + + /* Launches WebView activity */ + public static class WebViewActivity extends Activity { + private WebView webview; + private BroadcastReceiver broadcastReceiver; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + webview = new WebView(this); + setContentView(webview); + // Get the Intent that started this activity and extract the string + Intent intent = getIntent(); + String url = intent.getStringExtra("url"); + Boolean enableJavaScript = intent.getBooleanExtra("enableJavaScript", false); + webview.loadUrl(url); + if (enableJavaScript) { + webview.getSettings().setJavaScriptEnabled(enableJavaScript); + } + // Open new urls inside the webview itself. + webview.setWebViewClient( + new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + view.loadUrl(request.getUrl().toString()); + return false; + } + }); + + // Set broadcast receiver to handle calls to close the web view + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context arg0, Intent intent) { + String action = intent.getAction(); + if ("close".equals(action)) { + finish(); + } + } + }; + registerReceiver(broadcastReceiver, new IntentFilter("close")); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(broadcastReceiver); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && webview.canGoBack()) { + webview.goBack(); + return true; + } + return super.onKeyDown(keyCode, event); + } + } +} diff --git a/vendor/url_launcher/ios/Assets/.gitkeep b/vendor/url_launcher/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.h b/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.h new file mode 100644 index 0000000..e060091 --- /dev/null +++ b/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +@interface FLTUrlLauncherPlugin : NSObject +@end diff --git a/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.m b/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.m new file mode 100644 index 0000000..ce97b2c --- /dev/null +++ b/vendor/url_launcher/ios/Classes/UrlLauncherPlugin.m @@ -0,0 +1,147 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "UrlLauncherPlugin.h" + +@interface FLTUrlLaunchSession : NSObject +@property(strong) SFSafariViewController *safari; +@end + +@implementation FLTUrlLaunchSession { + NSURL *_url; + FlutterResult _flutterResult; + void (^_completion)(); +} + +- (instancetype)initWithUrl:url withFlutterResult:result completion:completion { + self = [super init]; + if (self) { + _url = url; + _flutterResult = result; + _safari = [[SFSafariViewController alloc] initWithURL:url]; + _safari.delegate = self; + _completion = completion; + } + return self; +} + +- (void)safariViewController:(SFSafariViewController *)controller + didCompleteInitialLoad:(BOOL)didLoadSuccessfully { + if (didLoadSuccessfully) { + _flutterResult(nil); + } else { + _flutterResult([FlutterError + errorWithCode:@"Error" + message:[NSString stringWithFormat:@"Error while launching %@", _url] + details:nil]); + } +} + +- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { + [controller dismissViewControllerAnimated:YES completion:_completion]; +} + +- (void)close { + [self safariViewControllerDidFinish:_safari]; +} + +@end + +@implementation FLTUrlLauncherPlugin { + UIViewController *_viewController; + FLTUrlLaunchSession *_currentSession; +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher" + binaryMessenger:registrar.messenger]; + UIViewController *viewController = + [UIApplication sharedApplication].delegate.window.rootViewController; + FLTUrlLauncherPlugin *plugin = + [[FLTUrlLauncherPlugin alloc] initWithViewController:viewController]; + [registrar addMethodCallDelegate:plugin channel:channel]; +} + +- (instancetype)initWithViewController:(UIViewController *)viewController { + self = [super init]; + if (self) { + _viewController = viewController; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + NSString *url = call.arguments[@"url"]; + if ([@"canLaunch" isEqualToString:call.method]) { + result(@([self canLaunchURL:url])); + } else if ([@"launch" isEqualToString:call.method]) { + NSNumber *useSafariVC = call.arguments[@"useSafariVC"]; + if (useSafariVC.boolValue) { + [self launchURLInVC:url result:result]; + } else { + [self launchURL:url result:result]; + } + } else if ([@"closeWebView" isEqualToString:call.method]) { + [self closeWebView:url result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (BOOL)canLaunchURL:(NSString *)urlString { + NSURL *url = [NSURL URLWithString:urlString]; + UIApplication *application = [UIApplication sharedApplication]; + return [application canOpenURL:url]; +} + +- (void)launchURL:(NSString *)urlString result:(FlutterResult)result { + NSURL *url = [NSURL URLWithString:urlString]; + UIApplication *application = [UIApplication sharedApplication]; + if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) { + [application openURL:url + options:@{} + completionHandler:^(BOOL success) { + if (success) { + result(nil); + } else { + result([FlutterError + errorWithCode:@"Error" + message:[NSString stringWithFormat:@"Error while launching %@", url] + details:nil]); + } + }]; + } else { + BOOL success = [application openURL:url]; + if (success) { + result(nil); + } else { + result([FlutterError + errorWithCode:@"Error" + message:[NSString stringWithFormat:@"Error while launching %@", url] + details:nil]); + } + } +} + +- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result { + NSURL *url = [NSURL URLWithString:urlString]; + _currentSession = [[FLTUrlLaunchSession alloc] initWithUrl:url + withFlutterResult:result + completion:^void() { + self->_currentSession = nil; + }]; + [_viewController presentViewController:_currentSession.safari animated:YES completion:nil]; +} + +- (void)closeWebView:(NSString *)urlString result:(FlutterResult)result { + if (_currentSession != nil) { + [_currentSession close]; + } + result(nil); +} + +@end diff --git a/vendor/url_launcher/ios/url_launcher.podspec b/vendor/url_launcher/ios/url_launcher.podspec new file mode 100644 index 0000000..d8436c1 --- /dev/null +++ b/vendor/url_launcher/ios/url_launcher.podspec @@ -0,0 +1,21 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'url_launcher' + s.version = '0.0.1' + s.summary = 'Flutter plugin for launching a URL.' + s.description = <<-DESC +A Flutter plugin for making the underlying platform (Android or iOS) launch a URL. + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end + diff --git a/vendor/url_launcher/lib/url_launcher.dart b/vendor/url_launcher/lib/url_launcher.dart new file mode 100644 index 0000000..0aedb20 --- /dev/null +++ b/vendor/url_launcher/lib/url_launcher.dart @@ -0,0 +1,103 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher'); + +/// Parses the specified URL string and delegates handling of it to the +/// underlying platform. +/// +/// The returned future completes with a [PlatformException] on invalid URLs and +/// schemes which cannot be handled, that is when [canLaunch] would complete +/// with false. +/// +/// [forceSafariVC] is only used in iOS. If unset, the launcher opens web URLs +/// in the safari VC, anything else is opened using the default handler on the +/// platform. If set to true, it opens the URL in the Safari view controller. +/// If false, the URL is opened in the default browser of the phone. Set this to +/// false if you want to use the cookies/context of the main browser of the app +/// (such as SSO flows). +/// +/// [forceWebView] is an Android only setting. If null or false, the URL is +/// always launched with the default browser on device. If set to true, the URL +/// is launched in a webview. Unlike iOS, browser context is shared across +/// WebViews. +/// [enableJavaScript] is an Android only setting. If true, webview enable +/// javascript. +/// +/// Note that if any of the above are set to true but the URL is not a web URL, +/// this will throw a [PlatformException]. +/// +/// [statusBarBrightness] Sets the status bar brightness of the application +/// after opening a link on iOS. Does nothing if no value is passed. This does +/// not handle reseting the previous status bar style. +Future launch( + String urlString, { + bool forceSafariVC, + bool forceWebView, + bool enableJavaScript, + Brightness statusBarBrightness, +}) { + assert(urlString != null); + final Uri url = Uri.parse(urlString.trimLeft()); + final bool isWebURL = url.scheme == 'http' || url.scheme == 'https'; + if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { + throw PlatformException( + code: 'NOT_A_WEB_SCHEME', + message: 'To use webview or safariVC, you need to pass' + 'in a web URL. This $urlString is not a web URL.'); + } + bool previousAutomaticSystemUiAdjustment; + if (statusBarBrightness != null && + defaultTargetPlatform == TargetPlatform.iOS) { + previousAutomaticSystemUiAdjustment = + WidgetsBinding.instance.renderView.automaticSystemUiAdjustment; + WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false; + SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light); + } + return _channel.invokeMethod( + 'launch', + { + 'url': urlString, + 'useSafariVC': forceSafariVC ?? isWebURL, + 'useWebView': forceWebView ?? false, + 'enableJavaScript': enableJavaScript ?? false, + }, + ).then((void _) { + if (statusBarBrightness != null) { + WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = + previousAutomaticSystemUiAdjustment; + } + }); +} + +/// Checks whether the specified URL can be handled by some app installed on the +/// device. +Future canLaunch(String urlString) async { + if (urlString == null) { + return false; + } + return await _channel.invokeMethod( + 'canLaunch', + {'url': urlString}, + ); +} + +/// Closes the current WebView, if one was previously opened via a call to [launch]. +/// +/// If [launch] was never called, then this call will not have any effect. +/// +/// On Android systems, if [launch] was called without `forceWebView` being set to +/// `true`, this call will not do anything either, simply because there is no +/// WebView available to be closed. +Future closeWebView() async { + return await _channel.invokeMethod('closeWebView'); +} diff --git a/vendor/url_launcher/pubspec.yaml b/vendor/url_launcher/pubspec.yaml new file mode 100644 index 0000000..e07d3dc --- /dev/null +++ b/vendor/url_launcher/pubspec.yaml @@ -0,0 +1,24 @@ +name: url_launcher +description: Flutter plugin for launching a URL on Android and iOS. Supports + web, phone, SMS, and email schemes. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher +version: 4.0.3 + +flutter: + plugin: + androidPackage: io.flutter.plugins.urllauncher + iosPrefix: FLT + pluginClass: UrlLauncherPlugin + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=0.5.6 <2.0.0"