Skip to content

Commit

Permalink
Implement fingerprinting capture for deferred deep linking (#40)
Browse files Browse the repository at this point in the history
* Add initial impl of fp capture and record

* Wire up click fp capture upon user accept action tap

* Rewire trigger support for ddl banner

* Update bundles

* Update fp catpure page url

* Reset host page body styles on unmount

* Prevent double load state firing in ddl mgr

* Refactor prompt activation to prevent race preventing initial display of banner

* Update bundles

* Remove redundant suppression check

* Update version

* Ensure iframe message is from expected origin

* Remove unused imports

* Enhance ui action redirection handling to store/deeplink

* Update and alter strategy for requesting fp

* Encapsulate fp readiness in component

* Remove await/req resolution in helper fn

* Uprev package and bundles

Co-authored-by: Rob Dick <r.dick@kumulos.com>
  • Loading branch information
robdick and krobd authored Aug 17, 2021
1 parent f3c3a4e commit 4f16749
Show file tree
Hide file tree
Showing 24 changed files with 488 additions and 58 deletions.
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/worker.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kumulos/web",
"version": "1.9.0",
"version": "1.10.0",
"description": "Official SDK for integrating Kumulos services with your web projects",
"main": "dist/index.js",
"types": "types/src/index.d.ts",
Expand Down
5 changes: 3 additions & 2 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { authedFetch, cyrb53, uuidv4 } from './utils';
import { del, get, set } from './storage';
import { Channel } from './channels';

const SDK_VERSION = '1.9.0';
const SDK_VERSION = '1.10.0';
const SDK_TYPE = 10;
const EVENTS_BASE_URL = 'https://events.kumulos.com';
export const PUSH_BASE_URL = 'https://push.kumulos.com';
export const DDL_BASE_URL = 'https://links.kumulos.com';
export const FP_CAPTURE_URL = 'https://pd.app.delivery';

export type InstallId = string;
export type UserId = string;
Expand Down Expand Up @@ -271,7 +272,7 @@ export interface AppRating {

type DdlDialogColorConfig = DialogColorConfig & { ratingFg: string };

type OpenStoreUiAction = {
export type OpenStoreUiAction = {
type: UiActionType.DDL_OPEN_STORE;
url: string;
deepLinkUrl: string;
Expand Down
123 changes: 123 additions & 0 deletions src/fp/fp-capture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Component, h, createRef, RefObject } from 'preact';
import { createPortal } from 'preact/compat';
import {
ClientMessageType,
HostMessage,
HostMessageType,
FingerprintComponents
} from './types';
import { FP_CAPTURE_URL } from '../core';

enum CaptureState {
IDLE,
CAPTURING
}

interface FpCaptureProps {
onCaptured: (components: FingerprintComponents) => void;
}

interface FpCaptureState {
isReady: boolean;
captureState: CaptureState;
}

export default class FpCapture extends Component<
FpCaptureProps,
FpCaptureState
> {
private iFrameRef: RefObject<HTMLIFrameElement>;

constructor(props: FpCaptureProps) {
super(props);

this.iFrameRef = createRef<HTMLIFrameElement>();

this.state = {
isReady: false,
captureState: CaptureState.IDLE
};
}

componentDidMount() {
window.addEventListener('message', this.onMessage);
}

componentWillUnmount() {
window.removeEventListener('message', this.onMessage);
}

componentWillUpdate(_: FpCaptureProps, nextState: FpCaptureState) {
const { isReady, captureState } = nextState;
const prevCaptureState = this.state.captureState;

if (
isReady &&
captureState === CaptureState.CAPTURING &&
prevCaptureState === CaptureState.IDLE
) {
this.dispatchMessage({
type: HostMessageType.REQUEST_FINGERPRINT
});
}
}

public requestFp() {
console.info(`FpCapure: requesting fp capture`);

if (this.state.captureState !== CaptureState.IDLE) {
console.error('FpCapture.requestFp: captureState not IDLE');
return;
}

this.setState({ captureState: CaptureState.CAPTURING });
}

private onMessage = (e: MessageEvent) => {
console.info(
`FpCapure: message ${e.data.type} received from ${e.origin}`
);

const message = e.data;

if (e.origin !== FP_CAPTURE_URL) {
return;
}

switch (message.type) {
case ClientMessageType.READY:
this.setState({ isReady: true });
break;
case ClientMessageType.FINGERPRINT_GENERATED:
this.setState({ captureState: CaptureState.IDLE }, () => {
this.props.onCaptured(message.data.components);
});
break;
}
};

private dispatchMessage = (message: HostMessage) => {
console.info(
`FpCapure: dispatching ${message.type} message to capture frame`
);

const window = this.iFrameRef.current?.contentWindow;

if (!window) {
return;
}

window.postMessage(message, FP_CAPTURE_URL);
};

render() {
return createPortal(
<iframe
ref={this.iFrameRef}
src={FP_CAPTURE_URL}
style={{ width: 0, height: 0 }}
/>,
document.body
);
}
}
20 changes: 20 additions & 0 deletions src/fp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DDL_BASE_URL, Context, getInstallId } from '../core';
import { authedFetch } from '../core/utils';
import { FingerprintComponents } from './types';

export async function sendClickRequest(
ctx: Context,
bannerUid: string,
fingerprint: FingerprintComponents
): Promise<Response> {
const url = `${DDL_BASE_URL}/v1/banners/${bannerUid}/taps`;
const webInstallId = await getInstallId();

return authedFetch(ctx, url, {
method: 'POST',
body: JSON.stringify({
webInstallId,
fingerprint
})
});
}
26 changes: 26 additions & 0 deletions src/fp/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

type Message<T, D = never> =
| {
type: T;
}
| { type: T; data: D };

export enum HostMessageType {
REQUEST_FINGERPRINT = 'REQUEST_FINGERPRINT',
}

export enum ClientMessageType {
READY = 'READY',
FINGERPRINT_GENERATED = 'FINGERPRINT_GENERATED',
}

export type FingerprintComponents = Record<string, string>;

export type HostMessage = Message<HostMessageType.REQUEST_FINGERPRINT>;

export type ClientMessage =
| Message<ClientMessageType.READY>
| Message<
ClientMessageType.FINGERPRINT_GENERATED,
{ components: FingerprintComponents }
>;
25 changes: 14 additions & 11 deletions src/prompts/ddl/deeplink-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, h } from 'preact';
import copy from 'clipboard-copy';
import { DdlUiActions, UiActionType, DdlBannerPromptConfig } from '../../core';
import { DdlUiActions, UiActionType } from '../../core';

interface DeeplinkButtonProps {
style: h.JSX.CSSProperties;
Expand All @@ -15,27 +15,30 @@ export default class DeeplinkButton extends Component<
never
> {
private handleAction = () => {
const { uiActions: {accept} } = this.props.promptActions;
const {
uiActions: { accept }
} = this.props.promptActions;

switch (accept.type) {
case UiActionType.DDL_OPEN_STORE:
copy(accept.deepLinkUrl)
.then(() => {
this.props.onAction();
window.location.href = accept.url;
})
.catch((e) => {
console.error('Unable to capture deeplink url for deferred app pickup', e);
.then(this.props.onAction)
.catch(e => {
console.error(
'Unable to capture deeplink url for deferred app pickup',
e
);
});
break;
case UiActionType.DDL_OPEN_DEEPLINK:
// not yet implemented
break;
this.props.onAction();
break;
default:
return console.error(
`Handle DeepLink Action: unsupported accept action type '${accept['type']}'`
);
}
}
};

render() {
const { style, class: cssClass, text } = this.props;
Expand Down
Loading

0 comments on commit 4f16749

Please sign in to comment.