diff --git a/create-packages.sh b/create-packages.sh
index b3d48057a2..82a8da6bc2 100755
--- a/create-packages.sh
+++ b/create-packages.sh
@@ -2,6 +2,39 @@
# Extract version from package.json
VERSION=$(node -pe "require('./package.json').version")
+NODE_ENV=production
+
+if [ -z ${ALBY_OAUTH_CLIENT_ID_CHROME+x} ];
+then
+ echo "OAuth client id for Chrome:"
+ read ALBY_OAUTH_CLIENT_ID_CHROME
+fi
+
+if [ -z ${ALBY_OAUTH_CLIENT_SECRET_CHROME+x} ];
+then
+ echo "OAuth client secret for Chrome:"
+ read ALBY_OAUTH_CLIENT_SECRET_CHROME
+fi
+
+if [ -z ${ALBY_OAUTH_CLIENT_ID_FIREFOX+x} ];
+then
+ echo "OAuth client id for Firefox:"
+ read ALBY_OAUTH_CLIENT_ID_FIREFOX
+fi
+
+if [ -z ${ALBY_OAUTH_CLIENT_SECRET_FIREFOX+x} ];
+then
+ echo "OAuth client secret for Firefox:"
+ read ALBY_OAUTH_CLIENT_SECRET_FIREFOX
+fi
+
+if [ -z ${ALBY_API_URL+x} ];
+then
+ ALBY_API_URL="https://api.getalby.com"
+fi
+
+echo "Creating the build for v$VERSION"
+yarn build
echo "Creating zip packages for v$VERSION"
cd dist/production
@@ -38,4 +71,4 @@ echo "Created alby-opera-v$VERSION.crx (SHA512: $SHA)"
echo "done!"
-cd ../../../
\ No newline at end of file
+cd ../../../
diff --git a/package.json b/package.json
index f8c43e519a..37a571c8ac 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lightning-browser-extension",
- "version": "3.2.1",
+ "version": "3.3.0",
"description": "Lightning browser extension",
"private": true,
"repository": "https://github.com/bumi/lightning-browser-extension.git",
@@ -21,7 +21,7 @@
"build:firefox": "NODE_ENV=production TARGET_BROWSER=firefox webpack",
"build:opera": "NODE_ENV=production TARGET_BROWSER=opera webpack",
"build": "yarn build:chrome && yarn build:firefox && yarn build:opera",
- "package": "yarn build && ./create-packages.sh",
+ "package": "./create-packages.sh",
"lint": "yarn lint:js && yarn tsc:compile && yarn format:fix",
"lint:js": "eslint src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"lint:js:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
@@ -35,7 +35,8 @@
"prepare": "husky install"
},
"dependencies": {
- "@bitcoin-design/bitcoin-icons-react": "^0.1.9",
+ "@bitcoin-design/bitcoin-icons-react": "^0.1.10",
+ "@bitcoinerlab/secp256k1": "^1.0.5",
"@getalby/sdk": "^2.2.3",
"@headlessui/react": "^1.7.16",
"@lightninglabs/lnc-web": "^0.2.4-alpha",
@@ -43,11 +44,11 @@
"@noble/secp256k1": "^2.0.0",
"@scure/bip32": "^1.3.1",
"@scure/bip39": "^1.2.1",
- "@scure/btc-signer": "^0.5.1",
"@tailwindcss/forms": "^0.5.4",
"@vespaiach/axios-fetch-adapter": "^0.3.0",
"axios": "^0.27.2",
"bech32": "^2.0.0",
+ "bitcoinjs-lib": "^6.1.0",
"bolt11": "^1.4.1",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.9",
diff --git a/src/app/components/Badge/index.tsx b/src/app/components/Badge/index.tsx
index be8125b1cd..2fe880828a 100644
--- a/src/app/components/Badge/index.tsx
+++ b/src/app/components/Badge/index.tsx
@@ -1,22 +1,22 @@
import { useTranslation } from "react-i18next";
+import { classNames } from "~/app/utils";
type Props = {
label: "budget" | "auth" | "imported";
- color: string;
- textColor: string;
- small?: boolean;
+ className: string;
};
-export default function Badge({ label, color, textColor, small }: Props) {
+export default function Badge({ label, className }: Props) {
const { t: tComponents } = useTranslation("components", {
keyPrefix: "badge",
});
return (
{tComponents(`label.${label}`)}
diff --git a/src/app/components/BadgesList/index.tsx b/src/app/components/BadgesList/index.tsx
index f91d696bb2..524121449d 100644
--- a/src/app/components/BadgesList/index.tsx
+++ b/src/app/components/BadgesList/index.tsx
@@ -10,29 +10,20 @@ export default function BadgesList({ allowance }: Props) {
if (allowance.remainingBudget > 0) {
badges.push({
label: "budget",
- color: "green-bitcoin",
- textColor: "white",
+ className: "bg-blue-500 text-white",
});
}
if (allowance.lnurlAuth) {
badges.push({
label: "auth",
- color: "green-bitcoin",
- textColor: "white",
+ className: "bg-green-bitcoin text-white",
});
}
return (
<>
{badges?.map((b) => {
- return (
-
- );
+ return ;
})}
>
);
diff --git a/src/app/components/LNURLAuth/index.tsx b/src/app/components/LNURLAuth/index.tsx
new file mode 100644
index 0000000000..cb11ef5eca
--- /dev/null
+++ b/src/app/components/LNURLAuth/index.tsx
@@ -0,0 +1,140 @@
+import Button from "@components/Button";
+import ConfirmOrCancel from "@components/ConfirmOrCancel";
+import Container from "@components/Container";
+import ContentMessage from "@components/ContentMessage";
+import PublisherCard from "@components/PublisherCard";
+import ResultCard from "@components/ResultCard";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import ScreenHeader from "~/app/components/ScreenHeader";
+import { useNavigationState } from "~/app/hooks/useNavigationState";
+import { USER_REJECTED_ERROR } from "~/common/constants";
+import api from "~/common/lib/api";
+import msg from "~/common/lib/msg";
+import type { LNURLAuthServiceResponse } from "~/types";
+
+function LNURLAuthComponent() {
+ const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" });
+ const { t: tCommon } = useTranslation("common");
+
+ const navigate = useNavigate();
+ const navState = useNavigationState();
+
+ const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse;
+ const origin = navState.origin;
+
+ const [successMessage, setSuccessMessage] = useState("");
+ const [errorMessage, setErrorMessage] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ async function confirm() {
+ try {
+ setLoading(true);
+ const response = await api.lnurlAuth({
+ origin,
+ lnurlDetails: details,
+ });
+
+ if (navState.isPrompt && origin?.host) {
+ const allowance = await api.getAllowance(origin.host);
+
+ if (allowance.lnurlAuth === false) {
+ await msg.request("updateAllowance", {
+ id: allowance.id,
+ lnurlAuth: true,
+ });
+ }
+ }
+
+ if (response.success) {
+ setSuccessMessage(
+ t("success", { name: origin ? origin.name : details.domain })
+ );
+ // ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction.
+ // We assume this is OK when it is called through webln.
+ if (navState.isPrompt) {
+ msg.reply(response);
+ }
+ } else {
+ setErrorMessage(t("errors.status"));
+ }
+ } catch (e) {
+ console.error(e);
+ if (e instanceof Error) {
+ setErrorMessage(`Error: ${e.message}`);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function reject(e: React.MouseEvent) {
+ e.preventDefault();
+ if (navState.isPrompt) {
+ msg.error(USER_REJECTED_ERROR);
+ } else {
+ navigate(-1);
+ }
+ }
+
+ function close(e: React.MouseEvent) {
+ // will never be reached via prompt
+ e.preventDefault();
+ navigate(-1);
+ }
+
+ return (
+
+
+ {!successMessage ? (
+ <>
+
+
+ {origin ? (
+
+ ) : (
+
+ )}
+
+
+
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+
+
+ >
+ ) : (
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default LNURLAuthComponent;
diff --git a/src/app/components/PaymentSummary/index.tsx b/src/app/components/PaymentSummary/index.tsx
index 641816c6ea..15ae87942b 100644
--- a/src/app/components/PaymentSummary/index.tsx
+++ b/src/app/components/PaymentSummary/index.tsx
@@ -41,7 +41,7 @@ const PaymentSummary: FC = ({
{tCommon("description")}
-
+
{description}
>
diff --git a/src/app/components/QRCode/index.tsx b/src/app/components/QRCode/index.tsx
index faf5719506..ff0447617e 100644
--- a/src/app/components/QRCode/index.tsx
+++ b/src/app/components/QRCode/index.tsx
@@ -5,9 +5,19 @@ export type Props = {
value: string;
size?: number;
className?: string;
+
+ // set the level to Q if there are overlays
+ // Q will improve error correction (so we can add overlays covering up to 25% of the QR)
+ // at the price of decreased information density (meaning the QR codes "pixels" have to be
+ // smaller to encode the same information).
+ // While that isn't that much of a problem for lightning addresses (because they are usually quite short),
+ // for invoices that contain larger amount of data those QR codes can get "harder" to read.
+ // (meaning you have to aim your phone very precisely and have to wait longer for the reader
+ // to recognize the QR code)
+ level?: "Q" | undefined;
};
-export default function QRCode({ value, size, className }: Props) {
+export default function QRCode({ value, size, level, className }: Props) {
const theme = useTheme();
const fgColor = theme === "dark" ? "#FFFFFF" : "#000000";
const bgColor = theme === "dark" ? "#000000" : "#FFFFFF";
@@ -16,10 +26,10 @@ export default function QRCode({ value, size, className }: Props) {
);
}
diff --git a/src/app/components/TransactionsTable/index.tsx b/src/app/components/TransactionsTable/index.tsx
index ab155106d3..af143e6a92 100644
--- a/src/app/components/TransactionsTable/index.tsx
+++ b/src/app/components/TransactionsTable/index.tsx
@@ -6,8 +6,6 @@ import Button from "~/app/components/Button";
import { useSettings } from "~/app/context/SettingsContext";
import { Transaction } from "~/types";
-import Badge from "../Badge";
-
export type Props = {
transactions: Transaction[] | null | undefined;
loading?: boolean;
@@ -60,18 +58,6 @@ export default function TransactionsTable({
{tx.date}
- {tx.badges && (
-
- {tx.badges.map((badge) => (
-
- ))}
-
- )}
diff --git a/src/app/components/onboard/index.tsx b/src/app/components/onboard/index.tsx
index 152b26f410..d5dfb14dc3 100644
--- a/src/app/components/onboard/index.tsx
+++ b/src/app/components/onboard/index.tsx
@@ -1,5 +1,6 @@
import {
ClockIcon,
+ InfoCircleIcon,
TwoKeysIcon,
} from "@bitcoin-design/bitcoin-icons-react/outline";
import { useTranslation } from "react-i18next";
@@ -9,7 +10,6 @@ import PublisherCard from "~/app/components/PublisherCard";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useAccount } from "~/app/context/AccountContext";
import { useNavigationState } from "~/app/hooks/useNavigationState";
-import InfoCircleIcon from "~/app/icons/InfoCircleIcon";
import { NO_KEYS_ERROR, USER_REJECTED_ERROR } from "~/common/constants";
import api from "~/common/lib/api";
import msg from "~/common/lib/msg";
diff --git a/src/app/icons/InfoCircleIcon.tsx b/src/app/icons/InfoCircleIcon.tsx
deleted file mode 100644
index 2c99d4b941..0000000000
--- a/src/app/icons/InfoCircleIcon.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { SVGProps } from "react";
-
-export default function InfoCircleIcon(props: SVGProps) {
- return (
-
-
-
-
-
- );
-}
diff --git a/src/app/router/Prompt/Prompt.tsx b/src/app/router/Prompt/Prompt.tsx
index 36ffae360c..a063ca49b5 100644
--- a/src/app/router/Prompt/Prompt.tsx
+++ b/src/app/router/Prompt/Prompt.tsx
@@ -22,6 +22,8 @@ import Toaster from "~/app/components/Toast/Toaster";
import Providers from "~/app/context/Providers";
import RequireAuth from "~/app/router/RequireAuth";
import BitcoinConfirmGetAddress from "~/app/screens/Bitcoin/ConfirmGetAddress";
+import ConfirmSignPsbt from "~/app/screens/Bitcoin/ConfirmSignPsbt";
+import ConfirmPaymentAsync from "~/app/screens/ConfirmPaymentAsync";
import AlbyEnable from "~/app/screens/Enable/AlbyEnable";
import LiquidEnable from "~/app/screens/Enable/LiquidEnable";
import NostrEnable from "~/app/screens/Enable/NostrEnable";
@@ -109,6 +111,10 @@ function Prompt() {
path="public/webbtc/confirmGetAddress"
element={ }
/>
+ }
+ />
}
@@ -137,6 +143,10 @@ function Prompt() {
} />
} />
} />
+ }
+ />
} />
} />
} />
diff --git a/src/app/screens/Accounts/Detail/index.tsx b/src/app/screens/Accounts/Detail/index.tsx
index 78ded234f9..54ad2b3a40 100644
--- a/src/app/screens/Accounts/Detail/index.tsx
+++ b/src/app/screens/Accounts/Detail/index.tsx
@@ -201,6 +201,7 @@ function AccountDetail() {
onChange={(event) => {
setAccountName(event.target.value);
}}
+ required
/>
@@ -403,8 +404,7 @@ function AccountDetail() {
)}
diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx
new file mode 100644
index 0000000000..8afcf54d4a
--- /dev/null
+++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.test.tsx
@@ -0,0 +1,144 @@
+import { act, render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter } from "react-router-dom";
+import msg from "~/common/lib/msg";
+import state from "~/extension/background-script/state";
+import { btcFixture } from "~/fixtures/btc";
+import type { OriginData } from "~/types";
+
+import { getFormattedSats } from "~/common/utils/currencyConvert";
+import Bitcoin from "~/extension/background-script/bitcoin";
+import Mnemonic from "~/extension/background-script/mnemonic";
+import ConfirmSignPsbt 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: {
+ psbt: btcFixture.regtestTaprootPsbt,
+ },
+ })),
+ };
+});
+
+const passwordMock = jest.fn;
+
+const mockState = {
+ password: passwordMock,
+ currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97",
+ getAccount: () => ({
+ mnemonic: btcFixture.mnemonic,
+ bitcoinNetwork: "regtest",
+ }),
+ getMnemonic: () => new Mnemonic(btcFixture.mnemonic),
+ getBitcoin: () => new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest"),
+ getConnector: jest.fn(),
+};
+
+state.getState = jest.fn().mockReturnValue(mockState);
+
+jest.mock("~/app/context/SettingsContext", () => ({
+ useSettings: () => ({
+ getFormattedSats: (amount: number) =>
+ getFormattedSats({
+ amount,
+ locale: "en",
+ }),
+ }),
+}));
+
+// mock getPsbtPreview request
+msg.request = jest
+ .fn()
+ .mockReturnValue(
+ new Bitcoin(new Mnemonic(btcFixture.mnemonic), "regtest").getPsbtPreview(
+ btcFixture.regtestTaprootPsbt
+ )
+ );
+
+describe("ConfirmSignPsbt", () => {
+ test("render", async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const user = userEvent.setup();
+
+ await act(async () => {
+ await user.click(screen.getByText("View details"));
+ });
+
+ expect(
+ await screen.findByText(
+ "This website asks you to sign a bitcoin transaction"
+ )
+ ).toBeInTheDocument();
+
+ // Check inputs
+ const inputsContainer = (await screen.getByText("Inputs")
+ .parentElement) as HTMLElement;
+ expect(inputsContainer).toBeInTheDocument();
+ const inputsRef = within(inputsContainer);
+ expect(
+ await inputsRef.findByText(
+ "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg"
+ )
+ ).toBeInTheDocument();
+
+ // Check outputs
+ const outputsContainer = screen.getByText("Outputs")
+ .parentElement as HTMLElement;
+ expect(outputsContainer).toBeInTheDocument();
+
+ const outputsRef = within(outputsContainer);
+ expect(
+ await outputsRef.findByText(
+ "bcrt1p6uav7en8k7zsumsqugdmg5j6930zmzy4dg7jcddshsr0fvxlqx7qnc7l22"
+ )
+ ).toBeInTheDocument();
+
+ expect(
+ await outputsRef.findByText(
+ "bcrt1p90h6z3p36n9hrzy7580h5l429uwchyg8uc9sz4jwzhdtuhqdl5eqkcyx0f"
+ )
+ ).toBeInTheDocument();
+
+ // Check fee
+ const feeContainer = screen.getByText("Fee").parentElement as HTMLElement;
+ expect(feeContainer).toBeInTheDocument();
+
+ const feeRef = within(feeContainer);
+ expect(await feeRef.findByText("155 sats")).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(screen.getByText("View raw transaction (Hex)"));
+ });
+ expect(
+ await screen.findByText(btcFixture.regtestTaprootPsbt)
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx
new file mode 100644
index 0000000000..03355a37f8
--- /dev/null
+++ b/src/app/screens/Bitcoin/ConfirmSignPsbt/index.tsx
@@ -0,0 +1,194 @@
+import ConfirmOrCancel from "@components/ConfirmOrCancel";
+import Container from "@components/Container";
+import PublisherCard from "@components/PublisherCard";
+import SuccessMessage from "@components/SuccessMessage";
+import { TFunction } from "i18next";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import Hyperlink from "~/app/components/Hyperlink";
+import Loading from "~/app/components/Loading";
+import ScreenHeader from "~/app/components/ScreenHeader";
+import toast from "~/app/components/Toast";
+import { useSettings } from "~/app/context/SettingsContext";
+import { useNavigationState } from "~/app/hooks/useNavigationState";
+import { USER_REJECTED_ERROR } from "~/common/constants";
+import api from "~/common/lib/api";
+import msg from "~/common/lib/msg";
+import type { Address, OriginData, PsbtPreview } from "~/types";
+
+function ConfirmSignPsbt() {
+ const navState = useNavigationState();
+ const { t: tCommon } = useTranslation("common");
+ const { t } = useTranslation("translation", {
+ keyPrefix: "bitcoin.confirm_sign_psbt",
+ });
+ const navigate = useNavigate();
+ const { getFormattedSats } = useSettings();
+
+ const psbt = navState.args?.psbt as string;
+ const origin = navState.origin as OriginData;
+ const [loading, setLoading] = useState(true);
+ const [successMessage, setSuccessMessage] = useState("");
+ const [preview, setPreview] = useState
(undefined);
+ const [showAddresses, setShowAddresses] = useState(false);
+ const [showHex, setShowHex] = useState(false);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const preview = await api.bitcoin.getPsbtPreview(psbt);
+ setPreview(preview);
+ setLoading(false);
+ } catch (e) {
+ console.error(e);
+ const error = e as { message: string };
+ const errorMessage = error.message || "Unknown error";
+ toast.error(`${tCommon("error")}: ${errorMessage}`);
+ }
+ })();
+ }, [origin, psbt, tCommon]);
+
+ async function confirm() {
+ try {
+ setLoading(true);
+ const response = await api.bitcoin.signPsbt(psbt);
+ msg.reply(response);
+ setSuccessMessage(tCommon("success"));
+ } catch (e) {
+ console.error(e);
+ const error = e as { message: string };
+ const errorMessage = error.message || "Unknown error";
+ toast.error(`${tCommon("error")}: ${errorMessage}`);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function reject(e: React.MouseEvent) {
+ e.preventDefault();
+ msg.error(USER_REJECTED_ERROR);
+ }
+
+ function close(e: React.MouseEvent) {
+ if (navState.isPrompt) {
+ window.close();
+ } else {
+ e.preventDefault();
+ navigate(-1);
+ }
+ }
+
+ function toggleShowAddresses() {
+ setShowAddresses((current) => !current);
+ }
+ function toggleShowHex() {
+ setShowHex((current) => !current);
+ }
+
+ if (!preview) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {!successMessage ? (
+
+
+
+
+
+ {t("allow_sign", { host: origin.host })}
+
+
+
+
+ {showAddresses ? t("hide_details") : t("view_details")}
+
+
+
+ {showAddresses && (
+ <>
+
+
{t("inputs")}
+
+ {preview.inputs.map((input) => (
+
+ ))}
+
+
{t("outputs")}
+
+ {preview.outputs.map((output) => (
+
+ ))}
+
+
{t("fee")}
+
+ {getFormattedSats(preview.fee)}
+
+
+
+
+ {showHex
+ ? t("hide_raw_transaction")
+ : t("view_raw_transaction")}
+
+
+ >
+ )}
+
+ {showHex && (
+
+ {psbt}
+
+ )}
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+}
+
+function AddressPreview({
+ address,
+ amount,
+ t,
+}: Address & {
+ t: TFunction<"translation", "bitcoin.confirm_sign_psbt", "translation">;
+}) {
+ const { getFormattedSats } = useSettings();
+ return (
+
+
{address}
+
+ {getFormattedSats(amount)}
+
+
+ );
+}
+
+export default ConfirmSignPsbt;
diff --git a/src/app/screens/ConfirmPayment/index.tsx b/src/app/screens/ConfirmPayment/index.tsx
index 4138f41c76..90321990ad 100644
--- a/src/app/screens/ConfirmPayment/index.tsx
+++ b/src/app/screens/ConfirmPayment/index.tsx
@@ -15,6 +15,7 @@ import { useAccount } from "~/app/context/AccountContext";
import { useSettings } from "~/app/context/SettingsContext";
import { useNavigationState } from "~/app/hooks/useNavigationState";
import { USER_REJECTED_ERROR } from "~/common/constants";
+import api from "~/common/lib/api";
import msg from "~/common/lib/msg";
function ConfirmPayment() {
@@ -76,16 +77,9 @@ function ConfirmPayment() {
try {
setLoading(true);
- // TODO: move to api
- const response = await msg.request(
- "sendPayment",
- { paymentRequest: paymentRequest },
- {
- origin: navState.origin,
- }
- );
- if (response.error) {
- throw new Error(response.error as string);
+ const response = await api.sendPayment(paymentRequest, navState.origin);
+ if ("error" in response) {
+ throw new Error(response.error);
}
auth.fetchAccountInfo(); // Update balance.
@@ -156,7 +150,7 @@ function ConfirmPayment() {
diff --git a/src/app/screens/ConfirmPaymentAsync/index.test.tsx b/src/app/screens/ConfirmPaymentAsync/index.test.tsx
new file mode 100644
index 0000000000..dd525f0fed
--- /dev/null
+++ b/src/app/screens/ConfirmPaymentAsync/index.test.tsx
@@ -0,0 +1,80 @@
+import { act, render, screen } from "@testing-library/react";
+import { MemoryRouter } from "react-router-dom";
+import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings";
+import type { OriginData } from "~/types";
+
+import ConfirmPaymentAsync 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,
+};
+
+const paymentRequest =
+ "lnbc250n1p3qzycupp58uc2wa29470f98wrxmy4xwuqt8cywjygf5t2cp0s376y7nwdyq3sdqhf35kw6r5de5kueeqg3jk6mccqzpgxqyz5vqsp5wfdmwtv5rmru00ajsnn3f8lzpxa4snug2tmqvc8zj8semr4kjjts9qyyssq83h74pte8nrkqs8sr2hscv5zcdmhwunwnd6xr3mskeayh96pu7ksswa6p7trknlpp6t3js4k6uytxutv5ecgcwaxz7fj4zfy5khjcjcpf66muy";
+
+let parameters = {};
+
+jest.mock("~/app/hooks/useNavigationState", () => {
+ return {
+ useNavigationState: jest.fn(() => parameters),
+ };
+});
+
+let mockGetFiatValue = jest.fn();
+let mockSettingsTmp = { ...mockSettings };
+
+jest.mock("~/app/context/SettingsContext", () => ({
+ useSettings: () => ({
+ settings: mockSettingsTmp,
+ isLoading: false,
+ updateSetting: jest.fn(),
+ getFormattedFiat: mockGetFiatValue,
+ getFormattedNumber: jest.fn(),
+ getFormattedSats: jest.fn(() => "25 sats"),
+ }),
+}));
+
+describe("ConfirmPaymentAsync", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("prompt: renders with fiat", async () => {
+ parameters = {
+ origin: mockOrigin,
+ args: {
+ paymentRequest,
+ },
+ };
+
+ mockSettingsTmp = { ...mockSettings };
+ mockGetFiatValue = jest.fn(() => Promise.resolve("$0.01"));
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ expect(await screen.findByText("Amount")).toBeInTheDocument();
+ expect(await screen.findByText("Description")).toBeInTheDocument();
+ expect(await screen.findByText("(~$0.01)")).toBeInTheDocument();
+ });
+});
diff --git a/src/app/screens/ConfirmPaymentAsync/index.tsx b/src/app/screens/ConfirmPaymentAsync/index.tsx
new file mode 100644
index 0000000000..26a12cd2f7
--- /dev/null
+++ b/src/app/screens/ConfirmPaymentAsync/index.tsx
@@ -0,0 +1,140 @@
+import ConfirmOrCancel from "@components/ConfirmOrCancel";
+import Container from "@components/Container";
+import PaymentSummary from "@components/PaymentSummary";
+import PublisherCard from "@components/PublisherCard";
+import lightningPayReq from "bolt11";
+import { useEffect, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import Alert from "~/app/components/Alert";
+import Hyperlink from "~/app/components/Hyperlink";
+import ScreenHeader from "~/app/components/ScreenHeader";
+import toast from "~/app/components/Toast";
+import { useSettings } from "~/app/context/SettingsContext";
+import { useNavigationState } from "~/app/hooks/useNavigationState";
+import { USER_REJECTED_ERROR } from "~/common/constants";
+import api from "~/common/lib/api";
+import msg from "~/common/lib/msg";
+
+function ConfirmPaymentAsync() {
+ const {
+ isLoading: isLoadingSettings,
+ settings,
+ getFormattedFiat,
+ } = useSettings();
+
+ const showFiat = !isLoadingSettings && settings.showFiat;
+
+ const { t } = useTranslation("translation", {
+ keyPrefix: "confirm_payment_async",
+ });
+
+ const navState = useNavigationState();
+ const paymentRequest = navState.args?.paymentRequest as string;
+ const invoice = lightningPayReq.decode(paymentRequest);
+
+ const navigate = useNavigate();
+
+ const [fiatAmount, setFiatAmount] = useState("");
+
+ useEffect(() => {
+ (async () => {
+ if (showFiat && invoice.satoshis) {
+ const res = await getFormattedFiat(invoice.satoshis);
+ setFiatAmount(res);
+ }
+ })();
+ }, [invoice.satoshis, showFiat, getFormattedFiat]);
+
+ const [loading, setLoading] = useState(false);
+
+ async function confirm() {
+ try {
+ setLoading(true);
+ const response = await api.sendPaymentAsync(
+ paymentRequest,
+ navState.origin
+ );
+
+ if ("error" in response) {
+ throw new Error(response.error);
+ }
+
+ msg.reply(response);
+ } catch (e) {
+ console.error(e);
+ if (e instanceof Error) toast.error(`Error: ${e.message}`);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function reject(e: React.MouseEvent
) {
+ e.preventDefault();
+ if (navState.isPrompt) {
+ msg.error(USER_REJECTED_ERROR);
+ } else {
+ navigate(-1);
+ }
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ confirm();
+ }
+
+ return (
+
+ );
+}
+
+export default ConfirmPaymentAsync;
diff --git a/src/app/screens/Enable/WebbtcEnable.tsx b/src/app/screens/Enable/WebbtcEnable.tsx
index 99fc52d15e..fe325dcf3c 100644
--- a/src/app/screens/Enable/WebbtcEnable.tsx
+++ b/src/app/screens/Enable/WebbtcEnable.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
-import LiquidEnableComponent from "~/app/components/Enable/LiquidEnable";
+import WebbtcEnableComponent from "~/app/components/Enable/WebbtcEnable";
import Onboard from "~/app/components/onboard";
import { useAccount } from "~/app/context/AccountContext";
import api from "~/common/lib/api";
@@ -34,7 +34,7 @@ export default function WebbtcEnable(props: Props) {
return (
<>
{hasMnemonic ? (
-
+
) : (
)}
diff --git a/src/app/screens/LNURLAuth/index.tsx b/src/app/screens/LNURLAuth/index.tsx
index 6d9e71b406..c09def68a3 100644
--- a/src/app/screens/LNURLAuth/index.tsx
+++ b/src/app/screens/LNURLAuth/index.tsx
@@ -1,140 +1,38 @@
-import Button from "@components/Button";
-import ConfirmOrCancel from "@components/ConfirmOrCancel";
-import Container from "@components/Container";
-import ContentMessage from "@components/ContentMessage";
-import PublisherCard from "@components/PublisherCard";
-import ResultCard from "@components/ResultCard";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { useNavigate } from "react-router-dom";
-import ScreenHeader from "~/app/components/ScreenHeader";
-import { useNavigationState } from "~/app/hooks/useNavigationState";
-import { USER_REJECTED_ERROR } from "~/common/constants";
+import { useEffect, useState } from "react";
+import LNURLAuthComponent from "~/app/components/LNURLAuth";
+import Onboard from "~/app/components/onboard";
+import { useAccount } from "~/app/context/AccountContext";
+import { isAlbyOAuthAccount } from "~/app/utils";
import api from "~/common/lib/api";
-import msg from "~/common/lib/msg";
-import type { LNURLAuthServiceResponse } from "~/types";
-function LNURLAuth() {
- const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" });
- const { t: tCommon } = useTranslation("common");
-
- const navigate = useNavigate();
- const navState = useNavigationState();
-
- const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse;
- const origin = navState.origin;
-
- const [successMessage, setSuccessMessage] = useState("");
- const [errorMessage, setErrorMessage] = useState("");
- const [loading, setLoading] = useState(false);
-
- async function confirm() {
- try {
- setLoading(true);
- const response = await api.lnurlAuth({
- origin,
- lnurlDetails: details,
- });
-
- if (navState.isPrompt && origin?.host) {
- const allowance = await api.getAllowance(origin.host);
-
- if (allowance.lnurlAuth === false) {
- await msg.request("updateAllowance", {
- id: allowance.id,
- lnurlAuth: true,
- });
+export default function LNURLAuth() {
+ const { account } = useAccount();
+ const [hasMnemonic, setHasMnemonic] = useState(false);
+ const [albyOAuthAccount, setAlbyOAuthAccount] = useState(false);
+
+ useEffect(() => {
+ async function fetchAccountInfo() {
+ try {
+ const fetchedAccount = await api.getAccount();
+ const isOAuthAccount = isAlbyOAuthAccount(fetchedAccount.connectorType);
+ setAlbyOAuthAccount(isOAuthAccount);
+
+ if (fetchedAccount.hasMnemonic) {
+ setHasMnemonic(true);
+ } else {
+ setHasMnemonic(false);
}
+ } catch (e) {
+ console.error(e);
}
-
- if (response.success) {
- setSuccessMessage(
- t("success", { name: origin ? origin.name : details.domain })
- );
- // ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction.
- // We assume this is OK when it is called through webln.
- if (navState.isPrompt) {
- msg.reply(response);
- }
- } else {
- setErrorMessage(t("errors.status"));
- }
- } catch (e) {
- console.error(e);
- if (e instanceof Error) {
- setErrorMessage(`Error: ${e.message}`);
- }
- } finally {
- setLoading(false);
- }
- }
-
- function reject(e: React.MouseEvent) {
- e.preventDefault();
- if (navState.isPrompt) {
- msg.error(USER_REJECTED_ERROR);
- } else {
- navigate(-1);
}
- }
- function close(e: React.MouseEvent) {
- // will never be reached via prompt
- e.preventDefault();
- navigate(-1);
- }
+ fetchAccountInfo();
+ }, [account]);
return (
-
-
- {!successMessage ? (
- <>
-
-
- {origin ? (
-
- ) : (
-
- )}
-
-
-
- {errorMessage && (
-
{errorMessage}
- )}
-
-
-
- >
- ) : (
-
-
-
-
-
-
- )}
-
+ <>
+ {albyOAuthAccount && !hasMnemonic ? : }
+ >
);
}
-
-export default LNURLAuth;
diff --git a/src/app/screens/Liquid/ConfirmSignPset.tsx b/src/app/screens/Liquid/ConfirmSignPset.tsx
index 119458a732..862ed58f94 100644
--- a/src/app/screens/Liquid/ConfirmSignPset.tsx
+++ b/src/app/screens/Liquid/ConfirmSignPset.tsx
@@ -117,14 +117,16 @@ function ConfirmSignPset() {
-
-
- {showDetails ? t("hide_details") : t("view_details")}
-
-
+
+
+
+ {showDetails ? t("hide_details") : t("view_details")}
+
+
- {showDetails && (
- <>
+ {showDetails && (
+ <>
+
{t("inputs")}
@@ -154,15 +156,17 @@ function ConfirmSignPset() {
))}
+
+
{showRawTransaction
? t("hide_raw_transaction")
: t("view_raw_transaction")}
- >
- )}
-
+
+ >
+ )}
{showRawTransaction && (
diff --git a/src/app/screens/Receive/index.tsx b/src/app/screens/Receive/index.tsx
index a2966598d4..9558692df9 100644
--- a/src/app/screens/Receive/index.tsx
+++ b/src/app/screens/Receive/index.tsx
@@ -76,14 +76,10 @@ function Receive() {
) : (
<>
-
>
)}
diff --git a/src/app/screens/ReceiveInvoice/index.tsx b/src/app/screens/ReceiveInvoice/index.tsx
index e0dfe387eb..02242e8e2c 100644
--- a/src/app/screens/ReceiveInvoice/index.tsx
+++ b/src/app/screens/ReceiveInvoice/index.tsx
@@ -1,6 +1,5 @@
import {
CaretLeftIcon,
- CheckIcon,
CopyIcon,
} from "@bitcoin-design/bitcoin-icons-react/outline";
import Button from "@components/Button";
@@ -15,6 +14,7 @@ import Confetti from "react-confetti";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import QRCode from "~/app/components/QRCode";
+import ResultCard from "~/app/components/ResultCard";
import toast from "~/app/components/Toast";
import { useAccount } from "~/app/context/AccountContext";
import { useSettings } from "~/app/context/SettingsContext";
@@ -46,9 +46,6 @@ function ReceiveInvoice() {
paymentRequest: string;
rHash: string;
} | null>();
- const [copyInvoiceLabel, setCopyInvoiceLabel] = useState(
- tCommon("actions.copy_invoice") as string
- );
const [paid, setPaid] = useState(false);
const [pollingForPayment, setPollingForPayment] = useState(false);
@@ -142,44 +139,22 @@ function ReceiveInvoice() {
if (!invoice) return null;
return (
<>
-
- {paid && (
-
- {
- setDefaults();
- navigate("/receive/invoice");
- }}
- />
-
- )}
{!paid && (
<>
-
+
+
{
try {
navigator.clipboard.writeText(invoice.paymentRequest);
- setCopyInvoiceLabel(tCommon("copied"));
- setTimeout(() => {
- setCopyInvoiceLabel(tCommon("actions.copy_invoice"));
- }, 1000);
+ toast.success(tCommon("copied"));
} catch (e) {
if (e instanceof Error) {
toast.error(e.message);
@@ -187,11 +162,10 @@ function ReceiveInvoice() {
}
}}
icon={ }
- label={copyInvoiceLabel}
+ label={tCommon("actions.copy_invoice")}
primary
/>
-
{pollingForPayment && (
@@ -210,15 +184,30 @@ function ReceiveInvoice() {
>
)}
{paid && (
-
{
- confetti && confetti.reset();
- }}
- style={{ pointerEvents: "none" }}
- />
+ <>
+
+
+ {
+ setDefaults();
+ navigate("/receive/invoice");
+ }}
+ />
+
+ {
+ confetti && confetti.reset();
+ }}
+ style={{ pointerEvents: "none" }}
+ />
+ >
)}
>
);
@@ -239,11 +228,13 @@ function ReceiveInvoice() {
{t("title")}
{invoice ? (
- {renderInvoice()}
+
+ {renderInvoice()}
+
) : (
-