diff --git a/MacMagazine/MacMagazine/MainApp/MainViewModel.swift b/MacMagazine/MacMagazine/MainApp/MainViewModel.swift index cac6a6b0..d233e8b4 100644 --- a/MacMagazine/MacMagazine/MainApp/MainViewModel.swift +++ b/MacMagazine/MacMagazine/MainApp/MainViewModel.swift @@ -85,6 +85,18 @@ class MainViewModel { } } +// MARK: - Deep Link + +extension MainViewModel { + func openDeepLink(_ url: URL, source: String = "widget") { + deepLinkPostURL = url.absoluteString + analytics.track(.buttonTap( + buttonId: AnalyticsConstants.ButtonID.deepLinkOpened(source).id, + screen: AnalyticsConstants.Screen.deepLinkDetail.name + )) + } +} + // MARK: - Onboarding extension MainViewModel { diff --git a/MacMagazine/MacMagazine/MainApp/SceneDelegate.swift b/MacMagazine/MacMagazine/MainApp/SceneDelegate.swift index 3bf4a862..a173827d 100644 --- a/MacMagazine/MacMagazine/MainApp/SceneDelegate.swift +++ b/MacMagazine/MacMagazine/MainApp/SceneDelegate.swift @@ -6,6 +6,7 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? let pushNotification = PushNotification() + private var viewModel: MainViewModel? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, @@ -16,6 +17,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { (UIApplication.shared.delegate as? AppDelegate)?.pushNotification = pushNotification let viewModel = MainViewModel(pushNotification: pushNotification) + self.viewModel = viewModel window = UIWindow(windowScene: windowScene) window?.rootViewController = UIHostingController(rootView: SceneView(viewModel: viewModel)) @@ -24,6 +26,28 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let shortcutItem = connectionOptions.shortcutItem { ShortcutManager.shared.pendingShortcut = shortcutItem } + openDeepLink(from: connectionOptions.urlContexts) + openUniversalLink(from: connectionOptions.userActivities) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + openDeepLink(from: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + openUniversalLink(from: [userActivity]) + } + + private func openDeepLink(from contexts: Set) { + guard let url = contexts.first?.url else { return } + viewModel?.openDeepLink(url) + } + + private func openUniversalLink(from activities: Set) { + guard let url = activities + .first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb })? + .webpageURL else { return } + viewModel?.openDeepLink(url, source: "universal_link") } func windowScene(_ windowScene: UIWindowScene, diff --git a/MacMagazine/MacMagazine/Resources/MacMagazine.entitlements b/MacMagazine/MacMagazine/Resources/MacMagazine.entitlements index 916acf14..634fd191 100644 --- a/MacMagazine/MacMagazine/Resources/MacMagazine.entitlements +++ b/MacMagazine/MacMagazine/Resources/MacMagazine.entitlements @@ -6,6 +6,11 @@ Development com.apple.developer.aps-environment Development + com.apple.developer.associated-domains + + applinks:macmagazine.com.br + applinks:www.macmagazine.com.br + com.apple.developer.icloud-container-identifiers iCloud.com.brit.macmagazine.cloudkit diff --git a/MacMagazine/MacMagazine/Resources/MacMagazineRelease.entitlements b/MacMagazine/MacMagazine/Resources/MacMagazineRelease.entitlements index cdcc7da8..c059d039 100644 --- a/MacMagazine/MacMagazine/Resources/MacMagazineRelease.entitlements +++ b/MacMagazine/MacMagazine/Resources/MacMagazineRelease.entitlements @@ -6,6 +6,11 @@ Production com.apple.developer.aps-environment Production + com.apple.developer.associated-domains + + applinks:macmagazine.com.br + applinks:www.macmagazine.com.br + com.apple.developer.icloud-container-identifiers iCloud.com.brit.macmagazine.cloudkit diff --git a/Support/AssociatedDomains/README.md b/Support/AssociatedDomains/README.md new file mode 100644 index 00000000..425dd6b5 --- /dev/null +++ b/Support/AssociatedDomains/README.md @@ -0,0 +1,39 @@ +# Associated Domains (Universal Links) + +Reference copy of the `apple-app-site-association` (AASA) file that must be hosted for +Universal Links to open `macmagazine.com.br` post URLs in the app instead of Safari. + +## Hosting requirements + +Serve the `apple-app-site-association` file (this exact content, **no `.json` extension**) at: + +``` +https://macmagazine.com.br/.well-known/apple-app-site-association +https://www.macmagazine.com.br/.well-known/apple-app-site-association +``` + +- HTTPS only, valid certificate. +- `Content-Type: application/json`. +- **No redirects** — must return `200` directly. +- Both apex and `www` must serve it (the app declares `applinks:` for both). + +## Scope + +`components` allows only `/post/*`, so only article URLs open the app; every other path +(homepage, categories, `/wp-admin`, etc.) stays in the browser. + +## App side (already in the repo) + +- `com.apple.developer.associated-domains` in `MacMagazine/Resources/MacMagazine.entitlements` + and `MacMagazineRelease.entitlements`. +- `SceneDelegate` handles the browsing-web `NSUserActivity` (cold + warm launch). + +Enable the **Associated Domains** capability on the `com.brit.macmagazine` App ID in the +Developer portal and regenerate provisioning profiles before device/TestFlight builds. + +## Verify after hosting + +```bash +curl -sI https://macmagazine.com.br/.well-known/apple-app-site-association +xcrun simctl openurl booted https://macmagazine.com.br/post/2025/12/03// +``` diff --git a/Support/AssociatedDomains/apple-app-site-association b/Support/AssociatedDomains/apple-app-site-association new file mode 100644 index 00000000..5b9c6633 --- /dev/null +++ b/Support/AssociatedDomains/apple-app-site-association @@ -0,0 +1,15 @@ +{ + "applinks": { + "details": [ + { + "appIDs": ["A5VW9QUF9L.com.brit.macmagazine"], + "components": [ + { + "/": "/post/*", + "comment": "Only post URLs open the app; everything else stays in the browser." + } + ] + } + ] + } +}