diff --git a/README.md b/README.md index a76defc..c8a70c1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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, …) | diff --git a/packages/url_launcher_tvos/.gitignore b/packages/url_launcher_tvos/.gitignore new file mode 100644 index 0000000..fc6ca5b --- /dev/null +++ b/packages/url_launcher_tvos/.gitignore @@ -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 diff --git a/packages/url_launcher_tvos/CHANGELOG.md b/packages/url_launcher_tvos/CHANGELOG.md new file mode 100644 index 0000000..27e264c --- /dev/null +++ b/packages/url_launcher_tvos/CHANGELOG.md @@ -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. diff --git a/packages/url_launcher_tvos/LICENSE b/packages/url_launcher_tvos/LICENSE new file mode 100644 index 0000000..29b709d --- /dev/null +++ b/packages/url_launcher_tvos/LICENSE @@ -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. diff --git a/packages/url_launcher_tvos/PORTING_REPORT.md b/packages/url_launcher_tvos/PORTING_REPORT.md new file mode 100644 index 0000000..aea421c --- /dev/null +++ b/packages/url_launcher_tvos/PORTING_REPORT.md @@ -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. diff --git a/packages/url_launcher_tvos/README.md b/packages/url_launcher_tvos/README.md new file mode 100644 index 0000000..47a218d --- /dev/null +++ b/packages/url_launcher_tvos/README.md @@ -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. diff --git a/packages/url_launcher_tvos/analysis_options.yaml b/packages/url_launcher_tvos/analysis_options.yaml new file mode 100644 index 0000000..b49c352 --- /dev/null +++ b/packages/url_launcher_tvos/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true diff --git a/packages/url_launcher_tvos/example/README.md b/packages/url_launcher_tvos/example/README.md new file mode 100644 index 0000000..35b4bdb --- /dev/null +++ b/packages/url_launcher_tvos/example/README.md @@ -0,0 +1,3 @@ +# url_launcher_example + +Demonstrates how to use the url_launcher plugin. diff --git a/packages/url_launcher_tvos/example/lib/main.dart b/packages/url_launcher_tvos/example/lib/main.dart new file mode 100644 index 0000000..3faee6b --- /dev/null +++ b/packages/url_launcher_tvos/example/lib/main.dart @@ -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 createState() => _HomePageState(); +} + +class _HomePageState extends State { + // 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 _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 _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 _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: [ + 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'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/url_launcher_tvos/example/pubspec.yaml b/packages/url_launcher_tvos/example/pubspec.yaml new file mode 100644 index 0000000..cd311fb --- /dev/null +++ b/packages/url_launcher_tvos/example/pubspec.yaml @@ -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 diff --git a/packages/url_launcher_tvos/example/tvos/.gitignore b/packages/url_launcher_tvos/example/tvos/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/url_launcher_tvos/example/tvos/Flutter/Debug.xcconfig b/packages/url_launcher_tvos/example/tvos/Flutter/Debug.xcconfig new file mode 100644 index 0000000..f5ba6d4 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/url_launcher_tvos/example/tvos/Flutter/Release.xcconfig b/packages/url_launcher_tvos/example/tvos/Flutter/Release.xcconfig new file mode 100644 index 0000000..075d0bd --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/url_launcher_tvos/example/tvos/Podfile b/packages/url_launcher_tvos/example/tvos/Podfile new file mode 100644 index 0000000..3fcee3e --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Podfile @@ -0,0 +1,45 @@ +# Flutter tvOS Podfile — auto-generated by flutter-tvos create. +# Reads .flutter-plugins-dependencies and adds local pods for each plugin. + +platform :tvos, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +target 'Runner' do + use_frameworks! + + # Install plugin pods from .flutter-plugins-dependencies + flutter_plugins_deps = File.expand_path(File.join('..', '.flutter-plugins-dependencies'), File.dirname(__FILE__)) + if File.exist?(flutter_plugins_deps) + require 'json' + deps = JSON.parse(File.read(flutter_plugins_deps)) + tvos_plugins = deps.dig('plugins', 'tvos') || [] + tvos_plugins.each do |plugin| + plugin_name = plugin['name'] + plugin_path = plugin['path'] + tvos_dir = File.join(plugin_path, 'tvos') + # Plugins that ship a Package.swift are resolved via Swift Package Manager + # (see flutter-tvos's generated FlutterGeneratedPluginSwiftPackage). Skip + # them here so they are never linked twice (SPM + CocoaPods). + has_spm = File.exist?(File.join(tvos_dir, 'Package.swift')) + if File.directory?(tvos_dir) && !has_spm && File.exist?(File.join(tvos_dir, "#{plugin_name}.podspec")) + pod plugin_name, :path => tvos_dir + end + end + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['TVOS_DEPLOYMENT_TARGET'] = '13.0' + end + end +end diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.pbxproj b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d6d3cca --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,545 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 97C146FB1CF9000082B4168C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000082B4168C /* AppDelegate.swift */; }; + 97C1470A1CF9000082B4168D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000082B4168D /* Main.storyboard */; }; + 97C1470B1CF9000082B4168D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000082B4168E /* LaunchScreen.storyboard */; }; + 97C1470F1CF9000082B4168C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000082B4168C /* Assets.xcassets */; }; + A7520CFEF34876166CE9DA38 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D1BD169D01CFFB544399692 /* Pods_Runner.framework */; }; + AAF20000000000000000F00D /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = AAF30000000000000000F00D /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAB004F5970 /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 4D1BD169D01CFFB544399692 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 52F068B04D03B88EDC30FEAF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000082B41680 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FA1CF9000082B4168C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 97C146FD1CF9000082B4168C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C146FE1CF9000082B4168C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 97C146FF1CF9000082B4168D /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FF1CF9000082B4168E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 9E5C6859A82F307067ED5B61 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + AAA000000000000000000003 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + AAA2105EE2136FA6602379F9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000082B4168C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AAF20000000000000000F00D /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + A7520CFEF34876166CE9DA38 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 97C146E51CF9000082B4168C = { + isa = PBXGroup; + children = ( + 97C146F01CF9000082B4168C /* Runner */, + 97C146F01CF9000082B4168E /* Flutter */, + 97C146F01CF9000082B4168F /* Frameworks */, + 97C146EF1CF9000082B41690 /* Products */, + F9A0954678CF7A5C96D227FE /* Pods */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000082B41690 /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000082B41680 /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000082B4168C /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000082B4168C /* AppDelegate.swift */, + AAA000000000000000000003 /* Runner-Bridging-Header.h */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 97C146FD1CF9000082B4168C /* Assets.xcassets */, + 97C146FE1CF9000082B4168C /* Info.plist */, + 97C146FF1CF9000082B4168D /* Main.storyboard */, + 97C146FF1CF9000082B4168E /* LaunchScreen.storyboard */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F01CF9000082B4168E /* Flutter */ = { + isa = PBXGroup; + children = ( + 74858FAE1ED2DC5600515810 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146F01CF9000082B4168F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAB004F5970 /* Flutter.framework */, + 4D1BD169D01CFFB544399692 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F9A0954678CF7A5C96D227FE /* Pods */ = { + isa = PBXGroup; + children = ( + 52F068B04D03B88EDC30FEAF /* Pods-Runner.debug.xcconfig */, + AAA2105EE2136FA6602379F9 /* Pods-Runner.release.xcconfig */, + 9E5C6859A82F307067ED5B61 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000082B41690 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000082B4168C /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5C01A3C59A866235C35E2DB4 /* [CP] Check Pods Manifest.lock */, + 97C146EA1CF9000082B4168C /* Sources */, + 97C146EB1CF9000082B4168C /* Frameworks */, + 97C146EC1CF9000082B4168C /* Resources */, + AAF10000000000000000F00D /* Embed App.framework */, + 9740EEB31CF901A200538489 /* Copy flutter_assets */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + AAF30000000000000000F00D /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000082B41680 /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000082B4168C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + 97C146ED1CF9000082B41690 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000082B4168C /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000082B4168C; + packageReferences = ( + AAF40000000000000000F00D /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000082B41690 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000082B41690 /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000082B4168C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C1470F1CF9000082B4168C /* Assets.xcassets in Resources */, + 97C1470A1CF9000082B4168D /* Main.storyboard in Resources */, + 97C1470B1CF9000082B4168D /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5C01A3C59A866235C35E2DB4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB31CF901A200538489 /* Copy flutter_assets */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(PROJECT_DIR)/Flutter/flutter_assets", + ); + name = "Copy flutter_assets"; + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(PRODUCT_NAME).app/flutter_assets", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n# Copy flutter_assets into the app bundle\nFLUTTER_ASSETS_SRC=\"${PROJECT_DIR}/Flutter/flutter_assets\"\nDEST=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/flutter_assets\"\nif [ -d \"${FLUTTER_ASSETS_SRC}\" ]; then\n echo \"Copying flutter_assets to app bundle...\"\n rsync -av --delete \"${FLUTTER_ASSETS_SRC}/\" \"${DEST}/\"\nelse\n echo \"warning: flutter_assets not found at ${FLUTTER_ASSETS_SRC}\"\nfi\n"; + }; + AAF10000000000000000F00D /* Embed App.framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(PROJECT_DIR)/Flutter/App.framework", + ); + name = "Embed App.framework"; + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(PRODUCT_NAME).app/Frameworks/App.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n# Embed App.framework (AOT Dart snapshots) into the app bundle.\n# Present only for release/profile (AOT) builds; debug/JIT has no App.framework.\n# Runs for build, run, AND archive, so TestFlight/App Store builds get it too.\nAPP_FRAMEWORK_SRC=\"${PROJECT_DIR}/Flutter/App.framework\"\nDEST_FRAMEWORKS=\"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Frameworks\"\nif [ -d \"${APP_FRAMEWORK_SRC}\" ]; then\n echo \"Embedding App.framework...\"\n mkdir -p \"${DEST_FRAMEWORKS}\"\n rsync -av --delete \"${APP_FRAMEWORK_SRC}\" \"${DEST_FRAMEWORKS}/\"\n if [ \"${CODE_SIGNING_REQUIRED}\" != \"NO\" ] && [ -n \"${EXPANDED_CODE_SIGN_IDENTITY}\" ]; then\n echo \"Codesigning App.framework with ${EXPANDED_CODE_SIGN_IDENTITY}...\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --timestamp=none --generate-entitlement-der \"${DEST_FRAMEWORKS}/App.framework\"\n fi\nelse\n echo \"No App.framework to embed (debug/JIT build).\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000082B4168C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C146FB1CF9000082B4168C /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = appletvos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TVOS_DEPLOYMENT_TARGET = 13.0; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.urlLauncherExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000082B41691 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = appletvos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TVOS_DEPLOYMENT_TARGET = 13.0; + }; + name = Debug; + }; + 97C147031CF9000082B41692 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.urlLauncherExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147041CF9000082B41691 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = appletvos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TVOS_DEPLOYMENT_TARGET = 13.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147041CF9000082B41692 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.urlLauncherExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000082B4168C /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000082B41691 /* Debug */, + 97C147041CF9000082B41691 /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000082B4168C /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000082B41692 /* Debug */, + 97C147041CF9000082B41692 /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + AAF40000000000000000F00D /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + AAF30000000000000000F00D /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000082B4168C /* Project object */; +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ee3561d --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/contents.xcworkspacedata b/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner/AppDelegate.swift b/packages/url_launcher_tvos/example/tvos/Runner/AppDelegate.swift new file mode 100644 index 0000000..acb0fac --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/AppDelegate.swift @@ -0,0 +1,31 @@ +import UIKit +import Flutter + +@main +class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil) + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = flutterViewController + window.makeKeyAndVisible() + self.window = window + + GeneratedPluginRegistrant.register(with: self) + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // When the example launches its own `ullauncherdemo://` scheme, tvOS delivers + // the URL back here — showing UIApplication.open() genuinely hands off. + override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + NSLog("url_launcher_tvos example opened via URL: \(url.absoluteString)") + return true + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AccentColor.colorset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..c6a0bc3 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "large_back.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/large_back.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/large_back.png new file mode 100644 index 0000000..b89e77a Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/large_back.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..f7cf529 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "large_front.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/large_front.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/large_front.png new file mode 100644 index 0000000..d1bf0b6 Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/large_front.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..fedb0ad --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "large_middle.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/large_middle.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/large_middle.png new file mode 100644 index 0000000..eca5900 Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/large_middle.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..1d59796 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "small_back.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/small_back.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/small_back.png new file mode 100644 index 0000000..eac8b47 Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/small_back.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..e8f0da2 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "small_front.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/small_front.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/small_front.png new file mode 100644 index 0000000..71eb8ae Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/small_front.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..9d01973 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "small_middle.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/small_middle.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/small_middle.png new file mode 100644 index 0000000..624d2bb Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/small_middle.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Contents.json new file mode 100644 index 0000000..5af3206 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Contents.json @@ -0,0 +1,26 @@ +{ + "assets" : [ + { + "filename" : "App Icon - Large.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "1280x768" + }, + { + "filename" : "App Icon - Small.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "400x240" + }, + { + "filename" : "Top Shelf Image.imageset", + "idiom" : "tv", + "role" : "top-shelf-image", + "size" : "1920x720" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000..74f7c24 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "top_shelf.png", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf.png b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf.png new file mode 100644 index 0000000..cbbebf8 Binary files /dev/null and b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/top_shelf.png differ diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/Contents.json b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/LaunchScreen.storyboard b/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..088a3ba --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/Main.storyboard b/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..4e805a1 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Info.plist b/packages/url_launcher_tvos/example/tvos/Runner/Info.plist new file mode 100644 index 0000000..6b586c5 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Info.plist @@ -0,0 +1,61 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Url_launcher_example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + url_launcher_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + + FLTAssetsPath + flutter_assets + + CFBundleURLTypes + + + CFBundleURLName + dev.fluttertv.urllauncherdemo + CFBundleURLSchemes + + ullauncherdemo + + + + + LSApplicationQueriesSchemes + + ullauncherdemo + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + + diff --git a/packages/url_launcher_tvos/example/tvos/Runner/Runner-Bridging-Header.h b/packages/url_launcher_tvos/example/tvos/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/packages/url_launcher_tvos/example/tvos/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/url_launcher_tvos/lib/src/messages.g.dart b/packages/url_launcher_tvos/lib/src/messages.g.dart new file mode 100644 index 0000000..f668ea3 --- /dev/null +++ b/packages/url_launcher_tvos/lib/src/messages.g.dart @@ -0,0 +1,226 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +/// Possible outcomes of launching a URL. +enum LaunchResult { + /// The URL was successfully launched (or could be, for `canLaunchUrl`). + success, + + /// There was no handler available for the URL. + failure, + + /// The URL could not be launched because it is invalid. + invalidUrl, +} + +/// Possible outcomes of handling a URL within the application. +enum InAppLoadResult { + /// The URL was successfully loaded. + success, + + /// The URL did not load successfully. + failedToLoad, + + /// The URL could not be launched because it is invalid. + invalidUrl, + + /// The URL could not be launched because no UI is available. + noUI, + + /// The controller was closed before loading. + dismissed, +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is LaunchResult) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is InAppLoadResult) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : LaunchResult.values[value]; + case 130: + final int? value = readValue(buffer) as int?; + return value == null ? null : InAppLoadResult.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } +} + +class UrlLauncherApi { + /// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + UrlLauncherApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Checks whether a URL can be loaded. + Future canLaunchUrl(String url) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as LaunchResult?)!; + } + } + + /// Opens the URL externally, returning the status of launching it. + Future launchUrl(String url, bool universalLinksOnly) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url, universalLinksOnly], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as LaunchResult?)!; + } + } + + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. + Future openUrlInSafariViewController(String url) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [url], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as InAppLoadResult?)!; + } + } + + /// Closes the view controller opened by [openUrlInSafariViewController]. + Future closeSafariViewController() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/url_launcher_tvos/lib/url_launcher_tvos.dart b/packages/url_launcher_tvos/lib/url_launcher_tvos.dart new file mode 100644 index 0000000..104dd95 --- /dev/null +++ b/packages/url_launcher_tvos/lib/url_launcher_tvos.dart @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import 'src/messages.g.dart'; + +/// An implementation of [UrlLauncherPlatform] for tvOS. +/// +/// tvOS has no web browser (no SafariServices/WebKit), so the in-app browser +/// modes are unsupported: [supportsMode] reports `false` for them and every +/// launch falls back to an external launch (`UIApplication.open`), matching the +/// other browser-less implementations (macOS/Windows/Linux). See +/// `PORTING_REPORT.md`. +/// +/// Note: [canLaunch] maps to `UIApplication.canOpenURL`, which on tvOS can +/// return `true` for an `http(s)` URL even when no installed app will actually +/// open it. Prefer the [launchUrl] return value over gating on [canLaunch]. +class UrlLauncherTvos extends UrlLauncherPlatform { + /// Creates a new plugin implementation instance. + UrlLauncherTvos({@visibleForTesting UrlLauncherApi? api}) + : _hostApi = api ?? UrlLauncherApi(); + + final UrlLauncherApi _hostApi; + + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherTvos(); + } + + @override + final LinkDelegate? linkDelegate = null; + + /// Whether some installed app can handle [url] (`UIApplication.canOpenURL`). + /// + /// On tvOS this can return `true` for an `http(s)` URL even when nothing will + /// open it (there is no browser), so prefer checking the [launchUrl] result. + @override + Future canLaunch(String url) async { + final LaunchResult result = await _hostApi.canLaunchUrl(url); + return _mapLaunchResult(result); + } + + @override + Future closeWebView() { + return _hostApi.closeSafariViewController(); + } + + @override + Future launch( + String url, { + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, + }) async { + final PreferredLaunchMode mode; + if (useSafariVC) { + mode = PreferredLaunchMode.inAppBrowserView; + } else if (universalLinksOnly) { + mode = PreferredLaunchMode.externalNonBrowserApplication; + } else { + mode = PreferredLaunchMode.externalApplication; + } + return launchUrl( + url, + LaunchOptions( + mode: mode, + webViewConfiguration: InAppWebViewConfiguration( + enableDomStorage: enableDomStorage, + enableJavaScript: enableJavaScript, + headers: headers, + ), + ), + ); + } + + @override + Future launchUrl(String url, LaunchOptions options) async { + // tvOS has no in-app browser: every mode falls back to an external launch, + // matching the browser-less macOS/Windows/Linux implementations (the + // platform interface encourages falling back over failing). An unclaimed URL + // returns false rather than throwing. + return _mapLaunchResult( + await _hostApi.launchUrl( + url, + options.mode == PreferredLaunchMode.externalNonBrowserApplication, + ), + ); + } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + switch (mode) { + case PreferredLaunchMode.platformDefault: + case PreferredLaunchMode.externalApplication: + case PreferredLaunchMode.externalNonBrowserApplication: + return true; + // tvOS has no SafariServices/WebKit, so the in-app browser modes are + // unavailable (unlike the iOS implementation, which supports them). + case PreferredLaunchMode.inAppWebView: + case PreferredLaunchMode.inAppBrowserView: + return false; + // Default is a desired behavior here since support for new modes is + // always opt-in, and the enum lives in a different package, so silently + // adding "false" for new values is the correct behavior. + // ignore: no_default_cases, unreachable_switch_default + default: + return false; + } + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + // No in-app browser on tvOS, so there is nothing to close for any mode. + return false; + } + + bool _mapLaunchResult(LaunchResult result) { + switch (result) { + case LaunchResult.success: + return true; + case LaunchResult.failure: + return false; + case LaunchResult.invalidUrl: + throw _invalidUrlException(); + } + } + + // TODO(stuartmorgan): Remove this as part of standardizing error handling. + // See https://github.com/flutter/flutter/issues/127665 + // + // This PlatformException (including the exact string details, since those + // are a defacto part of the API) is for compatibility with the previous + // native implementation. + PlatformException _invalidUrlException() { + throw PlatformException( + code: 'argument_error', + message: 'Unable to parse URL', + ); + } +} diff --git a/packages/url_launcher_tvos/pubspec.yaml b/packages/url_launcher_tvos/pubspec.yaml new file mode 100644 index 0000000..65fb5a3 --- /dev/null +++ b/packages/url_launcher_tvos/pubspec.yaml @@ -0,0 +1,29 @@ +name: url_launcher_tvos +description: "The tvOS (Apple TV) implementation of the url_launcher plugin, for launching URLs from Flutter apps." +version: 0.0.1 +homepage: https://fluttertv.dev +repository: https://github.com/fluttertv/plugins/tree/main/packages/url_launcher_tvos +issue_tracker: https://github.com/fluttertv/plugins/issues +# Generated by `flutter-tvos plugin port`. See PORTING_REPORT.md. +# License holder: The FlutterTV Authors + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.13.0" + +dependencies: + flutter: + sdk: flutter + url_launcher_platform_interface: ^2.2.0 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + tvos: + pluginClass: URLLauncherPlugin + dartPluginClass: UrlLauncherTvos diff --git a/packages/url_launcher_tvos/test/url_launcher_tvos_test.dart b/packages/url_launcher_tvos/test/url_launcher_tvos_test.dart new file mode 100644 index 0000000..9ed6842 --- /dev/null +++ b/packages/url_launcher_tvos/test/url_launcher_tvos_test.dart @@ -0,0 +1,194 @@ +// Copyright 2026 The FlutterTV Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Generated on 2026-08-19 by `flutter-tvos plugin port`. +// Source plugin: url_launcher_ios + +import 'package:flutter/services.dart' show BinaryMessenger; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'package:url_launcher_tvos/src/messages.g.dart'; +import 'package:url_launcher_tvos/url_launcher_tvos.dart'; + +/// A fake host API that records calls and returns canned results, so the tests +/// can assert *which* Pigeon channel a launch reaches without a real device. +class _FakeApi implements UrlLauncherApi { + final List calls = []; + LaunchResult canLaunchResult = LaunchResult.success; + LaunchResult launchResult = LaunchResult.success; + + // Names are dictated by the generated Pigeon `UrlLauncherApi` interface. + // ignore_for_file: non_constant_identifier_names + @override + BinaryMessenger? get pigeonVar_binaryMessenger => null; + + @override + String get pigeonVar_messageChannelSuffix => ''; + + @override + Future canLaunchUrl(String url) async { + calls.add('canLaunchUrl($url)'); + return canLaunchResult; + } + + @override + Future launchUrl(String url, bool universalLinksOnly) async { + calls.add('launchUrl($url, universalLinksOnly: $universalLinksOnly)'); + return launchResult; + } + + @override + Future openUrlInSafariViewController(String url) async { + calls.add('openUrlInSafariViewController($url)'); + return InAppLoadResult.noUI; + } + + @override + Future closeSafariViewController() async { + calls.add('closeSafariViewController()'); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('registerWith installs UrlLauncherTvos as the platform instance', () { + UrlLauncherTvos.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + group('every launch mode uses the external channel (no in-app browser)', () { + late _FakeApi api; + late UrlLauncherTvos launcher; + + setUp(() { + api = _FakeApi(); + launcher = UrlLauncherTvos(api: api); + }); + + test('externalApplication -> launchUrl channel', () async { + await launcher.launchUrl( + 'https://example.com', + const LaunchOptions(mode: PreferredLaunchMode.externalApplication), + ); + expect(api.calls, [ + 'launchUrl(https://example.com, universalLinksOnly: false)', + ]); + }); + + test('externalNonBrowserApplication -> universalLinksOnly = true', () async { + await launcher.launchUrl( + 'https://example.com', + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication, + ), + ); + expect(api.calls, [ + 'launchUrl(https://example.com, universalLinksOnly: true)', + ]); + }); + + test('platformDefault falls back to external, never in-app', () async { + await launcher.launchUrl( + 'https://example.com', + const LaunchOptions(mode: PreferredLaunchMode.platformDefault), + ); + expect(api.calls, [ + 'launchUrl(https://example.com, universalLinksOnly: false)', + ]); + }); + + test('inAppBrowserView falls back to external, does NOT throw', () async { + await launcher.launchUrl( + 'https://example.com', + const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView), + ); + // The in-app browser channel must NOT be reached on tvOS. + expect(api.calls, isNot(contains('openUrlInSafariViewController(https://example.com)'))); + expect(api.calls, [ + 'launchUrl(https://example.com, universalLinksOnly: false)', + ]); + }); + + test('inAppWebView falls back to external, does NOT throw', () async { + await launcher.launchUrl( + 'https://example.com', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView), + ); + expect(api.calls, [ + 'launchUrl(https://example.com, universalLinksOnly: false)', + ]); + }); + + // The regression the review flagged: the deprecated `launch()` infers + // `useSafariVC` from the URL scheme, so a bare web URL used to route to the + // in-app browser and throw. It must now launch externally. + test('deprecated launch(webUrl) launches externally, does NOT throw', + () async { + final bool result = await launcher.launch( + 'https://example.com/help', + useSafariVC: true, // what legacy_api.dart infers for an http(s) URL + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect(result, isTrue); + expect(api.calls, [ + 'launchUrl(https://example.com/help, universalLinksOnly: false)', + ]); + }); + + test('an unclaimed URL returns false rather than throwing', () async { + api.launchResult = LaunchResult.failure; + final bool result = await launcher.launchUrl( + 'https://example.com', + const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView), + ); + expect(result, isFalse); + }); + }); + + group('capabilities — tvOS has no in-app browser', () { + final UrlLauncherTvos launcher = UrlLauncherTvos(api: _FakeApi()); + + test('external modes are supported', () async { + expect( + await launcher.supportsMode(PreferredLaunchMode.externalApplication), + isTrue, + ); + expect( + await launcher.supportsMode( + PreferredLaunchMode.externalNonBrowserApplication, + ), + isTrue, + ); + expect( + await launcher.supportsMode(PreferredLaunchMode.platformDefault), + isTrue, + ); + }); + + test('in-app browser modes are NOT supported (unlike iOS)', () async { + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + isFalse, + ); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppWebView), + isFalse, + ); + }); + + test('nothing supports close, since there is no in-app browser', () async { + expect( + await launcher.supportsCloseForMode( + PreferredLaunchMode.inAppBrowserView, + ), + isFalse, + ); + }); + }); +} diff --git a/packages/url_launcher_tvos/tvos/Classes/Launcher.swift b/packages/url_launcher_tvos/tvos/Classes/Launcher.swift new file mode 100644 index 0000000..7b9cb1b --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Classes/Launcher.swift @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit + +/// Protocol for UIApplication methods relating to launching URLs. +/// +/// This protocol exists to allow injecting an alternate implementation for testing. +protocol Launcher { + /// Returns a Boolean value that indicates whether an app is available to handle a URL scheme. + func canOpenURL(_ url: URL) -> Bool + + /// Attempts to asynchronously open the resource at the specified URL. + func open( + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey: Any], + completionHandler completion: ((Bool) -> Void)?) +} + +// TODO(hellohuanlin): This wrapper is a workaround for iOS 18 Beta 3 where completionHandler is annotated with @MainActor @Sendable, resulting in compile error when conforming UIApplication to Launcher. We should try again in newer betas. +/// A default URL launcher. +final class DefaultLauncher: Launcher { + func canOpenURL(_ url: URL) -> Bool { + return UIApplication.shared.canOpenURL(url) + } + + func open( + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey: Any], + completionHandler completion: ((Bool) -> Void)? + ) { + UIApplication.shared.open( + url, + options: options, + completionHandler: completion) + } +} diff --git a/packages/url_launcher_tvos/tvos/Classes/URLLaunchSession.swift b/packages/url_launcher_tvos/tvos/Classes/URLLaunchSession.swift new file mode 100644 index 0000000..2a6db0c --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Classes/URLLaunchSession.swift @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +// SafariServices (SFSafariViewController) does not exist on tvOS, so the entire +// in-app browser session is compiled out. `URLLauncherPlugin` never references +// this type on tvOS — it returns `.noUI` for the in-app path instead. +#if !os(tvOS) + + import SafariServices + + typealias OpenInSafariCompletionHandler = (Result) -> Void + +/// A session responsible for launching a URL in Safari and handling its events. +final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { + + private let completion: OpenInSafariCompletionHandler + private let url: URL + private var isLoadCompleted: Bool = false + + /// The Safari view controller used for displaying the URL. + let safariViewController: SFSafariViewController + + // A closure to be executed after the Safari view controller finishes. + var didFinish: (() -> Void)? + + /// Initializes a new URLLaunchSession with the provided URL and completion handler. + /// + /// - Parameters: + /// - url: The URL to be opened in Safari. + /// - completion: The completion handler to be called after attempting to open the URL. + init(url: URL, completion: @escaping OpenInSafariCompletionHandler) { + self.url = url + self.completion = completion + self.safariViewController = SFSafariViewController(url: url) + super.init() + self.safariViewController.delegate = self + } + + /// Called when the Safari view controller completes the initial load. + /// + /// - Parameters: + /// - controller: The Safari view controller. + /// - didLoadSuccessfully: Indicates if the initial load was successful. + func safariViewController( + _ controller: SFSafariViewController, + didCompleteInitialLoad didLoadSuccessfully: Bool + ) { + if didLoadSuccessfully { + completion(.success(.success)) + } else { + completion(.success(.failedToLoad)) + } + isLoadCompleted = true + } + + /// Called when the user finishes using the Safari view controller. + /// + /// - Parameter controller: The Safari view controller. + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + if !isLoadCompleted { + completion(.success(.dismissed)) + } + controller.dismiss(animated: true, completion: nil) + didFinish?() + } + + /// Closes the Safari view controller. + func close() { + safariViewControllerDidFinish(safariViewController) + } +} + +#endif // !os(tvOS) diff --git a/packages/url_launcher_tvos/tvos/Classes/URLLauncherPlugin.swift b/packages/url_launcher_tvos/tvos/Classes/URLLauncherPlugin.swift new file mode 100644 index 0000000..64f7c4e --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Classes/URLLauncherPlugin.swift @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import UIKit + +public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { + + public static func register(with registrar: FlutterPluginRegistrar) { + let plugin = URLLauncherPlugin( + viewPresenterProvider: DefaultViewPresenterProvider(registrar: registrar)) + UrlLauncherApiSetup.setUp(binaryMessenger: registrar.messenger(), api: plugin) + registrar.publish(plugin) + } + + // tvOS has no SafariServices, so there is never an in-app browser session. + #if !os(tvOS) + private var currentSession: URLLaunchSession? + #endif + private let launcher: Launcher + /// The view presenter provider, for showing a Safari view controller. + private let viewPresenterProvider: ViewPresenterProvider + + init(launcher: Launcher = DefaultLauncher(), viewPresenterProvider: ViewPresenterProvider) { + self.launcher = launcher + self.viewPresenterProvider = viewPresenterProvider + } + + func canLaunchUrl(url: String) -> LaunchResult { + guard let url = URL(string: url) else { + return .invalidUrl + } + let canOpen = launcher.canOpenURL(url) + return canOpen ? .success : .failure + } + + func launchUrl( + url: String, + universalLinksOnly: Bool, + completion: @escaping (Result) -> Void + ) { + guard let url = URL(string: url) else { + completion(.success(.invalidUrl)) + return + } + let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] + launcher.open(url, options: options) { result in + completion(.success(result ? .success : .failure)) + } + } + + func openUrlInSafariViewController( + url: String, + completion: @escaping (Result) -> Void + ) { + guard let url = URL(string: url) else { + completion(.success(.invalidUrl)) + return + } + + #if os(tvOS) + // tvOS has no SafariServices / in-app browser. Report that no UI is + // available; callers should use an external launch mode instead. + completion(.success(.noUI)) + #else + guard let presenter = viewPresenterProvider.viewPresenter else { + completion(.success(.noUI)) + return + } + + let session = URLLaunchSession(url: url, completion: completion) + currentSession = session + + session.didFinish = { [weak self] in + self?.currentSession = nil + } + presenter.present(session.safariViewController, animated: true, completion: nil) + #endif + } + + func closeSafariViewController() { + // No-op on tvOS: there is never an in-app browser session to close. + #if !os(tvOS) + currentSession?.close() + #endif + } +} diff --git a/packages/url_launcher_tvos/tvos/Classes/ViewPresenter.swift b/packages/url_launcher_tvos/tvos/Classes/ViewPresenter.swift new file mode 100644 index 0000000..6375856 --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Classes/ViewPresenter.swift @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import UIKit + +/// Protocol for UIViewController methods relating to presenting a controller. +/// +/// This protocol exists to allow injecting an alternate implementation for testing. +protocol ViewPresenter { + /// Presents a view controller modally. + func present( + _ viewControllerToPresent: UIViewController, + animated flag: Bool, + completion: (() -> Void)? + ) +} + +/// ViewPresenter is intentionally a direct passthroguh to UIViewController. +extension UIViewController: ViewPresenter {} + +/// Protocol for FlutterPluginRegistrar method for accessing the view controller. +/// +/// This is necessary because Swift doesn't allow for only partially implementing a protocol, so +/// a stub implementation of FlutterPluginRegistrar for tests would break any time something was +/// added to that protocol. +protocol ViewPresenterProvider { + /// Returns the view presenter associated with the Flutter content. + var viewPresenter: ViewPresenter? { get } +} + +/// Non-test implementation of ViewPresenterProvider that forwards to the plugin registrar. +final class DefaultViewPresenterProvider: ViewPresenterProvider { + private let registrar: FlutterPluginRegistrar + + init(registrar: FlutterPluginRegistrar) { + self.registrar = registrar + } + + var viewPresenter: ViewPresenter? { + registrar.viewController + } +} diff --git a/packages/url_launcher_tvos/tvos/Classes/messages.g.swift b/packages/url_launcher_tvos/tvos/Classes/messages.g.swift new file mode 100644 index 0000000..a3a7b98 --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Classes/messages.g.swift @@ -0,0 +1,246 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) || os(tvOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// Possible outcomes of launching a URL. +enum LaunchResult: Int { + /// The URL was successfully launched (or could be, for `canLaunchUrl`). + case success = 0 + /// There was no handler available for the URL. + case failure = 1 + /// The URL could not be launched because it is invalid. + case invalidUrl = 2 +} + +/// Possible outcomes of handling a URL within the application. +enum InAppLoadResult: Int { + /// The URL was successfully loaded. + case success = 0 + /// The URL did not load successfully. + case failedToLoad = 1 + /// The URL could not be launched because it is invalid. + case invalidUrl = 2 + /// The URL could not be launched because no UI is available. + case noUI = 3 + /// The controller was closed before loading. + case dismissed = 4 +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return LaunchResult(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return InAppLoadResult(rawValue: enumResultAsInt) + } + return nil + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? LaunchResult { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? InAppLoadResult { + super.writeByte(130) + super.writeValue(value.rawValue) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol UrlLauncherApi { + /// Checks whether a URL can be loaded. + func canLaunchUrl(url: String) throws -> LaunchResult + /// Opens the URL externally, returning the status of launching it. + func launchUrl( + url: String, universalLinksOnly: Bool, + completion: @escaping (Result) -> Void) + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. + func openUrlInSafariViewController( + url: String, completion: @escaping (Result) -> Void) + /// Closes the view controller opened by [openUrlInSafariViewController]. + func closeSafariViewController() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class UrlLauncherApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?, messageChannelSuffix: String = "" + ) { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Checks whether a URL can be loaded. + let canLaunchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + canLaunchUrlChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let urlArg = args[0] as! String + do { + let result = try api.canLaunchUrl(url: urlArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + canLaunchUrlChannel.setMessageHandler(nil) + } + /// Opens the URL externally, returning the status of launching it. + let launchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + launchUrlChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let urlArg = args[0] as! String + let universalLinksOnlyArg = args[1] as! Bool + api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + launchUrlChannel.setMessageHandler(nil) + } + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. + let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let urlArg = args[0] as! String + api.openUrlInSafariViewController(url: urlArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + openUrlInSafariViewControllerChannel.setMessageHandler(nil) + } + /// Closes the view controller opened by [openUrlInSafariViewController]. + let closeSafariViewControllerChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + closeSafariViewControllerChannel.setMessageHandler { _, reply in + do { + try api.closeSafariViewController() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + closeSafariViewControllerChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/url_launcher_tvos/tvos/Package.swift b/packages/url_launcher_tvos/tvos/Package.swift new file mode 100644 index 0000000..081a719 --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 +// Generated by `flutter-tvos plugin port`. Do not edit by hand. +// +// This manifest lets the package be consumed via Swift Package Manager. The +// target reuses the same `Classes/` sources the CocoaPods podspec compiles, so +// both dependency managers stay in sync from a single source tree. +import PackageDescription + +let package = Package( + name: "url_launcher_tvos", + platforms: [ + .tvOS(.v13), + ], + products: [ + .library(name: "url-launcher-tvos", targets: ["url_launcher_tvos"]), + ], + dependencies: [ + // Lets the target `import Flutter`. flutter-tvos generates a FlutterFramework + // package (binary target wrapping Flutter.xcframework) as a sibling of this + // package under the app's ephemeral SwiftPM packages, so `../FlutterFramework` + // resolves at build time. Matches stock Flutter's plugin Package.swift. + .package(name: "FlutterFramework", path: "../FlutterFramework"), + ], + targets: [ + .target( + name: "url_launcher_tvos", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework"), + ], + path: "Classes" + ), + ] +) diff --git a/packages/url_launcher_tvos/tvos/Resources/PrivacyInfo.xcprivacy b/packages/url_launcher_tvos/tvos/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a34b7e2 --- /dev/null +++ b/packages/url_launcher_tvos/tvos/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/url_launcher_tvos/tvos/url_launcher_tvos.podspec b/packages/url_launcher_tvos/tvos/url_launcher_tvos.podspec new file mode 100644 index 0000000..5e757f9 --- /dev/null +++ b/packages/url_launcher_tvos/tvos/url_launcher_tvos.podspec @@ -0,0 +1,32 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint url_launcher_tvos.podspec` to validate before publishing. +# +# Generated by `flutter-tvos plugin port`. License holder: The FlutterTV Authors. +# +Pod::Spec.new do |s| + s.name = 'url_launcher_tvos' + s.version = '0.0.1' + s.summary = 'tvOS implementation of url_launcher.' + s.description = <<-DESC +tvOS implementation of url_launcher, the federated platform +package that ships native code targeting Apple tvOS. + DESC + s.homepage = 'https://github.com/fluttertv/plugins/tree/main/packages/url_launcher_tvos' + s.license = { :file => '../LICENSE' } + s.author = { 'The FlutterTV Authors' => 'noreply@fluttertv.dev' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*.swift' + s.resource_bundles = { 'url_launcher_tvos_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } + s.platform = :tvos, '13.0' + s.swift_version = '5.0' + + # IMPORTANT: this podspec must not depend on the Flutter CocoaPod. That + # pod does not declare tvOS support, so adding a dependency on it breaks + # `pod install` for tvOS consumers. Flutter.framework is resolved via + # FRAMEWORK_SEARCH_PATHS, populated by the host app's Podfile. + s.xcconfig = { + 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/../Flutter"', + } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end