summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-02-04 12:04:27 -0300
committerSebastian <sebasjm@gmail.com>2024-02-05 09:18:22 -0300
commitb71d6f2b11342bd22197289ad3872d8a341686b5 (patch)
treed263482af0121d87000476fd0306530a77580817 /packages
parent83ff7de59b8a00b313ecb00f4c6150a37c38902f (diff)
downloadwallet-core-b71d6f2b11342bd22197289ad3872d8a341686b5.tar.gz
wallet-core-b71d6f2b11342bd22197289ad3872d8a341686b5.tar.bz2
wallet-core-b71d6f2b11342bd22197289ad3872d8a341686b5.zip
wip DD39: removed webRequest permission and changes made into demobank
Diffstat (limited to 'packages')
-rw-r--r--packages/demobank-ui/src/Routing.tsx153
-rw-r--r--packages/demobank-ui/src/components/app.tsx8
-rw-r--r--packages/demobank-ui/src/context/navigation.ts80
-rw-r--r--packages/demobank-ui/src/context/wallet-integration.ts90
-rw-r--r--packages/demobank-ui/src/index.html42
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx16
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx49
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx10
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx19
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx31
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx46
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx2
-rw-r--r--packages/demobank-ui/src/route.ts124
-rw-r--r--packages/taler-wallet-webextension/manifest-v2.json1
-rw-r--r--packages/taler-wallet-webextension/manifest-v3.json1
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts35
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts300
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts7
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts11
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts121
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts26
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx205
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx23
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts10
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts81
26 files changed, 660 insertions, 835 deletions
diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx
index e73493d60..442a276a0 100644
--- a/packages/demobank-ui/src/Routing.tsx
+++ b/packages/demobank-ui/src/Routing.tsx
@@ -48,6 +48,8 @@ import { RemoveAccount } from "./pages/admin/RemoveAccount.js";
import { CreateCashout } from "./pages/business/CreateCashout.js";
import { ShowCashoutDetails } from "./pages/business/ShowCashoutDetails.js";
import { RouteParamsType, urlPattern, useCurrentLocation } from "./route.js";
+import { useNavigationContext } from "./context/navigation.js";
+import { useEffect } from "preact/hooks";
export function Routing(): VNode {
const backend = useBackendState();
@@ -89,12 +91,18 @@ function PublicRounting({
}): VNode {
const settings = useSettingsContext();
const { i18n } = useTranslationContext();
- const [loc, routeTo] = useCurrentLocation(publicPages);
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext()
const { api } = useBankCoreApiContext();
const [notification, notify, handleError] = useLocalNotification();
- if (loc === undefined) {
- routeTo("login", {});
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.home.url({}))
+ }
+ }, [location])
+
+ if (location === undefined) {
return <Fragment />;
}
@@ -132,7 +140,7 @@ function PublicRounting({
});
}
- switch (loc.name) {
+ switch (location.name) {
case "login": {
return (
<Fragment>
@@ -148,17 +156,17 @@ function PublicRounting({
return <PublicHistoriesPage />;
}
case "operationDetails": {
- const { wopid } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { wopid } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<WithdrawalOperationPage
operationId={wopid}
- onOperationAborted={() => routeTo("login", {})}
+ onOperationAborted={() => navigateTo(publicPages.login.url({}))}
routeClose={publicPages.login}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(publicPages.solveSecondFactor.url({}))}
/>
);
}
@@ -176,18 +184,17 @@ function PublicRounting({
case "solveSecondFactor": {
return (
<SolveChallengePage
- onChallengeCompleted={() => routeTo("login", {})}
+ onChallengeCompleted={() => navigateTo(publicPages.login.url({}))}
routeClose={publicPages.login}
/>
);
}
default:
- assertUnreachable(loc.name);
+ assertUnreachable(location.name);
}
}
export const privatePages = {
- home: urlPattern(/\/account/, () => "#/account"),
homeChargeWallet: urlPattern(
/\/account\/charge-wallet/,
() => "#/account/charge-wallet",
@@ -196,6 +203,7 @@ export const privatePages = {
/\/account\/wire-transfer/,
() => "#/account/wire-transfer",
),
+ home: urlPattern(/\/account/, () => "#/account"),
solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
cashoutDetails: urlPattern<{ cid: string }>(
@@ -233,7 +241,7 @@ export const privatePages = {
({ account }) => `#/profile/${account}/cashouts`,
),
operationDetails: urlPattern<{ wopid: string }>(
- /\/operation\/(?<wopid>[a-zA-Z0-9]+)/,
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
({ wopid }) => `#/operation/${wopid}`,
),
};
@@ -245,33 +253,38 @@ function PrivateRouting({
username: string;
isAdmin: boolean;
}): VNode {
- const [loc, routeTo] = useCurrentLocation(privatePages);
+ const { navigateTo } = useNavigationContext()
+ const location = useCurrentLocation(privatePages);
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.home.url({}))
+ }
+ }, [location])
- if (loc === undefined) {
- routeTo("home", {});
+ if (location === undefined) {
return <Fragment />;
}
- switch (loc.name) {
+ switch (location.name) {
case "operationDetails": {
- const { wopid } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { wopid } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<WithdrawalOperationPage
operationId={wopid}
- onOperationAborted={() => routeTo("home", {})}
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
routeClose={privatePages.home}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
/>
);
}
case "solveSecondFactor": {
return (
<SolveChallengePage
- onChallengeCompleted={() => routeTo("home", {})}
+ onChallengeCompleted={() => navigateTo(privatePages.home.url({}))}
routeClose={privatePages.home}
/>
);
@@ -286,64 +299,64 @@ function PrivateRouting({
return (
<CreateNewAccount
routeCancel={privatePages.home}
- onCreateSuccess={() => routeTo("home", {})}
+ onCreateSuccess={() => navigateTo(privatePages.home.url({}))}
/>
);
}
case "accountDetails": {
- const { account } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { account } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<ShowAccountDetails
account={account}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
}
case "accountChangePassword": {
- const { account } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { account } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<UpdateAccountPassword
focus
account={account}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
}
case "accountDelete": {
- const { account } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { account } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<RemoveAccount
account={account}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeCancel={privatePages.home}
/>
);
}
case "accountCashouts": {
- const { account } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { account } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<CashoutListForAccount
account={account}
routeCashoutDetails={privatePages.cashoutDetails}
routeClose={privatePages.home}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
/>
);
}
@@ -351,8 +364,8 @@ function PrivateRouting({
return (
<RemoveAccount
account={username}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeCancel={privatePages.home}
/>
);
@@ -361,8 +374,8 @@ function PrivateRouting({
return (
<ShowAccountDetails
account={username}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
@@ -372,8 +385,8 @@ function PrivateRouting({
<UpdateAccountPassword
focus
account={username}
- onUpdateSuccess={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
@@ -383,7 +396,7 @@ function PrivateRouting({
<CashoutListForAccount
account={username}
routeCashoutDetails={privatePages.cashoutDetails}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
@@ -392,7 +405,7 @@ function PrivateRouting({
if (isAdmin) {
return (
<AdminHome
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeCreate={privatePages.accountCreate}
routeRemoveAccount={privatePages.accountDelete}
routeShowAccount={privatePages.accountDetails}
@@ -408,9 +421,9 @@ function PrivateRouting({
routeChargeWallet={privatePages.homeChargeWallet}
routeWireTransfer={privatePages.homeWireTransfer}
routeClose={privatePages.home}
- onClose={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
- onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
+ onOperationCreated={(wopid) => navigateTo(privatePages.operationDetails.url({ wopid }))}
/>
);
}
@@ -418,15 +431,15 @@ function PrivateRouting({
return (
<CreateCashout
account={username}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeClose={privatePages.home}
/>
);
}
case "cashoutDetails": {
- const { cid } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { cid } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<ShowCashoutDetails
@@ -436,16 +449,16 @@ function PrivateRouting({
);
}
case "wireTranserCreate": {
- const { destination } = loc.values as RouteParamsType<
- typeof loc.parent,
- typeof loc.name
+ const { destination } = location.values as RouteParamsType<
+ typeof location.parent,
+ typeof location.name
>;
return (
<WireTransfer
toAccount={destination}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
routeCancel={privatePages.home}
- onSuccess={() => routeTo("home", {})}
+ onSuccess={() => navigateTo(privatePages.home.url({}))}
/>
);
}
@@ -457,9 +470,9 @@ function PrivateRouting({
routeChargeWallet={privatePages.homeChargeWallet}
routeWireTransfer={privatePages.homeWireTransfer}
routeClose={privatePages.home}
- onClose={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
- onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
+ onOperationCreated={(wopid) => navigateTo(privatePages.operationDetails.url({ wopid }))}
/>
);
}
@@ -471,13 +484,13 @@ function PrivateRouting({
routeChargeWallet={privatePages.homeChargeWallet}
routeWireTransfer={privatePages.homeWireTransfer}
routeClose={privatePages.home}
- onClose={() => routeTo("home", {})}
- onAuthorizationRequired={() => routeTo("solveSecondFactor", {})}
- onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({}))}
+ onOperationCreated={(wopid) => navigateTo(privatePages.operationDetails.url({ wopid }))}
/>
);
}
default:
- assertUnreachable(loc.name);
+ assertUnreachable(location.name);
}
}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index 31013388b..97778e6d7 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -30,6 +30,8 @@ import { SettingsProvider } from "../context/settings.js";
import { strings } from "../i18n/strings.js";
import { BankFrame } from "../pages/BankFrame.js";
import { BankUiSettings, fetchSettings } from "../settings.js";
+import { TalerWalletIntegrationBrowserProvider } from "../context/wallet-integration.js";
+import { BrowserHashNavigationProvider } from "../context/navigation.js";
const WITH_LOCAL_STORAGE_CACHE = false;
const App: FunctionalComponent = () => {
@@ -78,7 +80,11 @@ const App: FunctionalComponent = () => {
keepPreviousData: true,
}}
>
- <Routing />
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
</SWRConfig>
</BankCoreApiProvider>
</BackendStateProvider>
diff --git a/packages/demobank-ui/src/context/navigation.ts b/packages/demobank-ui/src/context/navigation.ts
new file mode 100644
index 000000000..fc1460c02
--- /dev/null
+++ b/packages/demobank-ui/src/context/navigation.ts
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = {
+ path: string;
+ params: Record<string, string>;
+ navigateTo: (path: string) => void;
+ // addNavigationListener: (listener: (path: string, params: Record<string, string>) => void) => (() => void);
+};
+
+// @ts-expect-error shold not be used without provider
+const Context = createContext<Type>(undefined);
+
+export const useNavigationContext = (): Type => useContext(Context);
+
+function getPathAndParamsFromWindow() {
+ const path = typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
+ const params: Record<string, string> = {}
+ if (typeof window !== "undefined") {
+ for (const [key, value] of new URLSearchParams(window.location.search)) {
+ params[key] = value;
+ }
+ }
+ return { path, params }
+}
+
+const { path: initialPath, params: initialParams } = getPathAndParamsFromWindow()
+
+// there is a posibility that if the browser does a redirection
+// (which doesn't go through navigatTo function) and that exectued
+// too early (before addEventListener runs) it won't be taking
+// into account
+const PopStateEventType = "popstate";
+
+export const BrowserHashNavigationProvider = ({ children }: { children: ComponentChildren }): VNode => {
+ const [{ path, params }, setState] = useState({ path: initialPath, params: initialParams })
+ if (typeof window === "undefined") {
+ throw Error("Can't use BrowserHashNavigationProvider if there is no window object")
+ }
+ function navigateTo(path: string) {
+ const { params } = getPathAndParamsFromWindow()
+ setState({ path, params })
+ window.location.href = path
+ }
+
+ useEffect(() => {
+ function eventListener() {
+ setState(getPathAndParamsFromWindow())
+ }
+ window.addEventListener(PopStateEventType, eventListener);
+ return () => {
+ window.removeEventListener(PopStateEventType, eventListener)
+ }
+ }, [])
+ return h(Context.Provider, {
+ value: { path, params, navigateTo },
+ children,
+ });
+};
diff --git a/packages/demobank-ui/src/context/wallet-integration.ts b/packages/demobank-ui/src/context/wallet-integration.ts
new file mode 100644
index 000000000..47bdc90ec
--- /dev/null
+++ b/packages/demobank-ui/src/context/wallet-integration.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ stringifyTalerUri,
+ TalerUri
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ createContext,
+ h,
+ VNode
+} from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ * https://docs.taler.net/design-documents/039-taler-browser-integration.html
+ *
+ * @param uri
+ */
+function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) {
+
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", stringifyTalerUri(uri));
+
+ document.head.appendChild(meta);
+
+ let walletFound = false
+ window.addEventListener("beforeunload", () => {
+ walletFound = true
+ })
+ setTimeout(() => {
+ if (!walletFound && onNotFound) {
+ onNotFound()
+ }
+ }, 10)//very short timeout
+}
+interface Type {
+ /**
+ * Tell the active wallet that an action is found
+ *
+ * @param uri
+ * @returns
+ */
+ publishTalerAction: (uri: TalerUri, onNotFound?: () => void) => void;
+}
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const Context = createContext<Type>(undefined);
+
+export const useTalerWalletIntegrationAPI = (): Type => useContext(Context);
+
+export const TalerWalletIntegrationBrowserProvider = ({ children }: { children: ComponentChildren }): VNode => {
+ const value: Type = {
+ publishTalerAction: createHeadMetaTag
+ };
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+
+export const TalerWalletIntegrationTestingProvider = ({
+ children,
+ value,
+}: {
+ children: ComponentChildren;
+ value: Type;
+}): VNode => {
+
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/demobank-ui/src/index.html b/packages/demobank-ui/src/index.html
index 720b678a3..6e0638e3f 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/demobank-ui/src/index.html
@@ -17,25 +17,25 @@
-->
<!doctype html>
<html lang="en" class="h-full bg-gray-100">
- <head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri" />
- <meta name="mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-capable" content="yes" />
- <link
- rel="icon"
- href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
- />
- <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
- <title>Bank</title>
- <!-- Entry point for the bank SPA. -->
- <script type="module" src="index.js"></script>
- <link rel="stylesheet" href="index.css" />
- </head>
- <body class="h-full">
- <div id="app"></div>
- </body>
-</html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Bank</title>
+ <!-- Entry point for the bank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+</head>
+
+<body class="h-full">
+ <div id="app"></div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
index ac3724eb8..4d193505e 100644
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -36,6 +36,7 @@ import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
import { State } from "./index.js";
+import { useTalerWalletIntegrationAPI } from "../../context/wallet-integration.js";
export function InvalidPaytoView({ payto }: State.InvalidPayto) {
return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
@@ -328,23 +329,12 @@ export function ReadyView({
onAbort: doAbort,
}: State.Ready): VNode<Record<string, never>> {
const { i18n } = useTranslationContext();
+ const walletInegrationApi = useTalerWalletIntegrationAPI()
const [notification, notify, errorHandler] = useLocalNotification();
const talerWithdrawUri = stringifyWithdrawUri(uri);
useEffect(() => {
- // Taler Wallet WebExtension is listening to headers response and tab updates.
- // In the SPA there is no header response with the Taler URI so
- // this hack manually triggers the tab update after the QR is in the DOM.
- // WebExtension will be using
- // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
- document.title = `${document.title} ${uri.withdrawalOperationId}`;
- const meta = document.createElement("meta");
- meta.setAttribute("name", "taler-uri");
- meta.setAttribute("content", talerWithdrawUri);
- document.head.insertBefore(
- meta,
- document.head.children.length ? document.head.children[0] : null,
- );
+ walletInegrationApi.publishTalerAction(uri)
}, []);
async function onAbort() {
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 53086d4cc..51a6a17a9 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -14,13 +14,43 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
+import { AmountJson, TalerError } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
import { useBankState } from "../hooks/bank-state.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
import { RouteDefinition } from "../route.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useWithdrawalDetails } from "../hooks/access.js";
+import { useEffect } from "preact/hooks";
+
+function ShowOperationPendingTag({ woid, onOperationAlreadyCompleted }: { woid: string, onOperationAlreadyCompleted?: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useWithdrawalDetails(woid);
+ const error = !result || result instanceof TalerError || result.type === "fail"
+ const completed = !error && (result.body.status === "aborted" || result.body.status === "confirmed")
+ useEffect(() => {
+ if (completed && onOperationAlreadyCompleted) {
+ onOperationAlreadyCompleted()
+ }
+ }, [completed])
+
+ if (error || completed) {
+ return <Fragment />;
+ }
+
+ return <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <svg
+ class="h-1.5 w-1.5 fill-green-500"
+ viewBox="0 0 6 6"
+ aria-hidden="true"
+ >
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>operation ready</i18n.Translate>
+ </span>
+
+}
/**
* Let the user choose a payment option,
@@ -46,7 +76,7 @@ export function PaymentOptions({
routeWireTransfer: RouteDefinition<Record<string, never>>;
}): VNode {
const { i18n } = useTranslationContext();
- const [bankState] = useBankState();
+ const [bankState, updateBankState] = useBankState();
return (
<div class="mt-4">
@@ -98,16 +128,9 @@ export function PaymentOptions({
</i18n.Translate>
</div>
{!!bankState.currentWithdrawalOperationId && (
- <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
- <svg
- class="h-1.5 w-1.5 fill-green-500"
- viewBox="0 0 6 6"
- aria-hidden="true"
- >
- <circle cx="3" cy="3" r="3" />
- </svg>
- <i18n.Translate>operation ready</i18n.Translate>
- </span>
+ <ShowOperationPendingTag woid={bankState.currentWithdrawalOperationId} onOperationAlreadyCompleted={() => {
+ updateBankState("currentWithdrawalOperationId", undefined)
+ }} />
)}
</div>
</label>
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
index a6615d578..02f30d8e8 100644
--- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
@@ -19,6 +19,7 @@ import { privatePages } from "../Routing.js";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
import { assertUnreachable } from "@gnu-taler/taler-util";
+import { useNavigationContext } from "../context/navigation.js";
export function ProfileNavigation({
current,
@@ -32,6 +33,7 @@ export function ProfileNavigation({
credentials.status !== "loggedIn"
? false
: !credentials.isUserAdministrator;
+ const { navigateTo } = useNavigationContext()
return (
<div>
<div class="sm:hidden">
@@ -46,19 +48,19 @@ export function ProfileNavigation({
const op = e.currentTarget.value as typeof current;
switch (op) {
case "details": {
- window.location.href = privatePages.myAccountDetails.url({});
+ navigateTo(privatePages.myAccountDetails.url({}));
return;
}
case "delete": {
- window.location.href = privatePages.myAccountDelete.url({});
+ navigateTo(privatePages.myAccountDelete.url({}));
return;
}
case "credentials": {
- window.location.href = privatePages.myAccountPassword.url({});
+ navigateTo(privatePages.myAccountPassword.url({}));
return;
}
case "cashouts": {
- window.location.href = privatePages.myAccountCashouts.url({});
+ navigateTo(privatePages.myAccountCashouts.url({}));
return;
}
default:
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index f21134aa1..037849804 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -19,7 +19,7 @@ import {
HttpStatusCode,
stringifyWithdrawUri,
TranslatedString,
- WithdrawUriResult,
+ WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
LocalNotificationBanner,
@@ -30,6 +30,7 @@ import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
import { useBankCoreApiContext } from "../context/config.js";
+import { useTalerWalletIntegrationAPI } from "../context/wallet-integration.js";
import { useBackendState } from "../hooks/backend.js";
export function QrCodeSection({
@@ -40,25 +41,15 @@ export function QrCodeSection({
onAborted: () => void;
}): VNode {
const { i18n } = useTranslationContext();
+ const walletInegrationApi = useTalerWalletIntegrationAPI()
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
useEffect(() => {
- // Taler Wallet WebExtension is listening to headers response and tab updates.
- // In the SPA there is no header response with the Taler URI so
- // this hack manually triggers the tab update after the QR is in the DOM.
- // WebExtension will be using
- // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
- document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`;
- const meta = document.createElement("meta");
- meta.setAttribute("name", "taler-uri");
- meta.setAttribute("content", talerWithdrawUri);
- document.head.insertBefore(
- meta,
- document.head.children.length ? document.head.children[0] : null,
- );
+ walletInegrationApi.publishTalerAction(withdrawUri)
}, []);
+
const [notification, notify, handleError] = useLocalNotification();
const { api } = useBankCoreApiContext();
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 1e48b818a..9f7f46c4f 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -21,6 +21,7 @@ import {
TranslatedString,
assertUnreachable,
parseWithdrawUri,
+ stringifyWithdrawUri,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -41,6 +42,8 @@ import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { OperationState } from "./OperationState/index.js";
import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import { useTalerWalletIntegrationAPI } from "../context/wallet-integration.js";
+import { useNavigationContext } from "../context/navigation.js";
const RefAmount = forwardRef(InputAmount);
@@ -57,18 +60,32 @@ function OldWithdrawalForm({
}): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreferences();
+
+ // const walletInegrationApi = useTalerWalletIntegrationAPI()
+ // const { navigateTo } = useNavigationContext();
+
const [bankState, updateBankState] = useBankState();
+ const { api } = useBankCoreApiContext();
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { api } = useBankCoreApiContext();
const [amountStr, setAmountStr] = useState<string | undefined>(
`${settings.maxWithdrawalAmount}`,
);
const [notification, notify, handleError] = useLocalNotification();
if (bankState.currentWithdrawalOperationId) {
+ // FIXME: doing the preventDefault is not optimal
+
+ // const suri = stringifyWithdrawUri({
+ // bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
+ // withdrawalOperationId: bankState.currentWithdrawalOperationId,
+ // });
+ // const uri = parseWithdrawUri(suri)!
+ const url = privatePages.operationDetails.url({
+ wopid: bankState.currentWithdrawalOperationId,
+ })
return (
<Attention type="warning" title={i18n.str`There is an operation already`}>
<span ref={focus ? doAutoFocus : undefined} />
@@ -77,9 +94,13 @@ function OldWithdrawalForm({
</i18n.Translate>{" "}
<a
class="font-semibold text-yellow-700 hover:text-yellow-600"
- href={privatePages.operationDetails.url({
- wopid: bankState.currentWithdrawalOperationId,
- })}
+ href={url}
+ // onClick={(e) => {
+ // e.preventDefault()
+ // walletInegrationApi.publishTalerAction(uri, () => {
+ // navigateTo(url)
+ // })
+ // }}
>
<i18n.Translate>this page</i18n.Translate>
</a>
@@ -324,7 +345,7 @@ export function WalletWithdrawForm({
onAuthorizationRequired={onAuthorizationRequired}
routeClose={routeCancel}
onAbort={onOperationAborted}
- // route={routeCancel}
+ // route={routeCancel}
/>
)}
</div>
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 3cf552f39..03f6556af 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -75,29 +75,39 @@ export function WithdrawalQRCode({
if (data.status === "aborted") {
return (
- <section id="main" class="content">
- <h1 class="nav">{i18n.str`Operation aborted`}</h1>
- <section>
- <p>
- <i18n.Translate>
- The wire transfer to the Taler Exchange operator's account was
- aborted, your balance was not affected.
- </i18n.Translate>
- </p>
- <p>
- <i18n.Translate>
- You can close this page now or continue to the account page.
- </i18n.Translate>
- </p>
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
+ <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Operation aborted</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler Exchange operator's account was
+ aborted from somewhere else, your balance was not affected.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-5 sm:mt-6">
<a
href={routeClose.url({})}
- class="pure-button pure-button-primary"
- style={{ float: "right" }}
+ class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<i18n.Translate>Continue</i18n.Translate>
</a>
- </section>
- </section>
+ </div>
+ </div>
);
}
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 75e0a9b84..46da5c847 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -316,7 +316,7 @@ export function CreateCashout({
const cashoutDisabled =
config.supported_tan_channels.length < 1 ||
!resultAccount.body.cashout_payto_uri;
- console.log("disab", cashoutDisabled);
+
const cashoutAccount = !resultAccount.body.cashout_payto_uri
? undefined
: parsePaytoUri(resultAccount.body.cashout_payto_uri);
diff --git a/packages/demobank-ui/src/route.ts b/packages/demobank-ui/src/route.ts
index 72b405791..912ba274d 100644
--- a/packages/demobank-ui/src/route.ts
+++ b/packages/demobank-ui/src/route.ts
@@ -13,7 +13,7 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useEffect, useState } from "preact/hooks";
+import { useNavigationContext } from "./context/navigation.js";
export function urlPattern<
T extends Record<string, string> = Record<string, never>,
@@ -24,29 +24,6 @@ export function urlPattern<
};
}
-// export function Router({
-// pageList,
-// onNotFound,
-// }: {
-// pageList: Array<PageEntry<unknown>>;
-// onNotFound: () => VNode;
-// }): VNode {
-// const current = useCurrentLocation<unknown>(pageList);
-// if (current !== undefined) {
-// const d = current.page.url
-// if (typeof current.page.url === "string") {
-// const p = current.page.url
-// return create(current.page.view, {});
-// }
-// const p = current.page.url
-// return create(current.page.view, current.values);
-// }
-// return onNotFound();
-// }
-// type PagesMap<T extends object> = {
-// [name in keyof T]: PageDefinition<any>;
-// };
-
export type RouteDefinition<T> = {
pattern: RegExp;
url: (p: T) => string;
@@ -72,17 +49,9 @@ export type RouteParamsType<
type Location<E, T extends RouteMap<E>, NAME extends keyof T> = {
parent: T;
name: NAME;
- // mapped values from params and url
values: RouteParamsType<T, NAME>;
};
-const STARTUP_SPA_LOCATION =
- typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
-const STARTUP_SPA_PARAMS =
- typeof window !== "undefined"
- ? new URLSearchParams(window.location.search)
- : new URLSearchParams();
-
/**
* Search path in the pageList
* get the values from the path found
@@ -91,11 +60,11 @@ const STARTUP_SPA_PARAMS =
* @param path
* @param params
*/
-function doSync<DEF, RM extends RouteMap<DEF>, ROUTES extends keyof RM>(
+function findMatch<DEF, RM extends RouteMap<DEF>, ROUTES extends keyof RM>(
pagesMap: RM,
pageList: Array<ROUTES>,
path: string,
- params: URLSearchParams,
+ params: Record<string, string>,
): Location<DEF, RM, ROUTES> | undefined {
for (let idx = 0; idx < pageList.length; idx++) {
const name = pageList[idx];
@@ -103,9 +72,10 @@ function doSync<DEF, RM extends RouteMap<DEF>, ROUTES extends keyof RM>(
if (found !== null) {
const values =
found.groups === undefined ? {} : structuredClone(found.groups);
- params.forEach((v, k) => {
- values[k] = v;
- });
+
+ Object.entries(params).forEach(([key, value]) => {
+ values[key] = value
+ })
// @ts-expect-error values is a map string which is equivalent to the RouteParamsType
return { name, parent: pagesMap, values };
@@ -114,87 +84,13 @@ function doSync<DEF, RM extends RouteMap<DEF>, ROUTES extends keyof RM>(
return undefined;
}
-const PopStateEventType = "popstate";
-
export function useCurrentLocation<
DEF,
RM extends RouteMap<DEF>,
ROUTES extends keyof RM,
>(pagesMap: RM) {
const pageList = Object.keys(pagesMap) as Array<ROUTES>;
- const [currentLocation, setCurrentLocation] = useState<
- Location<DEF, RM, ROUTES> | undefined
- >(doSync(pagesMap, pageList, STARTUP_SPA_LOCATION, STARTUP_SPA_PARAMS));
- useEffect(() => {
- window.addEventListener(PopStateEventType, () => {
- const path = window.location.hash.substring(1);
- console.log("event", path);
- const l = doSync(
- pagesMap,
- pageList,
- path,
- new URLSearchParams(window.location.search),
- );
- setCurrentLocation(l);
- });
- }, []);
- function routeTo<N extends ROUTES>(
- n: N,
- values: RouteParamsType<RM, N>,
- ): void {
- setCurrentLocation({
- parent: pagesMap,
- name: n,
- values,
- });
- }
- return [currentLocation, routeTo] as const;
-}
-
-// function doestUrlMatchToRoute(
-// url: string,
-// route: string,
-// ): undefined | Record<string, string> {
-// const paramsPattern = /(?:\?([^#]*))?$/;
+ const { path, params } = useNavigationContext()
-// const urlSeg = url.replace(paramsPattern, "").split("/");
-// const routeSeg = route.split("/");
-// let max = Math.max(urlSeg.length, routeSeg.length);
-
-// const result: Record<string, string> = {};
-// for (let i = 0; i < max; i++) {
-// if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
-// const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
-
-// const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
-// const plus = ~flags.indexOf("+");
-// const star = ~flags.indexOf("*");
-// const val = urlSeg[i] || "";
-
-// if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
-// return undefined;
-// }
-// result[param] = decodeURIComponent(val);
-// if (plus || star) {
-// result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
-// break;
-// }
-// } else if (routeSeg[i] !== urlSeg[i]) {
-// return undefined;
-// }
-// }
-
-// const params = url.match(paramsPattern);
-// if (params && params[1]) {
-// const paramList = params[1].split("&");
-// for (let i = 0; i < paramList.length; i++) {
-// const idx = paramList[i].indexOf("=");
-// const name = paramList[i].substring(0, idx);
-// const value = paramList[i].substring(idx + 1);
-// result[decodeURIComponent(name)] = decodeURIComponent(value);
-// }
-// }
-
-// return result;
-// }
-// const EMPTY: Record<string, string> = {};
+ return findMatch(pagesMap, pageList, path, params);
+}
diff --git a/packages/taler-wallet-webextension/manifest-v2.json b/packages/taler-wallet-webextension/manifest-v2.json
index 3475cd8aa..6f2096b05 100644
--- a/packages/taler-wallet-webextension/manifest-v2.json
+++ b/packages/taler-wallet-webextension/manifest-v2.json
@@ -18,7 +18,6 @@
"permissions": [
"unlimitedStorage",
"storage",
- "webRequest",
"<all_urls>",
"activeTab"
],
diff --git a/packages/taler-wallet-webextension/manifest-v3.json b/packages/taler-wallet-webextension/manifest-v3.json
index d6a303ed6..65a75824b 100644
--- a/packages/taler-wallet-webextension/manifest-v3.json
+++ b/packages/taler-wallet-webextension/manifest-v3.json
@@ -17,7 +17,6 @@
"storage",
"activeTab",
"scripting",
- "webRequest",
"declarativeContent",
"alarms"
],
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index a2b26441b..c7d297db9 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -46,20 +46,32 @@ export interface Permissions {
* Compatibility API that works on multiple browsers.
*/
export interface CrossBrowserPermissionsApi {
- containsHostPermissions(): Promise<boolean>;
- requestHostPermissions(): Promise<boolean>;
- removeHostPermissions(): Promise<boolean>;
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
- addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
- ): void;
}
-export type MessageFromBackend = WalletNotification;
+export enum ExtensionNotificationType {
+ SettingsChange = "settings-change",
+}
+
+export interface SettingsChangeNotification {
+ type: ExtensionNotificationType.SettingsChange;
+
+ currentValue: Settings;
+}
+
+export type ExtensionNotification = SettingsChangeNotification
+
+export type MessageFromBackend = {
+ type: "wallet",
+ notification: WalletNotification
+} | {
+ type: "web-extension",
+ notification: ExtensionNotification
+};
export type MessageFromFrontend<
Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
@@ -110,7 +122,7 @@ export interface Settings extends WebexWalletConfig {
}
export const defaultSettings: Settings = {
- injectTalerSupport: true,
+ injectTalerSupport: false,
autoOpen: true,
advanceMode: false,
backup: false,
@@ -207,13 +219,6 @@ export interface BackgroundPlatformAPI {
) => Promise<MessageResponse>,
): void;
- /**
- * Use by the wallet backend to activate the listener of HTTP request
- */
- registerTalerHeaderListener(): void;
-
- containsTalerHeaderListener(): boolean;
-
}
export interface ForegroundPlatformAPI {
/**
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index 18d282342..d791a560f 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -28,6 +28,7 @@ import { BackgroundOperations } from "../wxApi.js";
import {
BackgroundPlatformAPI,
CrossBrowserPermissionsApi,
+ ExtensionNotificationType,
ForegroundPlatformAPI,
MessageFromBackend,
MessageFromFrontend,
@@ -60,27 +61,31 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
useServiceWorkerAsBackgroundProcess,
keepAlive,
listenNetworkConnectionState,
- registerTalerHeaderListener,
- containsTalerHeaderListener,
};
export default api;
const logger = new Logger("chrome.ts");
-async function getSettingsFromStorage(): Promise<Settings> {
- const data = await chrome.storage.local.get("wallet-settings");
- if (!data) return defaultSettings;
- const settings = data["wallet-settings"];
- if (!settings) return defaultSettings;
+
+const WALLET_STORAGE_KEY = "wallet-settings";
+
+function jsonParseOrDefault(unparsed: any, def: any) {
+ if (!unparsed) return def
try {
- const parsed = JSON.parse(settings);
- return parsed;
+ return JSON.parse(unparsed);
} catch (e) {
- return defaultSettings;
+ return def;
}
}
+async function getSettingsFromStorage(): Promise<Settings> {
+ const data = await chrome.storage.local.get(WALLET_STORAGE_KEY);
+ if (!data) return defaultSettings;
+ const settings = data[WALLET_STORAGE_KEY];
+ return jsonParseOrDefault(settings, defaultSettings)
+}
+
function keepAlive(callback: any): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@@ -140,21 +145,8 @@ export function removeClipboardPermissions(): Promise<boolean> {
});
}
-function addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
-): void {
- chrome.permissions.onAdded.addListener((perm: Permissions) => {
- const lastError = chrome.runtime.lastError?.message;
- callback(perm, lastError);
- });
-}
-
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- containsHostPermissions,
- requestHostPermissions,
- removeHostPermissions,
- addPermissionsListener,
requestClipboardPermissions,
removeClipboardPermissions,
containsClipboardPermissions,
@@ -363,6 +355,18 @@ function registerAllIncomingConnections(): void {
logger.error("error trying to save incoming connection", e);
}
});
+ chrome.storage.onChanged.addListener((event) => {
+ if (event[WALLET_STORAGE_KEY]) {
+ sendMessageToAllChannels({
+ type: "web-extension",
+ notification: {
+ type: ExtensionNotificationType.SettingsChange,
+ currentValue: jsonParseOrDefault(event[WALLET_STORAGE_KEY], defaultSettings)
+ }
+ })
+ }
+ })
+
}
function listenToAllChannels(
@@ -723,253 +727,3 @@ function listenNetworkConnectionState(
};
}
-type HeaderListenerFunc = (
- details: chrome.webRequest.WebResponseHeadersDetails,
-) => void;
-let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
-
-// type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
-// let currentTabListener: TabListenerFunc | undefined = undefined;
-
-
-function containsTalerHeaderListener(): boolean {
- return (
- currentHeaderListener !== undefined
- // || currentTabListener !== undefined
- );
-}
-
-function headerListener(
- details: chrome.webRequest.WebResponseHeadersDetails,
-): chrome.webRequest.BlockingResponse | undefined {
- logger.trace("header listener run", details.statusCode, chrome.runtime.lastError)
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- return;
- }
-
- if (
- details.statusCode === 402 ||
- details.statusCode === 202 ||
- details.statusCode === 200
- ) {
- const values = (details.responseHeaders || [])
- .filter((h) => h.name.toLowerCase() === "taler")
- .map((h) => h.value)
- .filter((value): value is string => !!value);
-
- const talerUri = values.length > 0 ? values[0] : undefined
- if (talerUri) {
- logger.info(
- `Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}: ${talerUri}`,
- );
- parseTalerUriAndRedirect(details.tabId, talerUri);
- return;
- }
- }
- return details;
-}
-function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
- const talerUri = maybeTalerUri.startsWith("ext+")
- ? maybeTalerUri.substring(4)
- : maybeTalerUri;
- const uri = parseTalerUri(talerUri);
- if (!uri) {
- logger.warn(
- `Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
- );
- return;
- }
- redirectTabToWalletPage(
- tabId,
- `/taler-uri/${encodeURIComponent(talerUri)}`,
- );
-}
-
-/**
- * Not needed anymore since SPA use taler support
- */
-
-// async function tabListener(
-// tabId: number,
-// info: chrome.tabs.TabChangeInfo,
-// ): Promise<void> {
-// if (tabId < 0) return;
-// const tabLocationHasBeenUpdated = info.status === "complete";
-// const tabTitleHasBeenUpdated = info.title !== undefined;
-// if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
-// const uri = await findTalerUriInTab(tabId);
-// if (!uri) return;
-// logger.info(`Found a Taler URI in the tab ${tabId}`);
-// parseTalerUriAndRedirect(tabId, uri);
-// }
-// }
-
-/**
- * unused, declarative redirect is not good enough
- *
- */
-// async function registerDeclarativeRedirect() {
-// await chrome.declarativeNetRequest.updateDynamicRules({
-// removeRuleIds: [1],
-// addRules: [
-// {
-// id: 1,
-// priority: 1,
-// condition: {
-// urlFilter: "https://developer.chrome.com/docs/extensions/mv2/",
-// regexFilter: ".*taler_uri=([^&]*).*",
-// // isUrlFilterCaseSensitive: false,
-// // requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET]
-// // resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
-// },
-// action: {
-// type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
-// redirect: {
-// regexSubstitution: `chrome-extension://${chrome.runtime.id}/static/wallet.html?action=\\1`,
-// },
-// },
-// },
-// ],
-// });
-// }
-
-function registerTalerHeaderListener(): void {
- logger.info("setting up header listener");
-
- const prevHeaderListener = currentHeaderListener;
- // const prevTabListener = currentTabListener;
-
- if (
- prevHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
- ) {
- return;
- // console.log("removming on header listener")
- // chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
- // chrome.webRequest.onCompleted.removeListener(prevHeaderListener);
- // chrome.webRequest.onResponseStarted.removeListener(prevHeaderListener);
- // chrome.webRequest.onErrorOccurred.removeListener(prevHeaderListener);
- }
-
- // if (
- // prevTabListener &&
- // chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
- // ) {
- // console.log("removming on tab listener")
- // chrome.tabs.onUpdated.removeListener(prevTabListener);
- // }
-
- console.log("headers on, disabled:", chrome?.webRequest?.onHeadersReceived === undefined)
- if (chrome?.webRequest) {
- if (extensionIsManifestV3()) {
- chrome.webRequest.onHeadersReceived.addListener(headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders"]
- );
- } else {
- chrome.webRequest.onHeadersReceived.addListener(headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders"]
- );
- }
- // chrome.webRequest.onCompleted.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["responseHeaders", "extraHeaders"]
- // );
- // chrome.webRequest.onResponseStarted.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["responseHeaders", "extraHeaders"]
- // );
- // chrome.webRequest.onErrorOccurred.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["extraHeaders"]
- // );
- currentHeaderListener = headerListener;
- }
-
- // const tabsEvent: chrome.tabs.TabUpdatedEvent | undefined =
- // chrome?.tabs?.onUpdated;
- // if (tabsEvent) {
- // tabsEvent.addListener(tabListener);
- // currentTabListener = tabListener;
- // }
-
- //notify the browser about this change, this operation is expensive
- chrome?.webRequest?.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
-}
-
-const hostPermissions = {
- permissions: ["webRequest"],
- origins: ["http://*/*", "https://*/*"],
-};
-
-export function containsHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.contains(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export async function requestHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.request(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export async function removeHostPermissions(): Promise<boolean> {
- //if there is a handler already, remove it
- if (
- currentHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
- }
- // if (
- // currentTabListener &&
- // chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
- // ) {
- // chrome.tabs.onUpdated.removeListener(currentTabListener);
- // }
-
- currentHeaderListener = undefined;
- // currentTabListener = undefined;
-
- //notify the browser about this change, this operation is expensive
- if ("webRequest" in chrome) {
- chrome.webRequest.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
- }
-
- if (extensionIsManifestV3()) {
- // Trying to remove host permissions with manifest >= v3 throws an error
- return true;
- }
- return new Promise((res, rej) => {
- chrome.permissions.remove(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index 51744e318..2993c88bc 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -37,18 +37,11 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
listenNetworkConnectionState,
openNewURLFromPopup: () => undefined,
getPermissionsApi: () => ({
- addPermissionsListener: () => undefined,
- containsHostPermissions: async () => true,
- removeHostPermissions: async () => false,
- requestHostPermissions: async () => false,
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
requestClipboardPermissions: async () => false,
}),
- // registerDeclarativeRedirect: () => false,
- registerTalerHeaderListener: () => false,
- containsTalerHeaderListener: () => false,
getWalletWebExVersion: () => ({
version: "none",
}),
diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts
index 0bbe805cf..3d67423fd 100644
--- a/packages/taler-wallet-webextension/src/platform/firefox.ts
+++ b/packages/taler-wallet-webextension/src/platform/firefox.ts
@@ -26,9 +26,6 @@ import chromePlatform, {
containsClipboardPermissions as chromeClipContains,
removeClipboardPermissions as chromeClipRemove,
requestClipboardPermissions as chromeClipRequest,
- containsHostPermissions as chromeHostContains,
- requestHostPermissions as chromeHostRequest,
- removeHostPermissions as chromeHostRemove,
} from "./chrome.js";
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
@@ -47,16 +44,8 @@ function isFirefox(): boolean {
return true;
}
-function addPermissionsListener(callback: (p: Permissions) => void): void {
- // throw Error("addPermissionListener is not supported for Firefox");
-}
-
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- addPermissionsListener,
- containsHostPermissions: chromeHostContains,
- requestHostPermissions: chromeHostRequest,
- removeHostPermissions: chromeHostRemove,
containsClipboardPermissions: chromeClipContains,
removeClipboardPermissions: chromeClipRemove,
requestClipboardPermissions: chromeClipRequest,
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
index d1b1dc374..6cc4eb2b4 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -15,6 +15,7 @@
*/
import { CoreApiResponse, TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
+import type { MessageFromBackend } from "./platform/api.js";
/**
* This will modify all the pages that the user load when navigating with Web Extension enabled
@@ -46,6 +47,9 @@ const suffixIsNotXMLorPDF =
const rootElementIsHTML =
document.documentElement.nodeName &&
document.documentElement.nodeName.toLowerCase() === "html";
+// const pageAcceptsTalerSupport = document.head.querySelector(
+// "meta[name=taler-support]",
+// );
@@ -67,6 +71,7 @@ function convertURIToWebExtensionPath(uri: string) {
const shouldNotInject =
!documentDocTypeIsHTML ||
!suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport ||
!rootElementIsHTML;
const logger = {
@@ -93,16 +98,22 @@ function redirectToTalerActionHandler(element: HTMLMetaElement) {
return;
}
- location.href = convertURIToWebExtensionPath(uri)
+ const walletPage = convertURIToWebExtensionPath(uri)
+ window.location.replace(walletPage)
}
-function injectTalerSupportScript(head: HTMLHeadElement) {
+function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
const meta = head.querySelector("meta[name=taler-support]")
+ if (!meta) return;
+ const content = meta.getAttribute("content");
+ if (!content) return;
+ const features = content.split(",")
- const debugEnabled = meta?.getAttribute("debug") === "true";
+ const debugEnabled = meta.getAttribute("debug") === "true";
+ const hijackEnabled = features.indexOf("uri") !== -1
+ const talerApiEnabled = features.indexOf("api") !== -1 && trusted
const scriptTag = document.createElement("script");
-
scriptTag.setAttribute("async", "false");
const url = new URL(
chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
@@ -111,6 +122,12 @@ function injectTalerSupportScript(head: HTMLHeadElement) {
if (debugEnabled) {
url.searchParams.set("debug", "true");
}
+ if (talerApiEnabled) {
+ url.searchParams.set("api", "true");
+ }
+ if (hijackEnabled) {
+ url.searchParams.set("hijack", "true");
+ }
scriptTag.src = url.href;
try {
@@ -123,12 +140,14 @@ function injectTalerSupportScript(head: HTMLHeadElement) {
export interface ExtensionOperations {
- isInjectionEnabled: {
+ isAutoOpenEnabled: {
request: void;
response: boolean;
};
- isAutoOpenEnabled: {
- request: void;
+ isDomainTrusted: {
+ request: {
+ domain: string;
+ };
response: boolean;
};
}
@@ -200,48 +219,82 @@ async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
});
}
+let notificationPort: chrome.runtime.Port | undefined;
+function listenToWalletBackground(listener: (m: any) => void): () => void {
+ if (notificationPort === undefined) {
+ notificationPort = chrome.runtime.connect({ name: "notifications" });
+ }
+ notificationPort.onMessage.addListener(listener);
+ function removeListener(): void {
+ if (notificationPort !== undefined) {
+ notificationPort.onMessage.removeListener(listener);
+ }
+ }
+ return removeListener;
+}
+
+const loaderSettings = {
+ isAutoOpenEnabled: false,
+}
+
function start(
- onTalerMetaTagFound: (listener:(el: HTMLMetaElement)=>void) => void,
- onHeadReady: (listener:(el: HTMLHeadElement)=>void) => void
+ onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => void) => void,
+ onHeadReady: (listener: (el: HTMLHeadElement) => void) => void
) {
- // do not run everywhere, this is just expected to run on html
- // sites
+ // do not run everywhere, this is just expected to run on site
+ // that are aware of taler
if (shouldNotInject) return;
- const isAutoOpenEnabled_promise = callBackground("isAutoOpenEnabled", undefined)
- const isInjectionEnabled_promise = callBackground("isInjectionEnabled", undefined)
+ callBackground("isAutoOpenEnabled", undefined).then(result => {
+ loaderSettings.isAutoOpenEnabled = result
+ })
+ const isDomainTrusted_promise = callBackground("isDomainTrusted", {
+ domain: window.location.origin
+ })
- onTalerMetaTagFound(async (el)=> {
- const enabled = await isAutoOpenEnabled_promise;
- if (!enabled) return;
+ onTalerMetaTagFound(async (el) => {
+ if (!loaderSettings.isAutoOpenEnabled) return;
redirectToTalerActionHandler(el)
})
onHeadReady(async (el) => {
- const enabled = await isInjectionEnabled_promise;
- if (!enabled) return;
- injectTalerSupportScript(el)
+ const trusted = await isDomainTrusted_promise
+ injectTalerSupportScript(el, trusted)
+ })
+
+ listenToWalletBackground((e: MessageFromBackend) => {
+ if (e.type === "web-extension" && e.notification.type === "settings-change") {
+ const settings = e.notification.currentValue
+ loaderSettings.isAutoOpenEnabled = settings.autoOpen
+ }
+ console.log("loader ->", e)
})
}
+function isCorrectMetaElement(el: HTMLMetaElement): boolean {
+ const name = el.getAttribute("name")
+ if (!name) return false;
+ if (name !== "taler-uri") return false;
+ const uri = el.getAttribute("content");
+ if (!uri) return false;
+ return true
+}
+
/**
* Tries to find taler meta tag ASAP and report
* @param notify
* @returns
*/
-function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
+function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
if (document.head) {
const element = document.head.querySelector("meta[name=taler-uri]")
if (!element) return;
if (!(element instanceof HTMLMetaElement)) return;
- const name = element.getAttribute("name")
- if (!name) return;
- if (name !== "taler-uri") return;
- const uri = element.getAttribute("content");
- if (!uri) return;
- notify(element)
+ if (isCorrectMetaElement(element)) {
+ notify(element)
+ }
return;
}
const obs = new MutationObserver(async function (mutations) {
@@ -250,13 +303,10 @@ function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
if (mut.type === "childList") {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLMetaElement) {
- const name = added.getAttribute("name")
- if (!name) return;
- if (name !== "taler-uri") return;
- const uri = added.getAttribute("content");
- if (!uri) return;
- notify(added)
- obs.disconnect()
+ if (isCorrectMetaElement(added)) {
+ notify(added)
+ obs.disconnect()
+ }
}
});
}
@@ -279,7 +329,7 @@ function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
* @param notify
* @returns
*/
-function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
+function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
if (document.head) {
notify(document.head)
return;
@@ -290,7 +340,6 @@ function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
if (mut.type === "childList") {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLHeadElement) {
-
notify(added)
obs.disconnect()
}
@@ -309,4 +358,4 @@ function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
})
}
-start(onTalerMetaTag, onHeaderReady);
+start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
index 993c12703..8b15380f9 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -47,7 +47,7 @@
const shouldNotRun =
!documentDocTypeIsHTML ||
!suffixIsNotXMLorPDF ||
- // !pageAcceptsTalerSupport || FIXME: removing this before release for testing
+ !pageAcceptsTalerSupport ||
!rootElementIsHTML;
interface Info {
@@ -154,32 +154,38 @@
function start() {
if (shouldNotRun) return;
- // FIXME: we can remove this if the script caller send information we need
if (!(document.currentScript instanceof HTMLScriptElement)) return;
const url = new URL(document.currentScript.src);
const { protocol, searchParams, hostname } = url;
const extensionId = searchParams.get("id") ?? "";
const debugEnabled = searchParams.get("debug") === "true";
- if (debugEnabled) {
- logger.debug = logger.info;
- }
+ const apiEnabled = searchParams.get("api") === "true";
+ const hijackEnabled = searchParams.get("hijack") === "true";
const info: Info = Object.freeze({
extensionId,
protocol,
hostname,
});
+
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
+
const taler: TalerSupport = {
info,
__internal: buildApi(info),
};
- //@ts-ignore
- window.taler = taler;
+ if (apiEnabled) {
+ //@ts-ignore
+ window.taler = taler;
+ }
- //default behavior: register on install
- taler.__internal.registerProtocolHandler();
+ if (hijackEnabled) {
+ taler.__internal.registerProtocolHandler();
+ }
}
// utils functions
@@ -189,6 +195,6 @@
);
}
- return start
+ start();
})()
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index faa64e07d..d12ae864b 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -40,6 +40,9 @@ import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js";
+import { Checkbox } from "../components/Checkbox.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { useAlertContext } from "../context/alert.js";
export function DeveloperPage(): VNode {
const listenAllEvents = Array.from<NotificationType>({ length: 1 });
@@ -132,6 +135,8 @@ export function View({ operations, coins, onDownloadDatabase }: Props): VNode {
dump: JSON.parse(str),
});
}
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
@@ -256,26 +261,6 @@ export function View({ operations, coins, onDownloadDatabase }: Props): VNode {
<Button
variant="contained"
onClick={async () => {
- api.background.call("toggleHeaderListener", true);
- }}
- >
- <i18n.Translate>enable header listener</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
- api.background.call("toggleHeaderListener", false);
- }}
- >
- <i18n.Translate>disable header listener</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
navigator.registerProtocolHandler(
"taler",
`${window.location.origin}/static/wallet.html#/cta/withdraw?talerWithdrawUri=%s`,
@@ -360,6 +345,22 @@ export function View({ operations, coins, onDownloadDatabase }: Props): VNode {
</Button>
</Grid>{" "}
</Grid>
+ <Checkbox
+ label={i18n.str`Inject Taler support in all pages`}
+ name="inject"
+ description={
+ <i18n.Translate>
+ Enabling this option will make `window.taler` be available
+ in all sites
+ </i18n.Translate>
+ }
+ enabled={settings.injectTalerSupport!}
+ onToggle={safely("update support injection", async () => {
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport);
+ })}
+ />
+
+
<Paper style={{ padding: 10, margin: 10 }}>
<h3>Logging</h3>
<div>
@@ -396,92 +397,98 @@ export function View({ operations, coins, onDownloadDatabase }: Props): VNode {
Set log level
</Button>
</Paper>
- {downloadedDatabase && (
- <div>
- <i18n.Translate>
- Database exported at{" "}
- <Time
- timestamp={AbsoluteTime.fromMilliseconds(
- downloadedDatabase.time.getTime(),
- )}
- format="yyyy/MM/dd HH:mm:ss"
- />{" "}
- <a
- href={`data:text/plain;charset=utf-8;base64,${toBase64(
- downloadedDatabase.content,
- )}`}
- download={`taler-wallet-database-${format(
- downloadedDatabase.time,
- "yyyy/MM/dd_HH:mm",
- )}.json`}
- >
- <i18n.Translate>click here</i18n.Translate>
- </a>{" "}
- to download
- </i18n.Translate>
- </div>
- )}
+ {
+ downloadedDatabase && (
+ <div>
+ <i18n.Translate>
+ Database exported at{" "}
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ downloadedDatabase.time.getTime(),
+ )}
+ format="yyyy/MM/dd HH:mm:ss"
+ />{" "}
+ <a
+ href={`data:text/plain;charset=utf-8;base64,${toBase64(
+ downloadedDatabase.content,
+ )}`}
+ download={`taler-wallet-database-${format(
+ downloadedDatabase.time,
+ "yyyy/MM/dd_HH:mm",
+ )}.json`}
+ >
+ <i18n.Translate>click here</i18n.Translate>
+ </a>{" "}
+ to download
+ </i18n.Translate>
+ </div>
+ )
+ }
<br />
<p>
<i18n.Translate>Coins</i18n.Translate>:
</p>
- {Object.keys(money_by_exchange).map((ex, idx) => {
- const allcoins = money_by_exchange[ex];
- allcoins.sort((a, b) => {
- if (b.denom_value !== a.denom_value) {
- return b.denom_value - a.denom_value;
- }
- return b.denom_fraction - a.denom_fraction;
- });
+ {
+ Object.keys(money_by_exchange).map((ex, idx) => {
+ const allcoins = money_by_exchange[ex];
+ allcoins.sort((a, b) => {
+ if (b.denom_value !== a.denom_value) {
+ return b.denom_value - a.denom_value;
+ }
+ return b.denom_fraction - a.denom_fraction;
+ });
- const coins = allcoins.reduce(
- (prev, cur) => {
- if (cur.status === CoinStatus.Fresh) prev.usable.push(cur);
- if (cur.status === CoinStatus.Dormant) prev.spent.push(cur);
- return prev;
- },
- {
- spent: [],
- usable: [],
- } as SplitedCoinInfo,
- );
+ const coins = allcoins.reduce(
+ (prev, cur) => {
+ if (cur.status === CoinStatus.Fresh) prev.usable.push(cur);
+ if (cur.status === CoinStatus.Dormant) prev.spent.push(cur);
+ return prev;
+ },
+ {
+ spent: [],
+ usable: [],
+ } as SplitedCoinInfo,
+ );
- return (
- <ShowAllCoins
- key={idx}
- coins={coins}
- ex={ex}
- currencies={currencies}
- />
- );
- })}
+ return (
+ <ShowAllCoins
+ key={idx}
+ coins={coins}
+ ex={ex}
+ currencies={currencies}
+ />
+ );
+ })
+ }
<br />
- {operations && operations.length > 0 && (
- <Fragment>
- <p>
- <i18n.Translate>Pending operations</i18n.Translate>
- </p>
- <dl>
- {operations.reverse().map((o) => {
- return (
- <NotifyUpdateFadeOut key={hashObjectId(o)}>
- <dt>
- {o.type}{" "}
- <Time
- timestamp={o.timestampDue}
- format="yy/MM/dd HH:mm:ss"
- />
- </dt>
- <dd>
- <pre>{JSON.stringify(o, undefined, 2)}</pre>
- </dd>
- </NotifyUpdateFadeOut>
- );
- })}
- </dl>
- </Fragment>
- )}
- </div>
+ {
+ operations && operations.length > 0 && (
+ <Fragment>
+ <p>
+ <i18n.Translate>Pending operations</i18n.Translate>
+ </p>
+ <dl>
+ {operations.reverse().map((o) => {
+ return (
+ <NotifyUpdateFadeOut key={hashObjectId(o)}>
+ <dt>
+ {o.type}{" "}
+ <Time
+ timestamp={o.timestampDue}
+ format="yy/MM/dd HH:mm:ss"
+ />
+ </dt>
+ <dd>
+ <pre>{JSON.stringify(o, undefined, 2)}</pre>
+ </dd>
+ </NotifyUpdateFadeOut>
+ );
+ })}
+ </dl>
+ </Fragment>
+ )
+ }
+ </div >
);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
index 86c420b91..a5d6972de 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -55,7 +55,6 @@ export const AllOff = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
@@ -65,7 +64,6 @@ export const OneChecked = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
@@ -75,7 +73,6 @@ export const WithOneExchange = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
knownExchanges: [
@@ -100,7 +97,6 @@ export const WithExchangeInDifferentState = tests.createExample(
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
knownExchanges: [
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index b27413a96..e25629148 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -80,14 +80,6 @@ export function SettingsPage(): VNode {
}),
},
}}
- injectTalerToggle={{
- value: settings.injectTalerSupport,
- button: {
- onClick: safely("update support injection", async () => {
- updateSettings("injectTalerSupport", !settings.injectTalerSupport);
- }),
- },
- }}
advanceToggle={{
value: settings.advanceMode,
button: {
@@ -117,7 +109,6 @@ export interface ViewProps {
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
autoOpenToggle: ToggleHandler;
- injectTalerToggle: ToggleHandler;
advanceToggle: ToggleHandler;
langToggle: ToggleHandler;
knownExchanges: Array<ExchangeListItem>;
@@ -131,7 +122,6 @@ export interface ViewProps {
export function SettingsView({
knownExchanges,
autoOpenToggle,
- injectTalerToggle,
advanceToggle,
langToggle,
coreVersion,
@@ -276,19 +266,6 @@ export function SettingsView({
<i18n.Translate>Navigator</i18n.Translate>
</SubTitle>
<Checkbox
- label={i18n.str`Inject Taler support in all pages`}
- name="inject"
- description={
- <i18n.Translate>
- Disabling this option will make some web application not able to
- trigger the wallet when clicking links but you will be able to
- open the wallet using the keyboard shortcut
- </i18n.Translate>
- }
- enabled={injectTalerToggle.value!}
- onToggle={injectTalerToggle.button.onClick!}
- />
- <Checkbox
label={i18n.str`Automatically open wallet`}
name="autoOpen"
description={
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 8fb8211ae..d989c9662 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -83,14 +83,6 @@ export interface BackgroundOperations {
};
response: void;
};
- containsHeaderListener: {
- request: void;
- response: ExtendedPermissionsResponse;
- };
- toggleHeaderListener: {
- request: boolean;
- response: ExtendedPermissionsResponse;
- };
}
export interface BackgroundApiClient {
@@ -194,7 +186,7 @@ function onUpdateNotification(
return;
};
const onNewMessage = (message: MessageFromBackend): void => {
- const shouldNotify = messageTypes.includes(message.type);
+ const shouldNotify = message.type === "wallet" && messageTypes.includes(message.notification.type);
if (shouldNotify) {
doCallback();
}
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 1ecd66f05..95d31c519 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -122,18 +122,18 @@ async function sum(ns: Array<number>): Promise<number> {
}
const extensionHandlers: ExtensionHandlerType = {
- isInjectionEnabled,
isAutoOpenEnabled,
+ isDomainTrusted,
};
-async function isInjectionEnabled(): Promise<boolean> {
+async function isAutoOpenEnabled(): Promise<boolean> {
const settings = await platform.getSettingsFromStorage();
- return settings.injectTalerSupport === true;
+ return settings.autoOpen === true;
}
-async function isAutoOpenEnabled(): Promise<boolean> {
+async function isDomainTrusted(): Promise<boolean> {
const settings = await platform.getSettingsFromStorage();
- return settings.autoOpen === true;
+ return settings.injectTalerSupport === true;
}
const backendHandlers: BackendHandlerType = {
@@ -142,14 +142,8 @@ const backendHandlers: BackendHandlerType = {
resetDb,
runGarbageCollector,
setLoggingLevel,
- containsHeaderListener,
- toggleHeaderListener,
};
-async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
- const result = platform.containsTalerHeaderListener();
- return { newValue: result };
-}
async function setLoggingLevel({
tag,
@@ -309,8 +303,10 @@ async function reinitWallet(): Promise<void> {
return;
}
wallet.addNotificationListener((message) => {
- logger.info("wallet -> ui", message);
- platform.sendMessageToAllChannels(message);
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message
+ });
});
platform.keepAlive(() => {
@@ -360,65 +356,6 @@ export async function wxMain(): Promise<void> {
console.error(e);
}
- // platform.registerDeclarativeRedirect();
- // if (false) {
- /**
- * this is not working reliable on chrome, just
- * intercepts queries after the user clicks the popups
- * which doesn't make sense, keeping it to make more tests
- */
-
- logger.trace("check taler header listener");
- const enabled = platform.containsTalerHeaderListener()
- if (!enabled) {
- logger.info("header listener on")
- const perm = await platform.getPermissionsApi().containsHostPermissions()
- if (perm) {
- logger.info("header listener allowed")
- try {
- platform.registerTalerHeaderListener();
- } catch (e) {
- logger.error("could not register header listener", e);
- }
- } else {
- logger.info("header listener requested")
- await platform.getPermissionsApi().requestHostPermissions()
- }
- }
-
- // On platforms that support it, also listen to external
- // modification of permissions.
- platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
- logger.info(`permission added: ${perm}`,)
- if (lastError) {
- logger.error(
- `there was a problem trying to get permission ${perm}`,
- lastError,
- );
- return;
- }
- platform.registerTalerHeaderListener();
- });
-
- // }
}
-async function toggleHeaderListener(
- newVal: boolean,
-): Promise<ExtendedPermissionsResponse> {
- logger.trace("new extended permissions value", newVal);
- if (newVal) {
- try {
- platform.registerTalerHeaderListener();
- return { newValue: true };
- } catch (e) {
- logger.error("FAIL to toggle", e)
- }
- return { newValue: false }
- }
-
- const rem = await platform.getPermissionsApi().removeHostPermissions();
- logger.trace("permissions removed:", rem);
- return { newValue: false };
-}