Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat: snap notifications 137 #187

Merged
merged 31 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2d86a6d
chore: node engine requirements (#184) (#186)
mpetrunic Nov 15, 2022
3c4fd07
pair improvements
BeroBurny Oct 28, 2022
c628a86
fix lint
BeroBurny Oct 28, 2022
ada1274
feat: add notify Snap method
Lykhoyda Nov 1, 2022
56f3ac1
go back after getting all notifications
Lykhoyda Nov 7, 2022
c6f4821
remove timeout
mpetrunic Nov 9, 2022
0663d05
wip
Lykhoyda Nov 10, 2022
6f38415
Add notification observe method
Lykhoyda Nov 14, 2022
2506a33
update test
Lykhoyda Nov 14, 2022
a8bf66b
remove unused code
Lykhoyda Nov 16, 2022
abdc92c
wip notifiction observer
Lykhoyda Nov 17, 2022
840546b
wip observer based notifications
Lykhoyda Nov 17, 2022
711904a
wip observer based notifications
Lykhoyda Nov 17, 2022
4c9b429
remove unused dependency
Lykhoyda Nov 17, 2022
088c9ec
cleanup
Lykhoyda Nov 17, 2022
23a71fa
fixes after merge
Lykhoyda Nov 17, 2022
4e21814
revert method names
Lykhoyda Nov 17, 2022
a970759
clean waitForNotification method
Lykhoyda Nov 17, 2022
b6a5ed9
emitter solution - FP
Lykhoyda Nov 24, 2022
f5ac55e
emitter solution - ClassBased
Lykhoyda Nov 24, 2022
ea5ba86
Cleanup class based emitter
Lykhoyda Nov 25, 2022
14f4f31
fix lint
Lykhoyda Nov 25, 2022
1c08ce9
Merge branch 'unstable' into lykhoyda/snap-notifications_137
Lykhoyda Nov 25, 2022
370048c
fixes after merge
Lykhoyda Nov 25, 2022
4edd6bb
remove p-event library
Lykhoyda Nov 25, 2022
0f4bd83
remove eslint comment
Lykhoyda Nov 25, 2022
0d8cb49
update test configuration
Lykhoyda Nov 25, 2022
9cbd993
return NotificationList from waitForNotification
Lykhoyda Nov 25, 2022
fb71f83
add getNotificationEmitter method
Lykhoyda Nov 28, 2022
2a532ea
remove back button
Lykhoyda Nov 28, 2022
2f7c70b
Merge branch 'unstable', remote-tracking branch 'origin' into lykhoyd…
Lykhoyda Nov 28, 2022
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
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"dependencies": {
"@metamask/providers": "^9.1.0",
"node-stream-zip": "^1.13.0",
"serve-handler": "5.0.8"
"serve-handler": "5.0.8",
"strict-event-emitter": "^0.2.8"
},
"devDependencies": {
"@chainsafe/eslint-config": "^1.0.0",
Expand All @@ -70,8 +71,8 @@
"web3": "1.3.4"
},
"peerDependencies": {
"puppeteer": ">13",
"playwright": ">=1"
"playwright": ">=1",
"puppeteer": ">13"
},
"peerDependenciesMeta": {
"soy-puppeteer": {
Expand All @@ -81,4 +82,4 @@
"optional": true
}
}
}
}
3 changes: 1 addition & 2 deletions src/metamask/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Dappeteer } from "..";
import { DappeteerBrowser } from "../browser";
import { DappeteerPage } from "../page";

import { acceptDialog } from "../snap/acceptDialog";
import { rejectDialog } from "../snap/rejectDialog";
import { getAllNotifications, installSnap, invokeSnap } from "../snap";
Expand Down Expand Up @@ -59,10 +58,10 @@ export const getMetaMask = (page: DappeteerPage): Promise<Dappeteer> => {
deleteNetwork: deleteNetwork(page),
},
snaps: {
invokeSnap,
getAllNotifications: getAllNotifications(page),
acceptDialog: acceptDialog(page),
rejectDialog: rejectDialog(page),
invokeSnap,
installSnap: installSnap(page),
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved
},
page,
Expand Down
30 changes: 29 additions & 1 deletion src/page.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,80 @@
import { DappeteerBrowser } from "./browser";
import { DappeteerElementHandle } from "./element";
import { DappeteerBrowser } from "./browser";

