Skip to content
This repository has been archived by the owner on Sep 5, 2023. It is now read-only.

device routing/mobile deep link middleware #39

Open
wants to merge 1 commit into
base: master
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"lodash": "^4.17.11",
"webpack": "^4.28.4",
"sjcl": "^1.0.8",
"aws4": "^1.8.0"
"aws4": "^1.8.0",
"ua-parser-js": "^0.7.19"
},
"peerDependencies": {},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/backends/origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface OriginOptions {
forwardHostHeader?: boolean,
retries?: number,
headers?: { [name: string]: string | boolean | undefined },
queryStringParameters?: string,
}

/**
Expand All @@ -24,7 +25,7 @@ export interface OriginOptions {
export function origin(options: OriginOptions | string | URL): ProxyFunction<OriginOptions> {
const config = _normalizeOptions(options);

const fn = proxy(config.origin, { forwardHostHeader: config.forwardHostHeader, headers: config.headers, retries: config.retries });
const fn = proxy(config.origin, { forwardHostHeader: config.forwardHostHeader, headers: config.headers, retries: config.retries, queryStringParameters: config.queryStringParameters });

return Object.assign(fn, { proxyConfig: config });
}
Expand Down
1 change: 1 addition & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./middleware/response-headers";
export * from "./middleware/inject-html";
export * from "./middleware/http-cache";
export { autoWebp } from "./middleware/auto-webp";
export { deviceRouter } from "./middleware/device-router";
101 changes: 101 additions & 0 deletions src/middleware/device-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @module Middleware
*/
import { origin } from "../backends/origin";
import { FetchFunction } from "../fetch";
import { UAParser } from "ua-parser-js";

/**
* Device routing options
*/
export interface DeviceOptions {
/** `ios` should be the name of app in the App Store */
ios: string,
/** `android` should be the "package name" of app in the Google Play Store */
android: string,
}

/**
* Routes to an app's store page on the App Store/Play Store based on user device + OS
*
* Pre-requisites: UAParser.js(`npm install ua-parser-js`)
*
* Example:
*
* ```typescript
* import { origin } from "./src/backends";
* import { deviceRouter} from "./src/middleware/device-router";
*
* const backend = origin({
* origin: "https://www.airbnb.com/",
* headers: {host: "www.airbnb.com"}
* })
*
* const route = deviceRouter(backend, {
* ios: "airbnb",
* android: "com.airbnb.android"
* });
*
* declare var fly: any;
* fly.http.respondWith(route);
* ```
*
* @param fetch
* @param options
*/
export function deviceRouter(fetch: FetchFunction, options: DeviceOptions): FetchFunction {
return async function deviceRoute(req: RequestInfo, init?: RequestInit): Promise<Response> {

if(typeof req === "string"){
req = new Request(req, init);
init = undefined;
}

// get user-agent info from the request
const ua = req.headers.get("user-agent");

if(ua){
// parse the user agent so that we can read/use it
const parser = new UAParser(ua);

const device = parser.getDevice();
const deviceType = device.type;

const os = parser.getOS();
const osName = os.name;

// add device headers to the request
if(deviceType){
req.headers.set("Fly-Device-Type", deviceType);
console.log("Device Type:", deviceType);
}

if(osName){
req.headers.set("Fly-Device-OS", osName);
console.log("Device OS:", osName);
}

// route to app's App Store page if mobile device + iOS is detected
if (deviceType === "mobile" && osName === "iOS") {
const backend = origin({
origin: `http://appstore.com/${options.ios}`,
headers: {host: "appstore.com"}
});
return backend(req);
}

// route to app's Play Store page if mobile device + Android OS is detected
if (deviceType === "mobile" && osName === "Android") {
const backend = origin({
origin: `https://play.google.com/store/apps/details?id=${options.android}`,
headers: {host: "play.google.com"}
});
return backend(req);
}
}

// returns the original response if mobile is not detected (user is on Desktop, tablet, etc..)
let resp = await fetch(req, init);
return resp;
}
}
2 changes: 2 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function buildProxyRequest(origin: string | URL, options: ProxyOptions, r
url.hostname = origin.hostname
url.protocol = origin.protocol
url.port = origin.port
url.search = origin.search

if (options.stripPath && typeof options.stripPath === "string") {
// remove basePath so we can serve `onehosthame.com/dir/` from `origin.com/`
Expand Down Expand Up @@ -205,6 +206,7 @@ export function rewriteLocationHeader(url: URL | string, burl: URL | string, res
* Options for `proxy`.
*/
export interface ProxyOptions {
queryStringParameters?: string
/**
* Replace this portion of URL path before making request to origin.
*
Expand Down
50 changes: 50 additions & 0 deletions test/middleware/device-router.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect } from "chai";
import { deviceRouter } from "../../src/middleware";
import { echo } from "../../src/backends";

describe("middleware/deviceRouter", function() {
it("routes to an iOS app if user is on a mobile device with iOS", async ()=>{
const route = deviceRouter(echo, {
ios: "airbnb",
android: "com.airbnb.android"
});

const resp = await route("https://www.airbnb.com/", {
headers: {
"user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1" // a typical iPhone user-agent header
}
});

expect(resp.url).to.include("appstore.com");
})

it("routes to an Android app if user is on a mobile device with Android OS", async ()=>{
const route = deviceRouter(echo, {
ios: "airbnb",
android: "com.airbnb.android"
});

const resp = await route("https://www.airbnb.com/", {
headers: {
"user-agent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Mobile Safari/537.36" // a typical Google Pixel user-agent header
}
});

expect(resp.url).to.include("play.google.com");
})

it("successfully lets non-mobile responses pass through", async ()=>{
const route = deviceRouter(echo, {
ios: "airbnb",
android: "com.airbnb.android"
});

const resp = await route("https://www.airbnb.com/", {
headers: {
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" // a typical desktop/Mac user-agent header
}
});

expect(resp.status).to.eq(200);
})
})