One of the core technologies at the heart of Onefill is its ability to auto-fill even the most complex form on a website with 100% precision, with a single tap. And the techology that built does allow us to do just that, but with one exception - iframes.
And while you might think that iframes are a thing of the past (who would use them these days, right?) - the reality is that they're often powering some of the most critical piece of functionality there is on online store - its payment form.
Many payment processors (e.g. Stripe, or Braintree) offer their customer a way to accept payment in the simplest possible way - by just dropping a piece of javascript onto their checkout page. That renders an iframe with a form provided by the payment processor, that actually takes your card details - so that merchant never sees them at all, and everyone involved in the process stays safer.
The problem for us is that this, together with modern browser security mechanisms like CORS and same-origin policy, means that we can't automate the process of entering credit card data if we only inject Onefill's Javascript helpers into the main page, and the payment form is rendered inside an iframe.
But, after some experimentation we have found an approach to support iframe - at least with iOS web view - and to communicate with it.
To run the code in this blog post you'll need:
- Xcode: Version 8.2.1 (8C1002)
- Swift: Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
- Use WKWebView instead UIWebView or WebView for 2 reasons:
- Apple recommends it
- There is an API to inject JS into iFrame
- Use built-in
WKUserScript API
to inject JS, set
forMainFrameOnly
as false to inject JS into every frames (including iframe) - To communicate between iframe and app, use JS webkit API
As it turned out, the problem can be solved if we split it into two parts: first, how to inject our Javascript helpers into an iframe. And second - how to establish interactions between our code in an iframe, and the one in the main frame.
Let's look at those one by one.
Apparently it is really easy these days - all you need to do is to use
WKUserScript
API to inject JavaScript into all frames, and specifically -
forMainFrameOnly
parameter when creating an injectable user script object.
Example: Inject a script to make every h1 tag red.
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
let contentController = WKUserContentController()
let js: String = "var h1s = document.querySelectorAll('h1'); for (var i = 0; i < h1s.length; i++) { h1s[i].style.color = 'red' };"
let userScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false)
contentController.addUserScript(userScript)
webConfiguration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let myURL = URL(string: "http://localhost:3000")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
}
As you can see, all we have to do is just to create WKUserScript
, and
configure it with forMainFrameOnly: false
- which means that this script will
be executed in all frames of WKWebView, and not just the main one.
After that - add it to an instance of WKUserContentController
, create a
WKWebView
with that controller, and load your page with iframe - you should
see your code being executed in all of them.
Use JavaScript webkit API
Almost same as “inject JS into iframe”, however, we need to implement WKScriptMessageHandler
protocol to receive message sent by JavaScript.
Once we've got our code running inside all iframes, we need to communicate with
it to execute specific tasks. This can be done by implementing
WKScriptMessageHandler
protocol, and adding that resulting class to
WKUserContentController
used with the web view.
Once it is done, the handler becomes available to Javascript code running inside
the frame as window.webkit.messageHandlers.<handlerName>
object, exposing
postMessage
method that accepts strings data and sends it back to the main iOS
application.
The application then becomes the communication medium, passing and orchestrating messages back and forth between various frames on the page, and allowing them to work together and execute cross-iframe tasks.
Here's an example of the full configuration, with message handler being attached to the web view:
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
let contentController = WKUserContentController()
// Inject JavaScript which sending message to App
let js: String = "window.webkit.messageHandlers.callbackHandler.postMessage('Hello from JavaScript');"
let userScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false)
contentController.removeAllUserScripts()
contentController.addUserScript(userScript)
// Add ScriptMessageHandler
contentController.add(
self,
name: "callbackHandler"
)
webConfiguration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let myURL = URL(string: "http://localhost:3000")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
// Implement `WKScriptMessageHandler`,handle message which been sent by JavaScript
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
print("JavaScript is sending a message \(message.body)")
}
}
}
And this is all. Of course, WKWebView
is available only on iOS 8 - but
thankfully old OS support is that not big an issue on iOS platform.
Now off to implement the orchestration code, and to find a similar solution for Android!
PS: want to work with us on problems like this one? We're hiring - native mobile development, Javascript, and other roles - check out our jobs page and let's have a chat! http://onefill.com/meettheteam/