export interface DappeteerPage<P = unknown> {
$(selector: string): Promise<DappeteerElementHandle | null>;

$eval<T>(
selector: string,
evalFn: (e: HTMLElement) => Promise<T> | T
): Promise<T>;

$$eval<T>(
selector: string,
evalFn: (e: HTMLElement[]) => Promise<T[]> | T[]
): Promise<T[]>;

$$(selector: string): Promise<DappeteerElementHandle[]>;

getSource(): P;

url(): string;

browser(): DappeteerBrowser;

bringToFront(): Promise<void>;

goto(
url: string,
options?: {
timeout?: number;
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
}
): Promise<void>;

title(): Promise<string>;

close(options?: { runBeforeUnload?: boolean }): Promise<void>;

reload(): Promise<void>;

setViewport(opts: { height: number; width: number }): Promise<void>;

waitForResponse(
urlOrPredicate: string | ((res: Response) => boolean | Promise<boolean>),
options?: {
timeout?: number;
}
): Promise<Response>;

waitForSelector(
selector: string,
opts?: Partial<{ visible: boolean; timeout: number; hidden: boolean }>
): Promise<DappeteerElementHandle>;

waitForXPath(
xpath: string,
opts?: Partial<{ visible: boolean; timeout: number }>
): Promise<DappeteerElementHandle>;

waitForTimeout(timeout: number): Promise<void>;

evaluate<Params extends Serializable, Result>(
evaluateFn: (params: Unboxed<Params>) => Result,
params?: Params
): Promise<Result>;

screenshot(path: string): Promise<void>;

waitForFunction<Params extends Serializable>(
pageFunction: Function | string,
params?: Params
): Promise<void>;

exposeFunction(
name: string,
callback: Function | { default: Function }
): Promise<void>;
}

export type Unboxed<Arg> = Arg extends DappeteerElementHandle<any, infer T>
Expand Down
16 changes: 16 additions & 0 deletions src/playwright/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,30 @@ export class DPlaywrightPage implements DappeteerPage<Page> {
): Promise<DappeteerElementHandle<ElementHandle>> {
return this.waitForSelector(xpath, opts);
}

waitForTimeout(timeout: number): Promise<void> {
return this.page.waitForTimeout(timeout);
}

evaluate<Params, Result>(
evaluateFn: (params?: Unboxed<Params>) => Result | Promise<Result>,
params?: Params
): Promise<Result> {
//@ts-expect-error
return this.page.evaluate(evaluateFn, params);
}

async waitForFunction<Args>(
pageFunction: (params?: Unboxed<Args>) => void | string,
params?: Args
): Promise<void> {
await this.page.waitForFunction(pageFunction, {}, params);
}

exposeFunction(
name: string,
callback: Function | { default: Function }
): Promise<void> {
return this.page.exposeFunction(name, <Function>callback);
}
}
13 changes: 13 additions & 0 deletions src/puppeteer/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ export class DPupeteerPage implements DappeteerPage<Page> {
await this.page.waitForXPath(xpath, opts)
);
}

waitForTimeout(timeout: number): Promise<void> {
return this.page.waitForTimeout(timeout);
}

evaluate<Params extends Serializable, Result>(
evaluateFn: (params?: Unboxed<Params>) => Result | Promise<Result>,
params?: Params
Expand All @@ -131,4 +133,15 @@ export class DPupeteerPage implements DappeteerPage<Page> {
params
) as Promise<Result>;
}

async waitForFunction<Params extends Serializable>(
pageFunction: (params?: Unboxed<Params>) => void | string,
params?: Params
): Promise<void> {
await this.page.waitForFunction(pageFunction, {}, params);
}

exposeFunction(name: string, callback: Function): Promise<void> {
return this.page.exposeFunction(name, callback);
}
}
83 changes: 83 additions & 0 deletions src/snap/NotificationsEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { StrictEventEmitter } from "strict-event-emitter";
import { DappeteerPage } from "../page";
import * as dappeteer from "../../src";
import { clickOnElement, profileDropdownClick } from "../helpers";
import { NotificationItem, NotificationList } from "./types";

interface EventsMap {
notification: (notification: NotificationItem) => void;
}

class NotificationsEmitter extends StrictEventEmitter<EventsMap> {
private notificationsTab: DappeteerPage;
private readonly page: DappeteerPage;

constructor(
protected metamask: dappeteer.Dappeteer,
private notificationTimeout: number = 30000
) {
super();
this.page = metamask.page;
}

private async exposeEmitNotificationToWindow(): Promise<void> {
await this.notificationsTab.exposeFunction(
"emitNotification",
(notification: NotificationItem) => {
this.emit("notification", notification);
}
);
}

private async openNotificationPage(): Promise<void> {
await this.page.bringToFront();
await profileDropdownClick(this.page);
await clickOnElement(this.page, "Notifications");

const newPage = await this.page.browser().newPage();
await newPage.goto(this.page.url());

await newPage.waitForSelector(".notifications__container", {
timeout: this.notificationTimeout,
});
this.notificationsTab = newPage;
}
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved

private async observeNotificationsMutation(): Promise<void> {
await this.notificationsTab.evaluate(() => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
const element = mutation.addedNodes[0] as HTMLElement;
window.emitNotification({ message: element.innerText });
observer.disconnect();
}
}
});
observer.observe(document.querySelector(".notifications__container"), {
childList: true,
});
});
}

