Skip to content

📱 WebApp Integration

API partners can embed the Earth Miles web app directly into their applications using a WebView. This allows users to interact with Earth Miles without leaving your app.

Prerequisites

Before integrating the web app, ensure you have:

  • Completed the OAuth integration to connect users
  • Your unique Membership ID (provided by Earth Miles)

Integration Steps

1. Connect the User

First, complete the OAuth flow to connect the user and obtain their access token through your backend.

2. Get a Web App Token

With the user's access token obtained from the previous step, request a limited one-hour web app token:

curl -X POST https://backend.earthmiles.app/api/oauth/webapp-token \
  -H "Authorization: Bearer USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json"

This returns a temporary token valid for 1 hour, specifically for web app authentication. This request should be made from your backend server to keep the access token secure.

Keep Token Safe

The web app token is exclusively for the web app. Do not use it for anything else and ensure it is kept secure. Only pass it to the WebView and do not expose it elsewhere in your application.

3. Configure the WebView URL

Load the Earth Miles web app with your membership parameters:

https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID
Parameter Description
country Two-letter country code (e.g., DK, NO)
language Two-letter language code (e.g., da, en, no)
membership Your unique membership ID (provided by Earth Miles)

4. Inject the Authentication Token

Pass the web app token to the WebView by injecting it into the window object.

import android.webkit.WebView
import android.webkit.WebViewClient
import android.graphics.Bitmap

// Get the token from your backend
val token = "eyJhbGciOi..."

webView.settings.javaScriptEnabled = true

webView.webViewClient = object : WebViewClient() {
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        // Inject the auth token before the page loads
        view?.evaluateJavascript(
            "window.AUTH_TOKEN = '$token';",
            null
        )
    }
}

// Load the Earth Miles web app
val webAppUrl = "https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID"
webView.loadUrl(webAppUrl)
import WebKit

// Get the token from your backend
let token = "eyJhbGciOi..."

// Create a script to inject the auth token
let script = WKUserScript(
    source: "window.AUTH_TOKEN = '\(token)';",
    injectionTime: .atDocumentStart,
    forMainFrameOnly: true
)

let controller = WKUserContentController()
controller.addUserScript(script)

let config = WKWebViewConfiguration()
config.userContentController = controller

let webView = WKWebView(frame: .zero, configuration: config)

// Load the Earth Miles web app
let urlString = "https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID"
if let url = URL(string: urlString) {
    webView.load(URLRequest(url: url))
}

Use the injectedJavaScriptBeforeContentLoaded prop to set the token on window before the page's content loads:

import { WebView } from 'react-native-webview';

// Get the token from your backend
const token = 'eyJhbGciOi...';

const injectedToken = `window.AUTH_TOKEN = '${token}'; true;`;

<WebView
    source={{ uri: 'https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID' }}
    injectedJavaScriptBeforeContentLoaded={injectedToken}
    // An onMessage handler must be set for injected scripts to run reliably
    onMessage={() => {}}
/>

Android reliability

On Android, injectedJavaScriptBeforeContentLoaded is experimental and may occasionally run after content starts loading. Ensure your web app reads window.AUTH_TOKEN lazily (e.g. on a user action) rather than only at the very first script execution.

Using flutter_inappwebview, inject the token with an initialUserScript set to run at document start:

import 'package:flutter_inappwebview/flutter_inappwebview.dart';

// Get the token from your backend
const token = 'eyJhbGciOi...';

InAppWebView(
    initialUrlRequest: URLRequest(
        url: WebUri('https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID'),
    ),
    initialSettings: InAppWebViewSettings(
        javaScriptEnabled: true,
    ),
    initialUserScripts: UnmodifiableListView<UserScript>([
        UserScript(
            source: "window.AUTH_TOKEN = '$token';",
            injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
        ),
    ]),
)

Token Expiration

The web app token expires after 1 hour. There are no refresh tokens for web app tokens, so you must request a new token from your backend when it's near expiration. You can request a new token at any time using the user's access token.

5. Enable Geolocation Access

QRCode rewards in the Earth Miles system require location verification to confirm that users are near the physical location before they can redeem rewards. You must configure your WebView to allow geolocation access.

import android.webkit.GeolocationPermissions
import android.webkit.WebChromeClient

webView.settings.javaScriptEnabled = true
webView.settings.setGeolocationEnabled(true)

webView.webChromeClient = object : WebChromeClient() {
    override fun onGeolocationPermissionsShowPrompt(
        origin: String?,
        callback: GeolocationPermissions.Callback?
    ) {
        // Grant permission to the web app
        callback?.invoke(origin, true, false)
    }
}

You'll also need to request location permission from the user in your app's manifest:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

And request runtime permission:

import android.Manifest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager

// Register the permission launcher
private val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        // Permission granted, WebView can now access location
    } else {
        // Permission denied, inform user that location features won't work
    }
}

// Request permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
    != PackageManager.PERMISSION_GRANTED) {
    requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
import WebKit
import CoreLocation

class ViewController: UIViewController, WKUIDelegate, CLLocationManagerDelegate {
    var webView: WKWebView!
    var locationManager: CLLocationManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set up location manager
        locationManager = CLLocationManager()
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()

        // Configure WebView (+ token injection explained before)
        let config = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: config)
        webView.uiDelegate = self
        view = webView

        // Load the URL
        let urlString = "https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID"
        if let url = URL(string: urlString) {
            webView.load(URLRequest(url: url))
        }
    }

    // WKUIDelegate method - prompts are used by some web apps for geolocation
    func webView(
        _ webView: WKWebView,
        runJavaScriptAlertPanelWithMessage message: String,
        initiatedByFrame frame: WKFrameInfo,
        completionHandler: @escaping () -> Void
    ) {
        completionHandler()
    }
}

