Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for zApp communication #2750

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/apps/external-app/constants/whitelistedApps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const WHITELISTED_APPS = [
'https://explorer.zero.tech',
];
27 changes: 13 additions & 14 deletions src/apps/external-app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import { Location, withRouter, History } from 'react-router-dom';
import { Component } from 'react';
import { IFrame } from '../iframe';
import { IncomingMessage } from './types/types';
import { routeChangedHandler } from './message-handlers/routeChangeHandler';
import { isAuthenticateEvent, isRouteChangeEvent, isSubmitManifestEvent } from './types/messageTypeGuard';
import { authenticateHandler } from './message-handlers/authenticateHandler';
import { submitManifestHandler } from './message-handlers/submitManifestHandler';

export interface PublicProperties {
route: `/${string}`;
Expand Down Expand Up @@ -46,20 +51,14 @@ class ExternalAppComponent extends Component<Properties, State> {
window.removeEventListener('message', this.onMessage);
}

onMessage = (event: MessageEvent) => {
if (event.data.type === 'zapp-route-changed') {
const isFromCorrectSource = new URL(this.props.url).href === new URL(event.origin).href;

if (!isFromCorrectSource) {
return;
}

// If there's no pathname or it's the route route ('/'), we should navigate to the root of the app
if (!event.data.data.pathname || event.data.data.pathname === '/') {
this.props.history.push(this.props.route);
} else {
this.props.history.push(this.props.route + event.data.data.pathname);
}
onMessage = (event: MessageEvent<IncomingMessage>) => {
if (isRouteChangeEvent(event)) {
const handler = routeChangedHandler(this.props.history, this.props.route, this.props.url);
handler(event);
} else if (isAuthenticateEvent(event)) {
authenticateHandler(event);
} else if (isSubmitManifestEvent(event)) {
submitManifestHandler(event);
}
};

Expand Down
30 changes: 30 additions & 0 deletions src/apps/external-app/message-handlers/authenticateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AuthenticateMessage, AuthenticateResponseMessage, ZAppMessageType } from '../types/types';
import { WHITELISTED_APPS } from '../constants/whitelistedApps';

/**
* Give the app access to the user's access token. This is only allowed for whitelisted apps.
* In the future, we'll create a new token for each app that has specific permissions
* that the user can opt-in to.
*
* @param event
*/
export const authenticateHandler = (event: MessageEvent<AuthenticateMessage>) => {
const { origin } = new URL(event.origin);

if (!WHITELISTED_APPS.includes(origin)) {
return;
}

// To implement getting user token
const token = '';

const response: AuthenticateResponseMessage = {
type: ZAppMessageType.Authenticate,
token,
};

// Send the response back to the iframe that sent the message
if (event.source && event.source instanceof Window) {
event.source.postMessage(response, event.origin);
}
};
18 changes: 18 additions & 0 deletions src/apps/external-app/message-handlers/routeChangeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RouteChangeMessage } from '../types/types';
import { History } from 'react-router-dom';

export const routeChangedHandler =
(history: History, route: string, url: string) => (event: MessageEvent<RouteChangeMessage>) => {
const isFromCorrectSource = new URL(url).href === new URL(event.origin).href;

if (!isFromCorrectSource) {
return;
}

// If there's no pathname or it's the route route ('/'), we should navigate to the root of the app
if (!event.data.data.pathname || event.data.data.pathname === '/') {
history.push(route);
} else {
history.push(route + event.data.data.pathname);
}
};
10 changes: 10 additions & 0 deletions src/apps/external-app/message-handlers/submitManifestHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SubmitManifestMessage } from '../types/types';

/**
* Store the current apps manifest and enable the features listed in the manifest
*
* @param event
*/
export const submitManifestHandler = (event: MessageEvent<SubmitManifestMessage>) => {

Check failure on line 8 in src/apps/external-app/message-handlers/submitManifestHandler.ts

View workflow job for this annotation

GitHub Actions / run-lint

'event' is defined but never used. Allowed unused args must match /^_/u
// To implement
};
17 changes: 17 additions & 0 deletions src/apps/external-app/types/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* A feature is a capability that can change the app's relationship with the zOS.
*/
interface Feature {
type: string;
}

/**
* Changes the AppBar to be on top of the iframe instead of on the side of the iframe
*/
interface FullScreenFeature extends Feature {
type: 'fullscreen';
}

type ZAppFeature = FullScreenFeature;

export type { ZAppFeature };
14 changes: 14 additions & 0 deletions src/apps/external-app/types/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ZAppFeature } from './features';

/**
* The manifest is a JSON object that describes the app.
* It is used to provide information about the app to the zOS.
*
* For now, it'll just include features that the app supports. In the future,
* it'll include other information like the name, description, version, url, auth scopes, etc.
*/
type ZAppManifest = {
features: ZAppFeature[];
};

export type { ZAppManifest };
21 changes: 21 additions & 0 deletions src/apps/external-app/types/messageTypeGuard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
ZAppMessageType,
IncomingMessage,
RouteChangeMessage,
SubmitManifestMessage,
AuthenticateMessage,
} from './types';

export function isRouteChangeEvent(event: MessageEvent<IncomingMessage>): event is MessageEvent<RouteChangeMessage> {
return event.data.type === ZAppMessageType.RouteChange;
}

export function isSubmitManifestEvent(
event: MessageEvent<IncomingMessage>
): event is MessageEvent<SubmitManifestMessage> {
return event.data.type === ZAppMessageType.SubmitManifest;
}

export function isAuthenticateEvent(event: MessageEvent<IncomingMessage>): event is MessageEvent<AuthenticateMessage> {
return event.data.type === ZAppMessageType.Authenticate;
}
51 changes: 51 additions & 0 deletions src/apps/external-app/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
enum ZAppMessageType {
RouteChange = 'zapp-route-changed',
SubmitManifest = 'zapp-submit-manifest',
Authenticate = 'zapp-authenticate',
}

enum ZOSMessageType {
ManifestReceived = 'zos-manifest-received',
}

type RouteChangeMessage = {
type: ZAppMessageType.RouteChange;
data: {
pathname: string;
};
};

type SubmitManifestMessage = {
type: ZAppMessageType.SubmitManifest;
manifest: string;
};

type AuthenticateMessage = {
type: ZAppMessageType.Authenticate;
};

type IncomingMessage = RouteChangeMessage | SubmitManifestMessage | AuthenticateMessage;

type ManifestResponseMessage = {
type: ZOSMessageType.ManifestReceived;
status: 'success' | 'error';
error?: string;
};

type AuthenticateResponseMessage = {
type: ZAppMessageType.Authenticate;
token: string;
};

type OutgoingMessage = ManifestResponseMessage | AuthenticateResponseMessage;

export type {
IncomingMessage,
OutgoingMessage,
RouteChangeMessage,
SubmitManifestMessage,
AuthenticateMessage,
ManifestResponseMessage,
AuthenticateResponseMessage,
};
export { ZAppMessageType, ZOSMessageType };
Loading