mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-20 23:11:24 +02:00
fix(deep-link): filter out web links instead of app links in deep link domains (#3382)
* fix(deep-link): filter out web links instead of app links in deep link domains
This fixes a bug where on ios, deeplinks just don't work because the final Info.plist file doesn't have them.
And also used a better id for CFBundleURLName
Fixes regression from 2804803949
* feat(deep-link): add `webcredentials:` for iOS entitlements file
* chore: added changelog for deep-link patch
* add tests
* remove webcredentials
* -js
---------
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
bbbdd9dd54
commit
c92bb67d7a
15 files changed
+476
-104
No files matched your search
@@ -94,6 +94,8 @@ To verify if your domain has been properly configured to expose the app associat
|
||||
curl -v https://app-site-association.cdn-apple.com/a/v1/<host>
|
||||
```
|
||||
|
||||
The plugin only manages the `applinks:` entries of the `com.apple.developer.associated-domains` entitlement. Other entries you add to your app's entitlements file, such as `webcredentials:<host>` for [Password AutoFill or passkeys](https://developer.apple.com/documentation/xcode/supporting-associated-domains), are kept.
|
||||
|
||||
**The apple-app-site-association file must be served over HTTPS and the response must include the `Content-Type: application/json` header.**
|
||||
|
||||
To quickly open an app link on the iOS simulator you can execute `xcrun simctl openurl booted <url>`.
|
||||
|
||||
+37
-48
@@ -115,77 +115,66 @@ fn main() {
|
||||
// generated for explicit app links and not
|
||||
// other deep links because then they
|
||||
// are just going to complain and not be built or signed
|
||||
let has_app_links = config.mobile.iter().any(|d| d.is_app_link());
|
||||
tauri_plugin::mobile::update_entitlements(|entitlements| {
|
||||
let current = entitlements
|
||||
.get("com.apple.developer.associated-domains")
|
||||
.and_then(|domains| domains.as_array())
|
||||
.map(|domains| {
|
||||
domains
|
||||
.iter()
|
||||
.filter_map(|domain| domain.as_string())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !has_app_links {
|
||||
tauri_plugin::mobile::update_entitlements(|entitlements| {
|
||||
let associated_domains = config.ios_associated_domains(¤t);
|
||||
|
||||
if associated_domains.is_empty() {
|
||||
entitlements.remove("com.apple.developer.associated-domains");
|
||||
})
|
||||
.expect("failed to update entitlements");
|
||||
} else {
|
||||
tauri_plugin::mobile::update_entitlements(|entitlements| {
|
||||
} else {
|
||||
entitlements.insert(
|
||||
"com.apple.developer.associated-domains".into(),
|
||||
config
|
||||
.mobile
|
||||
.iter()
|
||||
.filter(|d| d.is_app_link())
|
||||
.filter_map(|d| d.host.as_ref())
|
||||
.map(|host| format!("applinks:{}", host).into())
|
||||
associated_domains
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
);
|
||||
})
|
||||
.expect("failed to update entitlements");
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to update entitlements");
|
||||
|
||||
let deep_link_domains = config
|
||||
.mobile
|
||||
.iter()
|
||||
.filter(|domain| !domain.is_app_link())
|
||||
.collect::<Vec<_>>();
|
||||
let url_types = config.ios_url_types();
|
||||
|
||||
if deep_link_domains.is_empty() {
|
||||
tauri_plugin::mobile::update_info_plist(|info_plist| {
|
||||
tauri_plugin::mobile::update_info_plist(|info_plist| {
|
||||
if url_types.is_empty() {
|
||||
info_plist.remove("CFBundleURLTypes");
|
||||
})
|
||||
.expect("failed to update Info.plist");
|
||||
} else {
|
||||
tauri_plugin::mobile::update_info_plist(|info_plist| {
|
||||
} else {
|
||||
info_plist.insert(
|
||||
"CFBundleURLTypes".into(),
|
||||
deep_link_domains
|
||||
.iter()
|
||||
.map(|domain| {
|
||||
let schemes = domain
|
||||
.scheme
|
||||
.iter()
|
||||
.filter(|scheme| {
|
||||
scheme.as_str() != "https" && scheme.as_str() != "http"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
url_types
|
||||
.into_iter()
|
||||
.map(|url_type| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
dict.insert(
|
||||
"CFBundleURLSchemes".into(),
|
||||
schemes
|
||||
.iter()
|
||||
.map(|s| s.to_string().into())
|
||||
url_type
|
||||
.schemes
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleURLName".into(),
|
||||
domain.scheme[0].clone().into(),
|
||||
);
|
||||
dict.insert("CFBundleURLName".into(), url_type.name.into());
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
);
|
||||
})
|
||||
.expect("failed to update Info.plist");
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to update Info.plist");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [ "DEVELOPMENT_TEAM_ID_HERE.com.tauri.deep-link-example" ],
|
||||
"appIDs": ["DEVELOPMENT_TEAM_ID_HERE.com.tauri.deep-link-example"],
|
||||
"components": [
|
||||
{
|
||||
"/": "/open/*",
|
||||
@@ -12,4 +12,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ http
|
||||
fs.readFile(
|
||||
path.resolve(__dirname, 'apple-app-site-association'),
|
||||
function (_error, content) {
|
||||
response.writeHead(200)
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' })
|
||||
response.end(content, 'utf-8')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,6 +37,14 @@ impl AssociatedDomain {
|
||||
.unwrap_or_else(|| self.is_web_link() && self.host.is_some())
|
||||
}
|
||||
|
||||
/// The custom URL schemes of this domain, i.e. every scheme but http and https.
|
||||
///
|
||||
/// A domain may list both (e.g. `["https", "myapp"]`), so this is not the same as `!is_web_link()`.
|
||||
#[allow(dead_code)] // Used in build.rs
|
||||
pub fn custom_schemes(&self) -> impl Iterator<Item = &String> {
|
||||
self.scheme.iter().filter(|s| *s != "https" && *s != "http")
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// Rule 1: All web links require a host.
|
||||
if self.is_web_link() && self.host.is_none() {
|
||||
@@ -92,6 +100,54 @@ pub struct Config {
|
||||
pub desktop: DesktopProtocol,
|
||||
}
|
||||
|
||||
/// An entry of the iOS `CFBundleURLTypes` Info.plist key.
|
||||
#[allow(dead_code)] // Used in build.rs
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct IosUrlType {
|
||||
/// `CFBundleURLName`
|
||||
pub name: String,
|
||||
/// `CFBundleURLSchemes`
|
||||
pub schemes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// The `com.apple.developer.associated-domains` entitlement values, given its current ones.
|
||||
///
|
||||
/// The plugin only manages the `applinks:` entries: they are replaced by the configured app links,
|
||||
/// and every other entry (e.g. `webcredentials:` for passkeys) the app added is kept.
|
||||
#[allow(dead_code)] // Used in build.rs
|
||||
pub fn ios_associated_domains(&self, current: &[String]) -> Vec<String> {
|
||||
self.mobile
|
||||
.iter()
|
||||
.filter(|d| d.is_app_link())
|
||||
.filter_map(|d| d.host.as_ref())
|
||||
.map(|host| format!("applinks:{host}"))
|
||||
.chain(
|
||||
current
|
||||
.iter()
|
||||
.filter(|entry| !entry.starts_with("applinks:"))
|
||||
.cloned(),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The `CFBundleURLTypes` entries for the configured custom URL schemes.
|
||||
///
|
||||
/// http and https are handled through app links (associated domains) on iOS,
|
||||
/// so they are never registered as URL schemes.
|
||||
#[allow(dead_code)] // Used in build.rs
|
||||
pub fn ios_url_types(&self) -> Vec<IosUrlType> {
|
||||
self.mobile
|
||||
.iter()
|
||||
.filter_map(|domain| {
|
||||
let schemes = domain.custom_schemes().cloned().collect::<Vec<_>>();
|
||||
let name = format!("$(PRODUCT_BUNDLE_IDENTIFIER).{}", schemes.first()?);
|
||||
Some(IosUrlType { name, schemes })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[allow(unused)] // Used in tauri-bundler
|
||||
@@ -128,3 +184,98 @@ impl DesktopProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn config(mobile: serde_json::Value) -> Config {
|
||||
serde_json::from_value(serde_json::json!({ "mobile": mobile })).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ios_url_types_only_register_custom_schemes() {
|
||||
let config = config(serde_json::json!([
|
||||
{ "scheme": ["myapp"] },
|
||||
{ "scheme": ["https"], "host": "example.com" },
|
||||
{ "scheme": ["https"], "host": "web.example.com", "appLink": false },
|
||||
{ "host": "default-schemes.example.com" },
|
||||
{ "scheme": ["https", "mixed"], "host": "mixed.example.com" },
|
||||
{ "scheme": ["http", "mixed-web", "mixed-web2"], "host": "mixed-web.example.com", "appLink": false },
|
||||
{ "scheme": [] },
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
config.ios_url_types(),
|
||||
vec![
|
||||
IosUrlType {
|
||||
name: "$(PRODUCT_BUNDLE_IDENTIFIER).myapp".into(),
|
||||
schemes: vec!["myapp".into()],
|
||||
},
|
||||
IosUrlType {
|
||||
name: "$(PRODUCT_BUNDLE_IDENTIFIER).mixed".into(),
|
||||
schemes: vec!["mixed".into()],
|
||||
},
|
||||
IosUrlType {
|
||||
name: "$(PRODUCT_BUNDLE_IDENTIFIER).mixed-web".into(),
|
||||
schemes: vec!["mixed-web".into(), "mixed-web2".into()],
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ios_associated_domains_only_include_app_links() {
|
||||
let config = config(serde_json::json!([
|
||||
{ "scheme": ["myapp"] },
|
||||
{ "scheme": ["https"], "host": "example.com" },
|
||||
{ "scheme": ["https"], "host": "web.example.com", "appLink": false },
|
||||
{ "scheme": ["https", "mixed"], "host": "mixed.example.com" },
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
config.ios_associated_domains(&[]),
|
||||
vec!["applinks:example.com", "applinks:mixed.example.com"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ios_associated_domains_replace_app_links_and_keep_other_entries() {
|
||||
let config = config(serde_json::json!([
|
||||
{ "host": "example.com" },
|
||||
{ "host": "new.example.com" },
|
||||
]));
|
||||
|
||||
let current = [
|
||||
"applinks:example.com".to_string(),
|
||||
"webcredentials:example.com".to_string(),
|
||||
"applinks:removed.example.com".to_string(),
|
||||
"activitycontinuation:example.com".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
config.ios_associated_domains(¤t),
|
||||
vec![
|
||||
"applinks:example.com",
|
||||
"applinks:new.example.com",
|
||||
"webcredentials:example.com",
|
||||
"activitycontinuation:example.com",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ios_associated_domains_keep_other_entries_without_app_links() {
|
||||
let config = config(serde_json::json!([{ "scheme": ["myapp"] }]));
|
||||
|
||||
assert_eq!(
|
||||
config.ios_associated_domains(&["webcredentials:example.com".to_string()]),
|
||||
vec!["webcredentials:example.com"]
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.ios_associated_domains(&["applinks:example.com".to_string()])
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user