Note: WKWebView handles geolocation through the JavaScript Geolocation API. When the web app requests location access, WKWebView will automatically use the iOS location permissions you've granted. Ensure your app has NSLocationWhenInUseUsageDescription in Info.plist and has requested location authorization as shown above.

Add the location usage description to your Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to verify QRCode rewards redemption</string>

On Android, set the geolocationEnabled prop to true (it defaults to false). On iOS the WebView uses the app's native location permission, so no extra prop is needed.

import { WebView } from 'react-native-webview';

<WebView
    source={{ uri: 'https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID' }}
    geolocationEnabled={true} // Android only
/>

You still need to declare and request the platform location permissions:

Android — add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

iOS — add to ios/<YourApp>/Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to verify QRCode rewards redemption</string>

Request the runtime permission from the user (for example using the PermissionsAndroid API on Android or a library such as react-native-permissions) before the user attempts to redeem a QRCode reward.

Using flutter_inappwebview, enable geolocation and grant the prompt via onGeolocationPermissionsShowPrompt on Android. On iOS the WebView uses the app's native location permission.

InAppWebView(
    initialUrlRequest: URLRequest(
        url: WebUri('https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID'),
    ),
    initialSettings: InAppWebViewSettings(
        javaScriptEnabled: true,
        geolocationEnabled: true,
    ),
    onGeolocationPermissionsShowPrompt: (controller, origin) async {
        // Grant location access to the web app
        return GeolocationPermissionShowPromptResponse(
            origin: origin,
            allow: true,
            retain: true,
        );
    },
)

Declare and request the platform location permissions as well. Add the Android permissions to AndroidManifest.xml and the iOS NSLocationWhenInUseUsageDescription key to Info.plist (as shown in the Android and iOS tabs), then request runtime permission with a plugin such as permission_handler before the user redeems a QRCode reward.

Location Permission Required

Without geolocation access, users will not be able to redeem QRCode rewards. Make sure to request and handle location permissions properly in your app.

6. Handle Bridge Messages

The Earth Miles web app communicates with the host application via window.postMessage. You must listen for these messages and respond accordingly.

Currently, the only supported bridge message is earthmiles:close, which is sent when the user taps on the close button and should be returned to the host app.

Use WebViewCompat.addWebMessageListener (requires AndroidX webkit library, API 23+):

import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.WebMessageCompat
import androidx.webkit.WebViewCompat
import android.net.Uri
import android.webkit.WebView

WebViewCompat.addWebMessageListener(
    webView,
    "earthmilesListener",
    setOf("https://webapp.earthmiles.app"),
    object : WebViewCompat.WebMessageListener {
        override fun onPostMessage(
            view: WebView,
            message: WebMessageCompat,
            sourceOrigin: Uri,
            isMainFrame: Boolean,
            replyProxy: JavaScriptReplyProxy
        ) {
            val msg = message.data ?: return
            if (msg == "earthmiles:close") {
                finish() // or popBackStack(), navigator.popBackStack(), etc.
            }
        }
    }
)

Inject a forwarding script to bridge window.postMessage to WKScriptMessageHandler, then add it alongside the auth token script from the earlier step:

import WebKit

let bridgeScript = WKUserScript(
    source: """
        window.addEventListener('message', function(e) {
            window.webkit.messageHandlers.earthmiles.postMessage(e.data);
        });
    """,
    injectionTime: .atDocumentStart,
    forMainFrameOnly: true
)
controller.addUserScript(bridgeScript)
controller.add(self, name: "earthmiles")

Then implement WKScriptMessageHandler in your view controller:

extension ViewController: WKScriptMessageHandler {
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        if message.name == "earthmiles", let body = message.body as? String {
            if body == "earthmiles:close" {
                dismiss(animated: true) // or navigationController?.popViewController(animated: true)
            }
        }
    }
}

Add an onMessage handler to your WebView component:

<WebView
    source={{ uri: 'https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID' }}
    onMessage={(event) => {
        const data = event.nativeEvent.data;
        if (data === 'earthmiles:close') {
            navigation.goBack();
        }
    }}
/>

Using flutter_inappwebview, add a WebMessageListener and inject a forwarding script so the web app's standard window.postMessage calls are captured by the native bridge:

InAppWebView(
    initialUrlRequest: URLRequest(
        url: WebUri('https://webapp.earthmiles.app/?country=DK&language=da&membership=YOUR_MEMBERSHIP_ID'),
    ),
    initialSettings: InAppWebViewSettings(
        javaScriptEnabled: true,
    ),
    onWebViewCreated: (controller) {
        controller.addWebMessageListener(
            WebMessageListener(
                jsObjectName: 'EarthMiles',
                onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
                    if (message?.data == 'earthmiles:close') {
                        Navigator.of(context).pop();
                    }
                },
            ),
        );
    },
    onLoadStop: (controller, url) async {
        await controller.evaluateJavascript(source: '''
            window.addEventListener('message', function(e) {
                EarthMiles.postMessage(e.data);
            });
        ''');
    },
)

The forwarding script is required because window.EarthMiles.postMessage() (the native bridge object exposed by flutter_inappwebview) is separate from the standard window.postMessage() API used by the web app.