Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ package's `README.md` and `PORTING_REPORT.md`.
| [`cloud_functions_tvos`](packages/cloud_functions_tvos) [![pub](https://img.shields.io/pub/v/cloud_functions_tvos.svg)](https://pub.dev/packages/cloud_functions_tvos) | [`cloud_functions`](https://pub.dev/packages/cloud_functions) |
| [`firebase_analytics_tvos`](packages/firebase_analytics_tvos) [![pub](https://img.shields.io/pub/v/firebase_analytics_tvos.svg)](https://pub.dev/packages/firebase_analytics_tvos) | [`firebase_analytics`](https://pub.dev/packages/firebase_analytics) |
| [`firebase_crashlytics_tvos`](packages/firebase_crashlytics_tvos) [![pub](https://img.shields.io/pub/v/firebase_crashlytics_tvos.svg)](https://pub.dev/packages/firebase_crashlytics_tvos) | [`firebase_crashlytics`](https://pub.dev/packages/firebase_crashlytics) |
| [`url_launcher_tvos`](packages/url_launcher_tvos) [![pub](https://img.shields.io/pub/v/url_launcher_tvos.svg)](https://pub.dev/packages/url_launcher_tvos) | [`url_launcher`](https://pub.dev/packages/url_launcher) |

### Evaluated but not provided

Expand All @@ -52,7 +53,6 @@ misleading:

| Plugin | Why not on tvOS |
|---|---|
| [`url_launcher`](https://pub.dev/packages/url_launcher) | No Safari / arbitrary URL or app launching on tvOS |
| [`google_sign_in`](https://pub.dev/packages/google_sign_in) | No GoogleSignIn tvOS SDK; tvOS uses a different device-pairing flow |
| [`geolocator`](https://pub.dev/packages/geolocator) | No location services on Apple TV |
| [`permission_handler`](https://pub.dev/packages/permission_handler) | tvOS lacks the permission surfaces (location, camera, photos, …) |
Expand Down
29 changes: 29 additions & 0 deletions packages/url_launcher_tvos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Dart / Flutter
.dart_tool/
build/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub/

# CocoaPods
tvos/Pods/
tvos/Podfile.lock
tvos/.symlinks/
tvos/Flutter/Flutter.framework
tvos/Flutter/Flutter.podspec

# SwiftPM build cache (regenerated by `flutter-tvos build`)
.build/

# Xcode / SwiftPM (per-user, generated when tvos/Package.swift is opened)
**/.swiftpm/
**/xcuserdata/

# IDE
.idea/
.vscode/
*.iml

# macOS
.DS_Store
9 changes: 9 additions & 0 deletions packages/url_launcher_tvos/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 0.0.1

* Initial tvOS implementation of `url_launcher`, ported from `url_launcher_ios`
6.4.1. External launches (`launchUrl`) and `canLaunchUrl` work via
`UIApplication.open` / `canOpenURL`. The in-app browser modes
(`inAppBrowserView` / `inAppWebView`) are unsupported on tvOS
(no SafariServices): `supportsMode` reports `false`, and a launch requested
with an in-app mode falls back to an external launch (matching the
macOS/Windows/Linux implementations) rather than throwing.
25 changes: 25 additions & 0 deletions packages/url_launcher_tvos/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright 2013 The Flutter Authors

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
102 changes: 102 additions & 0 deletions packages/url_launcher_tvos/PORTING_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# url_launcher_tvos — porting report

Ported by `flutter-tvos plugin port`, then finished + verified by hand.

Source: `url_launcher_ios` 6.4.1 (Swift, Pigeon 26). Base platform: ios.
Output: `./url_launcher_tvos`

## Summary

| Status | Count |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Pigeon methods kept + registered on tvOS | 4 (`canLaunchUrl`, `launchUrl`, `openUrlInSafariViewController`, `closeSafariViewController`) |
| Native methods behaving as-is on tvOS | 2 (`canLaunchUrl`, `launchUrl`) |
| Host methods kept for conformance, never called from tvOS Dart | 2 (`openUrlInSafariViewController`, `closeSafariViewController` — Dart falls back to external) |
| Native regions disabled on tvOS | 1 (the entire `URLLaunchSession` — SFSafariViewController) |
| tvOS build outlook | ✅ compiles (arm64 simulator, verified) |

## The one tvOS-absent API

`url_launcher_ios`'s only tvOS-incompatible surface is the **in-app browser**:
`SFSafariViewController` from **SafariServices**, which does not exist on tvOS.
Everything else — `UIApplication.canOpenURL` / `open(_:options:)` — is available
on tvOS. So `canLaunchUrl` and external `launchUrl` port unchanged; only the
in-app browser path is disabled.

## Native changes

- **`messages.g.swift` (generated Pigeon) — kept verbatim** except one line: the
import gate widened from `#if os(iOS)` to `#if os(iOS) || os(tvOS)`. The porter
had additionally wrapped `UrlLauncherApiSetup.setUp` in `#if !os(tvOS)` (it
matched the string `SFSafariViewController` inside a doc comment) — that would
have compiled out **all** channel registration on tvOS, so every method would
throw `MissingPluginException`. Reverted to upstream so all four channels
register.
- **`URLLaunchSession.swift`** — the whole class (`SFSafariViewControllerDelegate`
- `import SafariServices`) is wrapped in `#if !os(tvOS)`; it is never referenced
on tvOS.
- **`URLLauncherPlugin.swift`** — all four `UrlLauncherApi` methods are
implemented (protocol conformance intact). `canLaunchUrl` / `launchUrl` are
verbatim upstream. On tvOS, `openUrlInSafariViewController` returns
`InAppLoadResult.noUI` (there is no browser UI to present) and
`closeSafariViewController` is a no-op; the real Safari path stays under
`#if !os(tvOS)`.
- **`ViewPresenter.swift`, `Launcher.swift`** — verbatim upstream; both are pure
UIKit / `UIApplication` and compile on tvOS unchanged.

## Dart changes

This package's Dart runs **only on tvOS**, so it states tvOS behaviour directly
(no platform guards). The class is renamed `UrlLauncherIOS` → `UrlLauncherTvos`
(the `dartPluginClass` follows). The Pigeon-generated `messages.g.dart` is kept
**byte-identical to upstream 6.4.1**. The tvOS-honest behaviour:

- `supportsMode(inAppBrowserView` / `inAppWebView)` → `false` (was `true`);
`supportsCloseForMode(...)` → `false` (nothing to close).
- **`launchUrl` falls back to an external launch for _every_ mode** (external,
`platformDefault`, in-app), matching the browser-less macOS/Windows/Linux
impls. This keeps the deprecated `launch('https://…')` — which infers an in-app
mode from the URL scheme — from throwing: it launches externally and an
unclaimed URL returns `false`. The in-app host methods stay registered for
conformance but are unused on tvOS.

## Packaging

- Podspec: no Flutter CocoaPod dependency (resolved via `FRAMEWORK_SEARCH_PATHS`);
`s.platform = :tvos, '13.0'` (mirrors upstream's iOS 13 floor); privacy manifest
shipped as a `resource_bundles` entry.
- Ships **both** a podspec (CocoaPods) and `tvos/Package.swift` (Swift Package
Manager — the Flutter 3.44 default), matching the repo's other pure-Swift
method-channel `_tvos` plugins (e.g. `shared_preferences_tvos`). flutter-tvos's
Podfile skips SPM-owned plugins, so the two never double-link.
- **Version floor:** the package keeps the repo-standard `flutter: >=3.13.0`, not
upstream 6.4.1's `>=3.38.0` / Dart 3.10 (raised in 6.4.0). Nothing in this tvOS
slice uses an API that needs the higher floor — the port builds and `dart
analyze`s clean under it — and `>=3.13.0` keeps it consistent with the sibling
`_tvos` packages.

## Verification

- `dart analyze` clean; `flutter test` — **11/11 pass**, including a `_FakeApi`
that asserts every mode (external, `platformDefault`, in-app, deprecated
`launch()`) reaches the external channel and never throws.
- **`flutter-tvos build tvos --simulator`** — Xcode build succeeds (arm64), via
both the CocoaPods and SPM paths.
- **Runtime on an Apple TV 4K simulator** — all four channels round-trip with no
`MissingPluginException`; `canLaunchUrl(web)` returns `true` while `launchUrl`
returns `false`, so `canLaunchUrl` isn't a reliable gate (documented in README).
- **Real cross-app launch, two ways** (throwaway targets, not shipped): on the
sim the example launched a second app via `targetapp://` (its `AppDelegate`
logged the delivered URL); on a **physical Apple TV 4K** (release/AOT,
`devicectl`) `launchUrl` opened the **App Store**. `UIApplication.open`
genuinely hands off on tvOS.

## Checklist

- [x] All Pigeon methods kept and registered on tvOS (no `MissingPluginException`).
- [x] tvOS-absent API (`SFSafariViewController`) disabled behind `#if !os(tvOS)`;
the two affected handlers return an honest result rather than crashing.
- [x] Generated files (`messages.g.dart`, `messages.g.swift`) match upstream
(the Swift file differs only by the import gate).
- [x] `flutter-tvos build tvos --simulator` compiles the example.
- [x] Version set (`0.0.1`) and `CHANGELOG.md` updated.
52 changes: 52 additions & 0 deletions packages/url_launcher_tvos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# url_launcher_tvos

The tvOS implementation of [`url_launcher`](https://pub.dev/packages/url_launcher).

> Ported with [`flutter-tvos plugin port`](https://github.com/fluttertv/flutter-tvos)
> from `url_launcher_ios` 6.4.1, then finished + verified by hand. See
> `PORTING_REPORT.md`.

## Usage

Federated plugin implementation — no imports needed from app code; it registers
automatically. `url_launcher` does not endorse a tvOS implementation, so add this
package **explicitly** alongside it:

```yaml
dependencies:
url_launcher: ^6.3.2
url_launcher_tvos: ^0.0.1
```

Then use the `url_launcher` API exactly as on iOS.

## What works on tvOS — and what doesn't

tvOS has **no web browser** (no SafariServices / WebKit), so this
implementation supports only the _external_ launch surface:

| Capability | tvOS | Notes |
| ---------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canLaunchUrl` | ✅ | maps to `UIApplication.canOpenURL` |
| `launchUrl` (external / universal link / app scheme) | ✅ | maps to `UIApplication.open`; opens another installed app |
| `launchUrl` in-app browser (`inAppBrowserView` / `inAppWebView`) | ⚠️ falls back | no `SFSafariViewController` on tvOS — `supportsMode` returns `false`, and a launch requested with an in-app mode **falls back to an external launch** (like macOS/Windows/Linux) rather than throwing |
| `closeWebView` | ❌ (no-op) | nothing to close — there is no in-app browser |

Because there is no browser, a plain `http(s)` URL only opens if another
installed app claims it (universal link / app URL scheme). Note that on tvOS
`canLaunchUrl` can return `true` for a web URL even when nothing will actually
handle it, so rely on the boolean returned by `launchUrl` rather than gating on
`canLaunchUrl` alone. **Every** launch mode — `platformDefault`, and the in-app
browser modes — resolves to an **external** launch on tvOS (the iOS
implementation opens web URLs in-app instead).

## Status

| Platform | Implemented | Verified |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Apple TV simulator (`appletvsimulator`) | yes | ✅ builds (arm64); on a running Apple TV 4K sim all four channels round-trip (no `MissingPluginException`), **and a real external launch of a registered app URL scheme returns `true` and is delivered to the target** (`UIApplication.open` hands off) |
| Apple TV (`appletvos`) | yes | ✅ verified on a **physical Apple TV 4K** (release/AOT) — `launchUrl` opened the App Store (a real cross-app hand-off via `UIApplication.open`) |

## License

The FlutterTV Authors under a BSD-3-Clause license. See `LICENSE` for the full text.
7 changes: 7 additions & 0 deletions packages/url_launcher_tvos/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include: package:flutter_lints/flutter.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
3 changes: 3 additions & 0 deletions packages/url_launcher_tvos/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# url_launcher_example

Demonstrates how to use the url_launcher plugin.
124 changes: 124 additions & 0 deletions packages/url_launcher_tvos/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
runApp(const UrlLauncherTvosApp());
}

class UrlLauncherTvosApp extends StatelessWidget {
const UrlLauncherTvosApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'url_launcher tvOS example',
theme: ThemeData.dark(useMaterial3: true),
home: const HomePage(),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
// tvOS has no in-app browser, so only external launches do anything: an app
// URL scheme (or universal link) that another installed app can handle. This
// example registers `ullauncherdemo://` for itself in tvos/Runner/Info.plist,
// so the app-scheme launch below actually hands off. A plain web URL has
// nothing to open on tvOS.
static final Uri _appScheme = Uri.parse('ullauncherdemo://demo');
static final Uri _webUrl = Uri.parse('https://flutter.dev');

String _status = 'Pick an action with the Siri Remote.';

void _show(String message) => setState(() => _status = message);

Future<void> _canLaunch(Uri url) async {
try {
final bool can = await canLaunchUrl(url);
// On tvOS canLaunchUrl can report true for a web URL that nothing will
// actually open — rely on the launchUrl result, not this.
_show('canLaunchUrl($url) = $can');
} catch (e) {
_show('canLaunchUrl($url) threw: $e');
}
}

Future<void> _launch(Uri url, LaunchMode mode) async {
try {
final bool ok = await launchUrl(url, mode: mode);
_show('launchUrl($url, $mode) = $ok');
} catch (e) {
_show('launchUrl($url, $mode) threw: $e');
}
}

Future<void> _showSupport() async {
final bool external =
await supportsLaunchMode(LaunchMode.externalApplication);
final bool inApp = await supportsLaunchMode(LaunchMode.inAppBrowserView);
_show('supportsLaunchMode: external=$external inAppBrowser=$inApp');
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('url_launcher · tvOS')),
body: Padding(
padding: const EdgeInsets.all(48),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(_status, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 12),
const Text(
'In-app browser modes are unsupported on tvOS (no SafariServices) '
'and fall back to an external launch.',
style: TextStyle(fontSize: 18, color: Colors.white70),
),
const SizedBox(height: 32),
ElevatedButton(
autofocus: true,
onPressed: () =>
_launch(_appScheme, LaunchMode.externalApplication),
child: const Text('Launch app scheme (ullauncherdemo://)'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => _launch(_webUrl, LaunchMode.externalApplication),
child: const Text('Launch web URL (no browser → false)'),
),
const SizedBox(height: 12),
ElevatedButton(
// Requesting the in-app browser: on tvOS this falls back to an
// external launch instead of throwing.
onPressed: () => _launch(_webUrl, LaunchMode.inAppBrowserView),
child: const Text('Launch web URL in-app mode (falls back)'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => _canLaunch(_webUrl),
child: const Text('canLaunchUrl (web — may lie, returns true)'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _showSupport,
child: const Text('supportsLaunchMode readout'),
),
],
),
),
);
}
}
21 changes: 21 additions & 0 deletions packages/url_launcher_tvos/example/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: url_launcher_example
description: Demonstrates how to use the url_launcher plugin.
publish_to: none

environment:
sdk: ^3.6.0
flutter: ">=3.27.0"

dependencies:
url_launcher: ^6.3.2
url_launcher_tvos:
path: ../
flutter:
sdk: flutter

dev_dependencies:
flutter_test:
sdk: flutter

flutter:
uses-material-design: true
Loading
Loading