public async setup(): Promise<void> {
await this.openNotificationPage();
await this.exposeEmitNotificationToWindow();
await this.observeNotificationsMutation();
}

public async cleanup(): Promise<void> {
this.removeAllListeners("notification");
await this.notificationsTab.close();
}

public async waitForNotification(): Promise<void> {
await NotificationsEmitter.once(this, "notification");
}
Lykhoyda marked this conversation as resolved.
Show resolved Hide resolved

public async getAllNotifications(): Promise<NotificationList> {
return await this.metamask.snaps.getAllNotifications();
}
}

export default NotificationsEmitter;
3 changes: 2 additions & 1 deletion src/snap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export interface InstallSnapResult {
};
}

export type NotificationList = { message: string }[];
export type NotificationItem = { message: string };
export type NotificationList = NotificationItem[];
10 changes: 5 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { launch as puppeteerLaunch } from "puppeteer";
import { DappeteerPage, Serializable } from "./page";
import { Path } from "./setup/utils/metaMaskDownloader";
import { InstallStep } from "./snap/install";
import { NotificationList } from "./snap/types";
import { NotificationItem, NotificationList } from "./snap/types";
import { RECOMMENDED_METAMASK_VERSION } from "./index";

export type DappeteerLaunchOptions = {
Expand All @@ -27,6 +27,7 @@ export type DappeteerLaunchOptions = {
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
emitNotification: (notification: NotificationItem) => void;
}
}

Expand Down Expand Up @@ -64,7 +65,7 @@ export type Dappeteer = {
page: DappeteerPage;
snaps: {
/**
* Returns all notifications in Metamask notifications page
* Returns all notifications in Metamask notifications page and go back to main page
*/
getAllNotifications: () => Promise<NotificationList>;
/**
Expand All @@ -73,7 +74,7 @@ export type Dappeteer = {
* @param page Browser page where injected Metamask provider will be available.
* For most snaps, openning google.com will suffice.
* @param snapId id of your installed snap (result of invoking `installSnap` method)
* @param method snap method you wan't to invoke
* @param method snap method you want to invoke
* @param params required parameters of snap method
*/
invokeSnap: <Result = unknown, Params extends Serializable = Serializable>(
Expand All @@ -82,12 +83,11 @@ export type Dappeteer = {
method: string,
params?: Params
) => Promise<Partial<Result>>;

/**
* Installs snap. Function will throw if there is an error while installing snap.
* @param snapIdOrLocation either pass in snapId or full path to your snap directory
* where we can find bundled snap (you need to ensure snap is built)
* @param opts {Object} snap method you wan't to invoke
* @param opts {Object} snap method you want to invoke
* @param opts.hasPermissions Set to true if snap uses some permissions
* @param opts.hasKeyPermissions Set to true if snap uses key permissions
* @param installationSnapUrl url of your dapp. Defaults to google.com
Expand Down
4 changes: 1 addition & 3 deletions test/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";

import * as dappeteer from "../src";
import { profileDropdownClick } from "../src/helpers";
import { DappeteerPage } from "../src/page";

import { DappeteerPage } from "../src";
import { PASSWORD, TestContext } from "./constant";
import { clickElement } from "./utils/utils";

Expand Down
2 changes: 1 addition & 1 deletion test/flask/methods-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "An example Snap written in TypeScript.",
"proposedName": "Methods Snap\n",
"source": {
"shasum": "xxu7kMfZ4zKgviSZ5K4gZKSDDGEYi2VqOK5bKAqoybo=",
"shasum": "65ylJ5v6kEUqE7jJEezI49brIzKhs3dSY8Y2uRxIIEY=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion test/flask/methods-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
params: [
{
type: "inApp",
message: `Hello, in App notification`,
message: `Hello from methods snap in App notification`,
},
],
});
Expand Down
2 changes: 1 addition & 1 deletion test/flask/permissions-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "An example Snap written in TypeScript.",
"proposedName": "Permissions Snap\n",
"source": {
"shasum": "sApNS/DJjjs8psniSQzWlOdkzJ75IsbtlpdipfKarRI=",
"shasum": "LSrgdJ2wzwzWkfVsqwATqBpEFxjjanFr9JdZHu0DmdU=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
11 changes: 11 additions & 0 deletions test/flask/permissions-snap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export const onRpcRequest: OnRpcRequestHandler = ({ origin, request }) => {
},
],
});
case "notify_inApp": {
return wallet.request({
method: "snap_notify",
params: [
{
type: "inApp",
message: `Hello from permissions snap in App notification`,
},
],
});
}
default:
throw new Error("Method not found.");
}
Expand Down
Loading