Skip to content

Commit

Permalink
Merge pull request #1916 from AaronDewes/window-alby
Browse files Browse the repository at this point in the history
feat: Add window.alby methods
  • Loading branch information
im-adithya authored Jun 7, 2023
2 parents 8083bf9 + 3506586 commit 0369aeb
Show file tree
Hide file tree
Showing 15 changed files with 377 additions and 11 deletions.
6 changes: 6 additions & 0 deletions src/app/router/Prompt/Prompt.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AccountMenu from "@components/AccountMenu";
import ConfirmAddAccount from "@screens/ConfirmAddAccount";
import ConfirmKeysend from "@screens/ConfirmKeysend";
import ConfirmPayment from "@screens/ConfirmPayment";
import ConfirmRequestPermission from "@screens/ConfirmRequestPermission";
Expand Down Expand Up @@ -66,6 +67,10 @@ function Prompt() {
/>
}
/>
<Route
path="public/alby/enable"
element={<Enable origin={navigationState.origin as OriginData} />} // prompt will always have an `origin` set, just the type is optional to support usage via PopUp
/>
<Route
path="public/webln/enable"
element={<Enable origin={navigationState.origin as OriginData} />} // prompt will always have an `origin` set, just the type is optional to support usage via PopUp
Expand Down Expand Up @@ -96,6 +101,7 @@ function Prompt() {
<Route path="confirmPayment" element={<ConfirmPayment />} />
<Route path="confirmKeysend" element={<ConfirmKeysend />} />
<Route path="confirmSignMessage" element={<ConfirmSignMessage />} />
<Route path="confirmAddAccount" element={<ConfirmAddAccount />} />
<Route
path="public/confirmRequestPermission"
element={<ConfirmRequestPermission />}
Expand Down
56 changes: 56 additions & 0 deletions src/app/screens/ConfirmAddAccount/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { act, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import type { OriginData } from "~/types";

import ConfirmAddAccount from "./index";

const mockOrigin: OriginData = {
location: "https://getalby.com/demo",
domain: "https://getalby.com",
host: "getalby.com",
pathname: "/demo",
name: "Alby",
description: "",
icon: "https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg",
metaData: {
title: "Alby Demo",
url: "https://getalby.com/demo",
provider: "Alby",
image:
"https://getalby.com/assets/alby-503261fa1b83c396b7ba8d927db7072d15fea5a84d387a654c5d0a2cefd44604.svg",
icon: "https://getalby.com/favicon.ico",
},
external: true,
};

jest.mock("~/app/hooks/useNavigationState", () => {
return {
useNavigationState: jest.fn(() => ({
origin: mockOrigin,
args: {
connector: "nativecitadel",
name: "Your Citadel wallet",
config: { connectionString: "..." },
},
})),
};
});

describe("ConfirmAddAccount", () => {
test("render", async () => {
await act(async () => {
render(
<MemoryRouter>
<ConfirmAddAccount />
</MemoryRouter>
);
});

expect(
await screen.findByText(
"This website wants to add an account (Citadel (over Tor)):"
)
).toBeInTheDocument();
expect(await screen.findByText("Your Citadel wallet")).toBeInTheDocument();
});
});
79 changes: 79 additions & 0 deletions src/app/screens/ConfirmAddAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import ContentMessage from "@components/ContentMessage";
import PublisherCard from "@components/PublisherCard";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useNavigationState } from "~/app/hooks/useNavigationState";
import { USER_REJECTED_ERROR } from "~/common/constants";
import msg from "~/common/lib/msg";
import type { OriginData } from "~/types";

function ConfirmAddAccount() {
const navState = useNavigationState();
const { t: tCommon } = useTranslation("common");
const { t } = useTranslation("translation", {
keyPrefix: "confirm_add_account",
});

const name = navState.args?.name as string;
const connector = navState.args?.connector as string;
const config = navState.args?.config as unknown;
const origin = navState.origin as OriginData;
const [loading, setLoading] = useState(false);

async function confirm() {
try {
setLoading(true);
const response = await msg.request(
"addAccount",
{ name, connector, config },
{ origin }
);
msg.reply(response);
} catch (e) {
console.error(e);
if (e instanceof Error) toast.error(`${tCommon("error")}: ${e.message}`);
} finally {
setLoading(false);
}
}

function reject(e: React.MouseEvent<HTMLAnchorElement>) {
e.preventDefault();
msg.error(USER_REJECTED_ERROR);
}

return (
<div className="h-full flex flex-col overflow-y-auto no-scrollbar">
<ScreenHeader title={t("title")} />
<Container justifyBetween maxWidth="sm">
<div>
<PublisherCard
title={origin.name}
image={origin.icon}
url={origin.host}
/>
<ContentMessage
heading={t("content", {
connector: tCommon(
`connectors.${connector as "lndhub"}` /* Type hack */
),
})}
content={name}
/>
</div>
<ConfirmOrCancel
disabled={loading}
loading={loading}
onConfirm={confirm}
onCancel={reject}
/>
</Container>
</div>
);
}

export default ConfirmAddAccount;
4 changes: 3 additions & 1 deletion src/extension/background-script/actions/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import edit from "./edit";
import get from "./get";
import info from "./info";
import lock from "./lock";
import promptAdd from "./promptAdd";
import remove from "./remove";
import select from "./select";
import unlock from "./unlock";
Expand All @@ -18,6 +19,7 @@ export {
select,
info,
remove,
get,
decryptedDetails,
promptAdd,
get,
};
36 changes: 36 additions & 0 deletions src/extension/background-script/actions/accounts/promptAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import utils from "~/common/lib/utils";
import { Message } from "~/types";

export default async function promptAddAccount(message: Message) {
if (typeof message.args.name !== "string") {
return {
error: "Name missing.",
};
}

if (typeof message.args.connector !== "string") {
return {
error: "Connector missing.",
};
}

if (typeof message.args.config !== "object") {
return {
error: "Config missing.",
};
}

try {
await utils.openPrompt({
...message,
action: "confirmAddAccount",
});
return { data: { success: true } };
} catch (e) {
console.error("Adding account cancelled", e);
if (e instanceof Error) {
return { success: false, error: e.message };
}
return { success: false };
}
}
4 changes: 4 additions & 0 deletions src/extension/background-script/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const routes = {

// Public calls that are accessible from the inpage script (through the content script)
public: {
alby: {
enable: allowances.enable,
addAccount: accounts.promptAdd,
},
webln: {
enable: allowances.enable,
getInfo: ln.getInfo,
Expand Down
106 changes: 106 additions & 0 deletions src/extension/content-script/onendalby.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import browser from "webextension-polyfill";

import getOriginData from "./originData";
import shouldInject from "./shouldInject";

// Alby calls that can be executed from the AlbyProvider.
// Update when new calls are added
const albyCalls = ["alby/enable", "alby/addAccount"];
// calls that can be executed when alby is not enabled for the current content page
const disabledCalls = ["alby/enable"];

let isEnabled = false; // store if alby is enabled for this content page
let isRejected = false; // store if the alby enable call failed. if so we do not prompt again
let callActive = false; // store if a alby call is currently active. Used to prevent multiple calls in parallel

async function init() {
const inject = await shouldInject();
if (!inject) {
return;
}

// message listener to listen to inpage alby calls
// those calls get passed on to the background script
// (the inpage script can not do that directly, but only the inpage script can make alby available to the page)
window.addEventListener("message", (ev) => {
// Only accept messages from the current window
if (
ev.source !== window ||
ev.data.application !== "LBE" ||
ev.data.scope !== "alby"
) {
return;
}

if (ev.data && !ev.data.response) {
// if an enable call railed we ignore the request to prevent spamming the user with prompts
if (isRejected) {
postMessage(ev, {
error:
"window.alby call cancelled (rejecting further window.alby calls until the next reload)",
});
return;
}
// if a call is active we ignore the request
if (callActive) {
postMessage(ev, { error: "window.alby call already executing" });
return;
}
// limit the calls that can be made from window.alby
// only listed calls can be executed
// if not enabled only enable can be called.
const availableCalls = isEnabled ? albyCalls : disabledCalls;
if (!availableCalls.includes(ev.data.action)) {
console.error("Function not available.");
return;
}

const messageWithOrigin = {
// every call call is scoped in `public`
// this prevents websites from accessing internal actions
action: `public/${ev.data.action}`,
args: ev.data.args,
application: "LBE",
public: true, // indicate that this is a public call from the content script
prompt: true,
origin: getOriginData(),
};

const replyFunction = (response) => {
callActive = false; // reset call is active
// if it is the enable call we store if alby is enabled for this content script
if (ev.data.action === "alby/enable") {
isEnabled = response.data?.enabled;
if (response.error) {
console.error(response.error);
console.info("Enable was rejected ignoring further alby calls");
isRejected = true;
}
}
postMessage(ev, response);
};
callActive = true;
return browser.runtime
.sendMessage(messageWithOrigin)
.then(replyFunction)
.catch(replyFunction);
}
});
}

function postMessage(ev, response) {
window.postMessage(
{
id: ev.data.id,
application: "LBE",
response: true,
data: response,
scope: "alby",
},
"*"
);
}

init();

export {};
4 changes: 2 additions & 2 deletions src/extension/content-script/onendnostr.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const nostrCalls = [
"nostr/encryptOrPrompt",
"nostr/decryptOrPrompt",
];
// calls that can be executed when webln is not enabled for the current content page
// calls that can be executed when nostr is not enabled for the current content page
const disabledCalls = ["nostr/enable"];

let isEnabled = false; // store if nostr is enabled for this content page
Expand All @@ -29,7 +29,7 @@ async function init() {

// message listener to listen to inpage nostr calls
// those calls get passed on to the background script
// (the inpage script can not do that directly, but only the inpage script can make webln available to the page)
// (the inpage script can not do that directly, but only the inpage script can make nostr available to the page)
window.addEventListener("message", (ev) => {
// Only accept messages from the current window
if (
Expand Down
3 changes: 3 additions & 0 deletions src/extension/content-script/onstart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ async function onstart() {
if (nostrEnabled) {
injectScript(browser.runtime.getURL("js/inpageScriptNostr.bundle.js"));
}

// window.alby
injectScript(browser.runtime.getURL("js/inpageScriptAlby.bundle.js"));
}

onstart();
5 changes: 5 additions & 0 deletions src/extension/inpage-script/alby.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import AlbyProvider from "../providers/alby";

if (document) {
window.alby = new AlbyProvider();
}
Loading

0 comments on commit 0369aeb

Please sign in to comment.