Introducing PayPal’s open-source cross-domain javascript suite
At PayPal we write a lot of javascript that ends up running on other websites and other domains. The prime example of this is checkout.js, the integration script for our entire checkout experience, which allows merchants to embed our button and checkout flow seamlessly into their sites.
There are a huge number of perils and pitfalls around running our code on third-party sites, though:
- We absolutely can’t break any sites. That means no modifying globals that we didn’t create, including polyfills for browser apis. Doing that causes issues that are virtually impossible to track down, most of the time.
- Sites can absolutely break us, and they often do. One recent example of this was a site which overrode
Array.prototype.toJSON
which in turn brokeJSON.stringify
. Many sites include aWeakMap
polyfill that doesn’t work on cross-domain window objects, like the native version does. So we can’t trust anything we didn’t create or verify first. - Cross domain restrictions (as in what you can and can’t do when you have an iframe or a popup running on a page on a different domain) are incredibly nebulous, and the rules often change depending on which browser you’re in. For example, in IE and Edge, you literally can not send a message between a popup window and a parent window on a different domain, without resorting to soul-crushing hacks.
- Some browsers just do really weird things. For instance, to test if a window is same-domain or cross-domain, the only viable approach is to
try/catch
on accessing or setting a property on the window object. Even with that, Safari insists on logging a cross-origin error to the console — even if the error is caught. This problem used to cause no end of confusion, when our merchants raised bug reports with us only for us to tell them ignore that error. - Restrictions that are built to combat invasive advertising often hold back third-party experiences like ours. For example, it’s only possible to open a popup window in the exact instant a user clicks on our button. Do anything asynchronous, and we’ve lost our chance — the browser will block our popup, even though it was technically the result of a user-action. On top of this, restrictions around third-party cookies, designed to prevent ad-tracking, prevent us from being able to persist login sessions in our iframe much of the time.
Last year, we decided to focus on putting together a really solid suite of tools to help avoid these pitfalls, and allow us to create great experiences without constantly having to worry about whether a message would get through, or whether an iframe would successfully render.
We’ve open sourced the whole suite. So, if you’re hoping to build experiences that work on other sites (or maybe even just on different domains in your own organization or company), look no further than here.
grumbler
First things first, we wanted to set up a really solid boilerplate for all of the tools we had planned. We have a arm’s length list of the technologies we love, but getting them all to play nicely together is no small task. We decided to do all of that work up-front, and now whenever we want to publish a new cross-domain library or tool, we fork grumbler, and start coding without worrying about setting up any environment or build tools.
Read more here: https://medium.com/@bluepnume/introducing-grumbler-an-opinionated-javascript-module-template-612245e06d00
post-robot
The first real technical problem we wanted to solve was messaging between windows and iframes. Think you can just pick up window.postMessage
and start firing messages? Think again:
- There are no responses or callbacks, which makes requesting data from another window and getting something back really tricky.
- IE/Edge doesn’t let you use
postMessage
between a popup and a parent window on two different domains. - Error handling is virtually non-existent. So is determining whether the other window ever received your message.
- You can’t send more interesting data types over the wire, like functions, promises, error objects, etc.
post-robot aims to solve all of those problems in one fell swoop, providing a consistent, reliable way to send messages and receive responses.
Read more here: https://medium.com/@bluepnume/introducing-post-robot-smart-cross-domain-messaging-from-paypal-bebf27c8619e
zoid
We love using React, and we love the incredible simple idea behind components, using the data-down, actions up principle. That said, React is really targeted towards rendering UI directly onto your page, without any iframes or cross-domain restrictions. On top of that, we would never want to attempt to load React on a site we don’t own.
We asked ourselves the question: what if we turn iframes (and popup windows) into React-like components, that we could directly pass props and callbacks to, and allow the child windows to directly call functions passed by the parent?
Iframes typically aren’t suited to this — normally with window.postMessage
you can only send simple objects and strings down to an iframe and back up to its parent. But with zoid, we allow you to render an iframe without setting up a single message listener — you just pass data and functions down directly into the iframe, and the iframe directly gets any props you pass in the window.xprops
object.
It even automatically sets up bindings with popular frameworks like React, Angular, Vue, etc. so you can natively render iframes in your app, without ever having to worry about how terrible iframes normally are.
Read more here: https://medium.com/@bluepnume/introducing-xcomponent-seamless-cross-domain-web-components-from-paypal-c0144f3e82bf
And here’s how we use zoid at PayPal: https://medium.com/@bluepnume/less-is-more-reducing-thousands-of-paypal-buttons-into-a-single-iframe-using-xcomponent-d902d71d8875
zoid-demo
In the spirit of grumbler, we wanted it to be really easy to start working on zoid components without being bogged down in environment and tooling setup. Everyone loves to just be able to start coding.
We released zoid-demo as a boilerplate that you can immediately fork and start working on, with everything you need to build, test, publish, and even statically-type your code, and publish zoid components for people to use.
cross-domain-utils
Dealing with cross-domain windows is hard. How do you reliably know when they’re closed, whether they’re on the same domain as you or not, what their children are, etc? Browsers provide simple apis for some of these tasks, but they’re not all totally consistent in terms of how they work in different browsers.
Cross-domain-utils attempts to create a library of utility methods for dealing with a bunch of common use-cases, making it easier to work with cross-domain windows without constantly worrying about throwing exceptions or warnings in the console, and avoiding really weird corner cases like this:
cross-domain-safe-weakmap
Storing window objects is expensive in terms of memory — even if the window whose reference you’re storing is closed.
WeakMaps are perfect for storing data on windows, since:
- They don’t keep a reference to the window object hanging around when it’s no longer being used anywhere else
- It’s impossible to directly set properties on a cross-domain window, so associating data with a window (like, say, a callback that needs to be called when a window is closed) is difficult to do. Using a WeakMap makes it easy. Just use the window as the weakmap key and store whatever data you like.
The only problem is, a lot of existing WeakMap shims attempt to call Object.defineProperty
on the key, which causes an instant failure if the key is a cross-domain window with limited access.
cross-domain-safe-weakmap, on the other hand, is designed to work reliably and consistently with cross-domain windows as keys, using the native WeakMap implementation if it’s available.
zalgo-promise
Many browsers will totally de-prioritize anything passed to setTimeout
if the browser tab your code is running in is not focused. So if you’re trying to build an experience in a popup window, and you need to message the parent and have it do something… in this case any setTimeout
will totally stall in the parent.
So just don’t use setTimeout
for any critical code paths, right?
The problem is, a lot of existing Promise shims fall back on setTimeout
to guarantee that the function passed to .then
is called asynchronously, in the absence of helpers like setImmediate
. So if you want to use promises cross-browser, and you want to be able to run code in unfocused windows… you’re kinda out of luck.
zalgo-promise attempts to solve this problem by forcing all promises into synchronous mode by default — meaning if they’re resolved synchronously, the function passed to .then()
will also be called synchronously. It’s called zalgo-promise because we literally decided to release zalgo (after carefully considering the trade-offs).
Read more about that here: https://medium.com/@bluepnume/intentionally-unleashing-zalgo-with-promises-ab3f63ead2fd
beaver-logger
One thing about running code on different domains is, you need to know what’s going on and how people are using what you’ve released. At the very least, you need to know if your code is triggering any error cases in the wild, and breaking anyone’s site.
On the other hand, you don’t want to send beacon after beacon, with hundreds of requests for each event you want to log.
beaver-logger attempts to solve this problem by batching together your logs, and flushing them to your server periodically. It even comes with a server-side node component for receiving the logs, but you’re free to reimplement this for any platform you like.
fetch-robot
CORS is a pain. You have to configure each endpoint on your server to return the correct headers, and it adds extra round-trips to anything that isn’t a GET request, to determine if it’s safe to make the final request.
fetch-robot is a new, experimental module which attempts to solve this by allowing you to publish a manifest in one place, defining which urls can be called, by whom, and with which headers and other fields.
It exposes your cross-domain urls via an iframe, through which all of the messages are channelled using post-robot, which also avoids the problem of making extra round trips (since all of the validation is done on the client side, inside the trusted frame).
Read more here: https://medium.com/@bluepnume/reinventing-cross-origin-requests-without-cors-b9c4cb645376
—
Anyway, thanks for reading, and if anyone has any thoughts about what’s missing from our cross-domain suite, or what could be improved, please let me know!
— Daniel, PayPal Checkout team