Skip to content

feat: Update hotwire-native-ios to 1.2 #224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/turbo/ios/RNSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ extension RNSession: SessionDelegate {
func sessionDidFinishFormSubmission(_ session: Session) {
visitableView?.didFinishFormSubmission()
}

func session(_ session: Session, decidePolicyFor navigationAction: WKNavigationAction) -> WebViewPolicyManager.Decision {
guard let url = navigationAction.request.url else {
return .allow
}
// regardless of the return value here nothing happens, so we have to manually open external URL
visitableView?.didOpenExternalUrl(url: url)
return .allow
}
Comment on lines +120 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has introduced a new behavior in our app. This seems to be called for on-page <iframe src=".."> elements.

As an example, in our app we use Google Tag Manager. They have an initialization script that includes an <iframe src="https://www.googletagmanager.com/ns.html?...">. On every page view in the app, we see that didOpenExternalUrl is called with the Google Tag Manager URL with no way to differentiate if it was a user-decided navigation action or an on-page iframe.

Copy link
Contributor

@pfeiffer pfeiffer May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hotwire Native has an extension that provides some additional properties to indicate if the navigationAction happened in the main frame etc: https://github.com/hotwired/hotwire-native-ios/blob/f18b87938f55cbe8976e96702b5512fa9afcd674/Source/Turbo/Navigator/Extensions/WKNavigationAction%2BUtils.swift#L1C1-L12C1

I think there's a few things going on here:

  1. The previous Session#openExternalURL that we relied on to trigger the didOpenExternalUrl on the RN-side was changed in Hotwire Native iOS to support the cross-origin redirects.
  2. We have not yet fully supported the change
  3. The current implementation in this PR is to naive and triggers even for iframes etc.

}

extension RNSession: WKScriptMessageHandler {
Expand Down
6 changes: 3 additions & 3 deletions packages/turbo/ios/RNVisitableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ class RNVisitableView: UIView, RNSessionSubscriber {
}

private func visit() {
if (controller?.visitableURL?.absoluteString == url as String) {
if (controller?.currentVisitableURL.absoluteString == url as String) {
return
}
controller!.visitableURL = URL(string: String(url))
controller!.initializeVisit(url: URL(string: String(url))!)
session?.visit(controller!)
}

Expand Down Expand Up @@ -218,7 +218,7 @@ class RNVisitableView: UIView, RNSessionSubscriber {

public func didFailRequestForVisitable(visitable: Visitable, error: Error){
let event: [AnyHashable: Any] = [
"url": visitable.visitableURL.absoluteString,
"url": visitable.currentVisitableURL.absoluteString,
"description": error.localizedDescription,
"statusCode": getStatusCodeFromError(error: error as? TurboError)
]
Expand Down
58 changes: 52 additions & 6 deletions packages/turbo/ios/RNVisitableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import WebKit

public protocol RNVisitableViewControllerDelegate {

Expand All @@ -29,14 +30,28 @@ class RNVisitableViewController: UIViewController, Visitable {
public var delegate: RNVisitableViewControllerDelegate?

open weak var visitableDelegate: VisitableDelegate?
open var visitableURL: URL!
public var initialVisitableURL: URL
public var currentVisitableURL: URL {
resolveVisitableLocation()
}

private var reactViewController: UIViewController? = nil

public convenience init(reactViewController: UIViewController?, delegate: RNVisitableViewControllerDelegate?) {
self.init()

public init(reactViewController: UIViewController?, delegate: RNVisitableViewControllerDelegate?) {
self.initialVisitableURL = URL(string: "about:blank")!
self.visitableLocationState = .initialized(self.initialVisitableURL)
self.reactViewController = reactViewController
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
}

required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public func initializeVisit(url: URL) {
initialVisitableURL = url
visitableLocationState = .initialized(url)
}

// MARK: View Lifecycle
Expand Down Expand Up @@ -71,6 +86,7 @@ class RNVisitableViewController: UIViewController, Visitable {

func visitableDidRender() {
delegate?.visitableDidRender(visitable: self)
visitableLocationState = .resolved
}

func showVisitableActivityIndicator() {
Expand All @@ -81,9 +97,21 @@ class RNVisitableViewController: UIViewController, Visitable {
delegate?.hideVisitableActivityIndicator()
}

func visitableDidActivateWebView(_ webView: WKWebView) {
// No-op
}

func visitableWillDeactivateWebView() {
visitableLocationState = .deactivated(visitableView.webView?.url ?? initialVisitableURL)
}

open func visitableDidDeactivateWebView() {
// No-op
}

// MARK: Visitable View

open private(set) lazy var visitableView: VisitableView! = {
open private(set) lazy var visitableView: VisitableView = {
let view = VisitableView(frame: CGRect.zero)
view.translatesAutoresizingMaskIntoConstraints = false

Expand All @@ -103,5 +131,23 @@ class RNVisitableViewController: UIViewController, Visitable {
public var visitableViewController: UIViewController {
self.reactViewController?.parent ?? self
}


enum VisitableLocationState {
case resolved
case initialized(URL)
case deactivated(URL)
}

private var visitableLocationState: VisitableLocationState

private func resolveVisitableLocation() -> URL {
switch visitableLocationState {
case .resolved:
return visitableView.webView?.url ?? initialVisitableURL
case .initialized(let url):
return url
case .deactivated(let url):
return url
}
}
}
2 changes: 1 addition & 1 deletion packages/turbo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"types": "lib/typescript/src/index.d.ts",
"react-native": "src/index.tsx",
"hotwireNative": {
"ios": "1.1.3",
"ios": "1.2.0",
"android": "1.1.1"
},
"scripts": {
Expand Down