diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 768042be4cef..10057e147adf 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.9 + +* Laid the groundwork for introducing a Link widget. + ## 1.0.8 * Added webOnlyWindowName parameter diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart new file mode 100644 index 000000000000..425dc886d29f --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -0,0 +1,113 @@ +// 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 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Signature for a function provided by the [Link] widget that instructs it to +/// follow the link. +typedef FollowLink = Future Function(); + +/// Signature for a builder function passed to the [Link] widget to construct +/// the widget tree under it. +typedef LinkWidgetBuilder = Widget Function( + BuildContext context, + FollowLink followLink, +); + +/// Signature for a delegate function to build the [Link] widget. +typedef LinkDelegate = Widget Function(LinkInfo linkWidget); + +final MethodCodec _codec = const JSONMethodCodec(); + +/// Defines where a Link URL should be open. +/// +/// This is a class instead of an enum to allow future customizability e.g. +/// opening a link in a specific iframe. +class LinkTarget { + /// Const private constructor with a [debugLabel] to allow the creation of + /// multiple distinct const instances. + const LinkTarget._({this.debugLabel}); + + /// Used to distinguish multiple const instances of [LinkTarget]. + final String debugLabel; + + /// Use the default target for each platform. + /// + /// On Android, the default is [blank]. On the web, the default is [self]. + /// + /// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for + /// non-web URLs. + static const defaultTarget = LinkTarget._(debugLabel: 'defaultTarget'); + + /// On the web, this opens the link in the same tab where the flutter app is + /// running. + /// + /// On Android and iOS, this opens the link in a webview within the app. + static const self = LinkTarget._(debugLabel: 'self'); + + /// On the web, this opens the link in a new tab or window (depending on the + /// browser and user configuration). + /// + /// On Android and iOS, this opens the link in the browser or the relevant + /// app. + static const blank = LinkTarget._(debugLabel: 'blank'); +} + +/// Encapsulates all the information necessary to build a Link widget. +abstract class LinkInfo { + /// Called at build time to construct the widget tree under the link. + LinkWidgetBuilder get builder; + + /// The destination that this link leads to. + Uri get uri; + + /// The target indicating where to open the link. + LinkTarget get target; + + /// Whether the link is disabled or not. + bool get isDisabled; +} + +/// Pushes the [routeName] into Flutter's navigation system via a platform +/// message. +Future pushRouteNameToFramework( + BuildContext context, + String routeName, { + @visibleForTesting bool debugForceRouter = false, +}) { + final Completer completer = Completer(); + if (debugForceRouter || _hasRouter(context)) { + SystemNavigator.routeInformationUpdated(location: routeName); + window.onPlatformMessage( + 'flutter/navigation', + _codec.encodeMethodCall( + MethodCall('pushRouteInformation', { + 'location': routeName, + 'state': null, + }), + ), + completer.complete, + ); + } else { + window.onPlatformMessage( + 'flutter/navigation', + _codec.encodeMethodCall(MethodCall('pushRoute', routeName)), + completer.complete, + ); + } + return completer.future; +} + +bool _hasRouter(BuildContext context) { + try { + return Router.of(context) != null; + } on AssertionError { + // When a `Router` can't be found, an assertion error is thrown. + return false; + } +} diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart index f87630ee3045..ac5bfa230289 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart @@ -7,12 +7,16 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show required; +import 'link.dart'; import 'url_launcher_platform_interface.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher'); /// An implementation of [UrlLauncherPlatform] that uses method channels. class MethodChannelUrlLauncher extends UrlLauncherPlatform { + @override + final LinkDelegate linkDelegate = null; + @override Future canLaunch(String url) { return _channel.invokeMethod( diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index 1de5742c1f6f..75002ff9eb4d 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart' show required; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:url_launcher_platform_interface/link.dart'; import 'method_channel_url_launcher.dart'; @@ -38,6 +39,9 @@ abstract class UrlLauncherPlatform extends PlatformInterface { _instance = instance; } + /// The delegate used by the Link widget to build itself. + LinkDelegate get linkDelegate; + /// Returns `true` if this platform is able to launch [url]. Future canLaunch(String url) { throw UnimplementedError('canLaunch() has not been implemented.'); diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 0c4096278bcb..ce0fdd936c9a 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the url_launcher plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.8 +version: 1.0.9 dependencies: flutter: @@ -19,4 +19,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + flutter: ">=1.22.0 <2.0.0" diff --git a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart new file mode 100644 index 000000000000..99a885ccc179 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart @@ -0,0 +1,71 @@ +// 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:ui'; + +import 'package:mockito/mockito.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:url_launcher_platform_interface/link.dart'; + +final MethodCodec _codec = const JSONMethodCodec(); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + PlatformMessageCallback oldHandler; + MethodCall lastCall; + + setUp(() { + oldHandler = window.onPlatformMessage; + window.onPlatformMessage = ( + String name, + ByteData data, + PlatformMessageResponseCallback callback, + ) { + lastCall = _codec.decodeMethodCall(data); + callback(_codec.encodeSuccessEnvelope(true)); + }; + }); + + tearDown(() { + window.onPlatformMessage = oldHandler; + }); + + test('pushRouteNameToFramework() calls pushRoute when no Router', () async { + await pushRouteNameToFramework(CustomBuildContext(), '/foo/bar'); + expect( + lastCall, + isMethodCall( + 'pushRoute', + arguments: '/foo/bar', + ), + ); + }); + + test( + 'pushRouteNameToFramework() calls pushRouteInformation when Router exists', + () async { + await pushRouteNameToFramework( + CustomBuildContext(), + '/foo/bar', + debugForceRouter: true, + ); + expect( + lastCall, + isMethodCall( + 'pushRouteInformation', + arguments: { + 'location': '/foo/bar', + 'state': null, + }, + ), + ); + }, + ); +} + +class CustomBuildContext extends Mock implements BuildContext {} diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart index 628ab48498ec..d88f53ad58d0 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/method_channel_url_launcher.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -286,4 +287,7 @@ class UrlLauncherPlatformMock extends Mock class ImplementsUrlLauncherPlatform extends Mock implements UrlLauncherPlatform {} -class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {} +class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform { + @override + final LinkDelegate linkDelegate = null; +}