From f0fb25a9b75f93351fb3b4d5931f33570acbec44 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Thu, 16 May 2024 02:09:52 +0300 Subject: [PATCH] feat(shell): support opening URLs on mobile (#1319) * feat(shell): support opening URLs on mobile closes #595 * Update and rename StorePlugin.swift to ShellPlugin.swift * unwrap * fix func name (ios) * use undeprecated func if avail --------- Co-authored-by: fabianlars --- plugins/shell/android/.gitignore | 2 + plugins/shell/android/build.gradle.kts | 40 +++++++++++++++++++ plugins/shell/android/proguard-rules.pro | 21 ++++++++++ plugins/shell/android/settings.gradle | 2 + .../android/src/main/AndroidManifest.xml | 3 ++ .../android/src/main/java/ShellPlugin.kt | 30 ++++++++++++++ plugins/shell/build.rs | 15 +++++++ plugins/shell/ios/Package.resolved | 16 ++++++++ plugins/shell/ios/Package.swift | 33 +++++++++++++++ plugins/shell/ios/Sources/ShellPlugin.swift | 34 ++++++++++++++++ plugins/shell/src/error.rs | 3 ++ plugins/shell/src/lib.rs | 30 ++++++++++++++ plugins/store/android/.gitignore | 2 + 13 files changed, 231 insertions(+) create mode 100644 plugins/shell/android/.gitignore create mode 100644 plugins/shell/android/build.gradle.kts create mode 100644 plugins/shell/android/proguard-rules.pro create mode 100644 plugins/shell/android/settings.gradle create mode 100644 plugins/shell/android/src/main/AndroidManifest.xml create mode 100644 plugins/shell/android/src/main/java/ShellPlugin.kt create mode 100644 plugins/shell/ios/Package.resolved create mode 100644 plugins/shell/ios/Package.swift create mode 100644 plugins/shell/ios/Sources/ShellPlugin.swift create mode 100644 plugins/store/android/.gitignore diff --git a/plugins/shell/android/.gitignore b/plugins/shell/android/.gitignore new file mode 100644 index 00000000..c0f21ec2 --- /dev/null +++ b/plugins/shell/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/shell/android/build.gradle.kts b/plugins/shell/android/build.gradle.kts new file mode 100644 index 00000000..31a1283e --- /dev/null +++ b/plugins/shell/android/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.shell" + compileSdk = 33 + + defaultConfig { + minSdk = 19 + targetSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation(project(":tauri-android")) +} diff --git a/plugins/shell/android/proguard-rules.pro b/plugins/shell/android/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/plugins/shell/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/shell/android/settings.gradle b/plugins/shell/android/settings.gradle new file mode 100644 index 00000000..14a752e4 --- /dev/null +++ b/plugins/shell/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/shell/android/src/main/AndroidManifest.xml b/plugins/shell/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/plugins/shell/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/shell/android/src/main/java/ShellPlugin.kt b/plugins/shell/android/src/main/java/ShellPlugin.kt new file mode 100644 index 00000000..4839483c --- /dev/null +++ b/plugins/shell/android/src/main/java/ShellPlugin.kt @@ -0,0 +1,30 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.shell + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.Plugin +import java.io.File + +@TauriPlugin +class ShellPlugin(private val activity: Activity) : Plugin(activity) { + @Command + fun open(invoke: Invoke) { + try { + val url = invoke.parseArgs(String::class.java) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity?.applicationContext?.startActivity(intent) + invoke.resolve() + } catch (ex: Exception) { + invoke.reject(ex.message) + } + } +} \ No newline at end of file diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs index 6a728570..fbfbb470 100644 --- a/plugins/shell/build.rs +++ b/plugins/shell/build.rs @@ -11,5 +11,20 @@ fn main() { tauri_plugin::Builder::new(COMMANDS) .global_api_script_path("./api-iife.js") .global_scope_schema(schemars::schema_for!(scope_entry::Entry)) + .android_path("android") + .ios_path("ios") .build(); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } } diff --git a/plugins/shell/ios/Package.resolved b/plugins/shell/ios/Package.resolved new file mode 100644 index 00000000..5f998e0e --- /dev/null +++ b/plugins/shell/ios/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftRs", + "repositoryURL": "https://github.com/Brendonovich/swift-rs", + "state": { + "branch": null, + "revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e", + "version": "1.0.6" + } + } + ] + }, + "version": 1 +} diff --git a/plugins/shell/ios/Package.swift b/plugins/shell/ios/Package.swift new file mode 100644 index 00000000..fa5d363d --- /dev/null +++ b/plugins/shell/ios/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-shell", + platforms: [ + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-shell", + type: .static, + targets: ["tauri-plugin-shell"]), + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-shell", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/shell/ios/Sources/ShellPlugin.swift b/plugins/shell/ios/Sources/ShellPlugin.swift new file mode 100644 index 00000000..0fcb7dac --- /dev/null +++ b/plugins/shell/ios/Sources/ShellPlugin.swift @@ -0,0 +1,34 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +import SwiftRs +import Tauri +import UIKit +import WebKit + +class ShellPlugin: Plugin { + + @objc public func open(_ invoke: Invoke) throws { + do { + let urlString = try invoke.parseArgs(String.self) + if let url = URL(string: urlString) { + if #available(iOS 10, *) { + UIApplication.shared.open(url, options: [:]) + } else { + UIApplication.shared.openURL(url) + } + } + invoke.resolve() + } catch { + invoke.reject(error.localizedDescription) + } + } +} + +@_cdecl("init_plugin_shell") +func initPlugin() -> Plugin { + return ShellPlugin() +} diff --git a/plugins/shell/src/error.rs b/plugins/shell/src/error.rs index 99b13cfd..dfed22a1 100644 --- a/plugins/shell/src/error.rs +++ b/plugins/shell/src/error.rs @@ -8,6 +8,9 @@ use serde::{Serialize, Serializer}; #[derive(Debug, thiserror::Error)] pub enum Error { + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), #[error(transparent)] Io(#[from] std::io::Error), #[error("current executable path has no parent")] diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 1f92c6cc..e5f71688 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -35,11 +35,21 @@ mod scope_entry; pub use error::Error; type Result = std::result::Result; + +#[cfg(mobile)] +use tauri::plugin::PluginHandle; +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.shell"; +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_shell); + type ChildStore = Arc>>; pub struct Shell { #[allow(dead_code)] app: AppHandle, + #[cfg(mobile)] + mobile_plugin_handle: PluginHandle, open_scope: scope::OpenScope, children: ChildStore, } @@ -61,9 +71,20 @@ impl Shell { /// Open a (url) path with a default or specific browser opening program. /// /// See [`crate::open::open`] for how it handles security-related measures. + #[cfg(desktop)] pub fn open(&self, path: impl Into, with: Option) -> Result<()> { open::open(&self.open_scope, path.into(), with).map_err(Into::into) } + + /// Open a (url) path with a default or specific browser opening program. + /// + /// See [`crate::open::open`] for how it handles security-related measures. + #[cfg(mobile)] + pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin("open", path.into()) + .map_err(Into::into) + } } pub trait ShellExt { @@ -89,10 +110,19 @@ pub fn init() -> TauriPlugin> { .setup(|app, api| { let default_config = config::Config::default(); let config = api.config().as_ref().unwrap_or(&default_config); + + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_shell)?; + app.manage(Shell { app: app.clone(), children: Default::default(), open_scope: open_scope(&config.open), + + #[cfg(mobile)] + mobile_plugin_handle: handle, }); Ok(()) }) diff --git a/plugins/store/android/.gitignore b/plugins/store/android/.gitignore new file mode 100644 index 00000000..c0f21ec2 --- /dev/null +++ b/plugins/store/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri