How we built the Compliance library on a modern web framework

July 28, 2024
Primož Bevk, Director of Engineering

TL;DR: How we built the Compliance library on Vue 2 with full offline autonomy and 100% device compatibility.

I’m Primož Bevk, Senior Lead Software Engineer at Outfit7. I joined the company in 2012 as a student and as the company has grown, I’ve had the privilege of working on countless projects involving web technology, from marketing campaigns to websites, complex web apps, SEO, app store testing, advertising and, most recently, compliance.

A little more than two years ago, after the announcement of TCF 2.0 and the realization that increasing privacy awareness across the globe would push other countries to implement similar transparency mechanisms as the EU, we realized we needed to plan ahead to remain compliant and keep putting our user’s privacy first.

With this in mind, we set out to create a compliance library that would be modular and have all regulation-specific logic abstracted in a web app. The native client library on Android and iOS would be generalized and would handle the loading and communication with the web app, which would run in a localized webview, ensuring privacy. All the heavy lifting and the majority of the updates would have to be done within the web app. This way, we could iterate quickly, push updates remotely and have a shared codebase for all platforms wherever we need to provide the compliance functionality.

What is a “compliance library” anyway?

We use the term “compliance library” to describe the system that handles the collection and enforcement of user privacy preferences. From the user’s perspective it’s a collection of screens that collect required data, such as age, specific opt-outs, and more comprehensive consents such as GDPR (TCF2.0), LGPD, PIPL, and others.

The foundation & moving parts

We chose Vue 2 as the JavaScript framework, which was the stable release of Vue at the time the project started. The decision to use Vue came about partly due to the library size, which at the time was one of the smallest across the popular frameworks, and partly due to the fact that our team had adopted Vue as the framework of choice for all web projects, simplifying the development process.

The compliance library itself is written as a plugin using ES6 modules, which allows us to modularise and logically separate core library logic and regulation-specific logic in separate modules. The library is loaded at run time and registered both within the Vue application as well as in the global window context. This allows us to expose the functionalities to specific compliance screen views as well as to the native client, which enables us to have two-way communication with the device.

A core requirement is full offline autonomy. We’re not talking about a PWA, workerized offline capable web app, but a completely autonomous web app capable of running off a local file system with no web server and no outside connections. This means we need to disable the history mode in our router, so we can navigate using routes with hashes since we can’t count on URL rewrites. A lot of the usual best practices for optimizations (which rely on deferred loading of either components, dependencies or assets) also cannot be used. Everything needs to be packed into the release. Because of this, we have to be very pragmatic with which libraries we use, what can be written using Vanilla JavaScript so we can keep the overall bundle size as small as possible.

Environment & total support

The environment where we run the web app is constrained by the capabilities of the default device’s webview. Because of this, we run into several limitations that need to be overcome in order to ensure wide support. Since our games are available from iOS 10 and Android 5 onwards, we need to support older webview browsers as well. We need full functionality on every single device. We cannot afford not to support a certain model or version, since this essentially renders our games unplayable if the user cannot pass the compliance step.

Old versions of Android are especially notorious for not updating the webview component, so even though the user might have a newer browser version installed, the webview might still remain on the stock version. To overcome this, we use a two-layered approach to ensure that the compliance library will run and work in all situations.

The first layer is Babel, which we use to transpile and polyfill our code and dependencies to be backwards compatible. This solves 99% of compatibility issues, with just some lone corner cases.

The second layer is defensive code writing. This requires an understanding of certain limitations of older browser versions, especially in relation to layout and CSS styles, to achieve the desired UI & UX through graceful degradation or hacks. For example, dealing with scrolling issues (e.g. fixed elements or overflows) in various versions of Safari. Fixed elements can be made to behave as expected by triggering hardware acceleration. However, there is a limit to how far this hack can take us. We are still unable to create more complex layouts with multiple fixed elements. The overflow issue can be bypassed by declaring the -webkit-overflow-scrolling: touch; style.

.fixedElement {
 position: fixed;
 -webkit-transform: translate3d(0, 0, 0);
 transform: translate3d(0, 0, 0);
}
.overflowScrollingFix {
 overflow-y: scroll;
 -webkit-overflow-scrolling: touch;
}

To overcome the remaining JavaScript-related limitations, we detect specific problematic OS versions and set global flags, which are then used by the library to employ a specific strategy for the edge case.

export const osVersion = (ver, _ctx) => {
 if (ver <= 5 && _ctx.os === platform.ANDROID)
 _ctx.setProp('legacyMode', true)
 if (ver < 7 && _ctx.os === platform.ANDROID)
 _ctx.setProp('customMode', 'scrollToUnsupported')
 if (ver >= 12 && ver < 13 && _ctx.os === platform.IOS)
 _ctx.setProp('customMode', 'redrawRequired', true)
}

Let’s take a brief look at the specific operating modes outlined by this code snippet:

legacyMode

legacyMode enables use of callbacks instead of promises throughout the library. In addition to this, it also disables certain performance-intensive functionalities, such as displaying long lists of items and doing modifications across such lists.

Android partially implemented promises in 4.4.4 and later added complete support in Android 5. From our extensive testing, we found this to hold true for the web browser that comes pre-installed on devices; however, several OEM Android 5 versions had webviews with the outdated Android 4.4 version instead of the expected newer version.

customMode

customMode allows us to handle certain version specific corner cases, such as lack of support for features that we use.

scrollToUnsupported is pretty self-explanatory, Android versions up to 7 don’t support the scrollTo(x, y) function, meaning we have to forego scrolling to last position when navigating back to an already-visited page.

Illustration of the fixed-click zone alignment bug in iOS 12.
Illustration of the fixed-click zone alignment bug in iOS 12.

iOS 12 introduced an interesting bug, where the combination of using a fixed-position element and hiding the Safari toolbar places the click zone of the fixed element higher than it should be. The result is unclickable buttons; but a redraw of the element can correct the bug. The redrawRequired flag instructs the library to wait for the screen to be shown and then dispatches a resize event to force a redraw.

Web — Client communication

The compliance web app lifecycle.
The compliance web app lifecycle.

To understand how the web app communicates with the client, we must first take a look at its lifecycle. The graph shown above displays a simplified flow of the compliance library lifecycle. Red elements show Vue lifecycle hooks, green ones show compliance library steps and yellow ones show client/library events in the direction they flow.

Since our web app is based on Vue, the initialization phase is dependent upon it. We can roughly separate it in the initialization and interactive phase. The compliance initialization phase finishes after Vue mounts the required components. Then, the compliance library resolves the path (screen requested) and signals to the client by sending the onShown event. Because we’re relying on communication with the client for part of the initialization phase, resolvePath might trigger after the Vue mounted phase, triggering a redraw (update step) before we are actually ready to display the page.

The client’s webview loads the compliance library in the background and waits for the onShown event, after which the webview is moved into the foreground and shown to the user. And with this, we enter the interactive phase, in which the requested compliance screen is displayed to the user and we wait for their input. Based on the user interaction, required compliance responses are generated, which are sent to the client through the onResult event. Both the onResult and closeWebApp events trigger the close of the webview. Depending on the result and instructions, the initialization process triggers once again, with the instructions to show an additional compliance screen to the user.

Communication protocol in the library

In the compliance library, we expose two functions which enable the native Android and iOS code to communicate with the web app: onComplianceModuleData and setFlag.

onComplianceModuleData(json, _ctx) {
 _ctx.setProp('regulation', json.p.aR)
 _ctx.initCollector(json.t)
 initModule(json.t, _ctx)
}

setFlag(json, _ctx) {
 if (json.t === 'offline') {
 // Offline status true/false, run custom logic
 …
 }
 if (json.t === 'backButton') {
 // Back button should be enabled
 …
 }
 …
 _ctx.setProp(json.t, json.p)
}

setFlag enables the client to change internal props at any time, based on certain client-specific settings, for instance if we need to enable the Android back button, or notify about a network change (e.g. online/offline).

The onComplianceModuleData function is called after the web app calls the getComplianceModuleData, which signals the “ready” status to the client. This function passes a JSON data structure, containing initialization instructions for a specific compliance screen, regulation, the user’s previous consent response (if available), and more. With this information, we open the requested compliance screen and set its initial state and behaviour.

Communication protocol with the client

To send responses to the client (onComplianceModuleData or any other event), we need to identify the operating system the web app is running on. On Android, we use JavaScript bridge, which exposes native functions to the window object we can call. On iOS, we use WebKit message handlers to post messages, which are intercepted natively.

Releasing and reaching the end user

The web app is built as a regular Vue app, using webpack. We omit certain unnecessary optimizations, such as splitting bundled JavaScript into chunks. We also minimize all JavaScript and remove CSS source maps from the final bundle.

During build time, a release version is created, hardcoded in the web app and pushed to Sentry so we can associate error reports with specific versions and related commits. Sentry is also used to monitor the overall performance of a release, as well as rollout, adoption and overall health.

Sentry: Crash-free session rate, based on a sample of four million daily sessions.
Sentry: Crash-free session rate, based on a sample of four million daily sessions.

The resulting built code is then run through a post-processing script, which removes all unnecessary files, such as JavaScript maps, JSON files and other unnecessary assets. The cleaned-up dist/ folder is then zipped in a versioned archive, which is deployed on CDN. The complete final zip is just shy of 580kb in size, most of which is made up of the graphic elements required to show the UI.

This brings us to the final part: the delivery mechanism. There are two ways in which the compliance library (the web app) zip can be shipped to our games. The latest version is always included in each game update. The client then handles the unzipping and loading of the web app on first run. In case an update is required (either because of regulation changes or simply to make sure users have the latest compliance version), we can remotely push it. In the compliance check phase, the client checks with our backend and downloads a newer zip bundle, if available. The newer bundle then replaces the outdated compliance library that was initially included in the build.

Where next?

With this system in place, streamlined across our games, we’re already looking at potential improvements to simplify our processes and fully leverage its potential, not the least of which is an upgrade path to Vue3. Additionally, we’re looking at introducing vertically-integrated e2e tests, ways to further minimize bundle size and having game-specific library versions, which are more visually-integrated into the general UX of the game.

Want to share this article?