summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src')
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx45
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.stories.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts1
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/index.ts17
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx25
-rw-r--r--packages/taler-wallet-webextension/src/popup/Application.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/index.ts61
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/state.ts48
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx58
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx220
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/wallet/index.stories.tsx2
20 files changed, 592 insertions, 41 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index ff2404800..b900fab9d 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -24,7 +24,7 @@
/**
* Imports.
*/
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import {
NavigationHeader,
NavigationHeaderHolder,
@@ -33,6 +33,11 @@ import {
import { useTranslationContext } from "./context/translation.js";
import settingsIcon from "./svg/settings_black_24dp.svg";
import qrIcon from "./svg/qr_code_24px.svg";
+import warningIcon from "./svg/warning_24px.svg";
+import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
+import { wxApi } from "./wxApi.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { JustInDevMode } from "./components/JustInDevMode.js";
/**
* List of pages used by the wallet
@@ -102,6 +107,7 @@ export const Pages = {
backupProviderAdd: "/backup/provider/add",
qr: "/qr",
+ notifications: "/notifications",
settings: "/settings",
settingsExchangeAdd: pageDefinition<{ currency?: string }>(
"/settings/exchange/add/:currency?",
@@ -127,7 +133,21 @@ export const Pages = {
),
};
-export function PopupNavBar({ path = "" }: { path?: string }): VNode {
+export function PopupNavBar({
+ path = "",
+}: {
+ path?: string;
+}): // api: typeof wxApi,
+VNode {
+ const api = wxApi; //FIXME: as parameter
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionUnreadCount,
+ {},
+ );
+ });
+ const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+
const { i18n } = useTranslationContext();
return (
<NavigationHeader>
@@ -141,6 +161,17 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
<i18n.Translate>Backup</i18n.Translate>
</a>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
+ {attentionCount > 0 ? (
+ <a href={Pages.notifications}>
+ <SvgIcon
+ title={i18n.str`Notifications`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="yellow"
+ />
+ </a>
+ ) : (
+ <Fragment />
+ )}
<a href={Pages.qr}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
@@ -178,10 +209,16 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
<i18n.Translate>Backup</i18n.Translate>
</a>
- <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
- <i18n.Translate>Dev</i18n.Translate>
+ <a href={Pages.notifications}>
+ <i18n.Translate>Notifications</i18n.Translate>
</a>
+ <JustInDevMode>
+ <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
+ <i18n.Translate>Dev</i18n.Translate>
+ </a>
+ </JustInDevMode>
+
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
>
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
index 3183364a8..ff9a71992 100644
--- a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
@@ -50,7 +50,6 @@ function RenderAmount(): VNode {
<AmountField
required
label={<i18n.Translate>Amount</i18n.Translate>}
- currency="USD"
highestDenom={2000000}
lowestDenom={0.01}
handler={handler}
diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
index f8b23081d..c2c4b52e3 100644
--- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
@@ -27,6 +27,7 @@ import { h, VNode } from "preact";
import { useTranslationContext } from "../context/translation.js";
import { Avatar } from "../mui/Avatar.js";
import { Pages } from "../NavigationBar.js";
+import { assertUnreachable } from "../utils/index.js";
import {
Column,
ExtraLargeText,
@@ -175,8 +176,7 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
/>
);
default: {
- const pe: never = tx;
- throw Error(`unsupported transaction type ${pe}`);
+ assertUnreachable(tx);
}
}
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index 1846794fc..c7fb48958 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -89,6 +89,7 @@ export function useComponentState(
const insufficientBalance: PreparePayResult = {
status: PreparePayResultType.InsufficientBalance,
+ talerUri: "taler://pay",
proposalId: "fakeID",
contractTerms: {} as any,
amountRaw: hook.response.p2p.amount,
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
index f0270b96c..80822b381 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
@@ -18,6 +18,7 @@ import {
AmountJson,
PreparePayResult,
PreparePayResultAlreadyConfirmed,
+ PreparePayResultInsufficientBalance,
PreparePayResultPaymentPossible,
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
@@ -26,7 +27,7 @@ import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js";
import { useComponentState } from "./state.js";
-import { BaseView, LoadingUriView } from "./views.js";
+import { BaseView, LoadingUriView, LostView } from "./views.js";
export interface Props {
talerPayUri?: string;
@@ -40,6 +41,7 @@ export type State =
| State.LoadingUriError
| State.Ready
| State.NoEnoughBalance
+ | State.Lost
| State.NoBalanceForCurrency
| State.Confirmed;
@@ -62,12 +64,15 @@ export namespace State {
}
export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency";
- payStatus: PreparePayResult;
+ payStatus:
+ | PreparePayResultInsufficientBalance
+ | PreparePayResultPaymentPossible
+ | PreparePayResultAlreadyConfirmed;
balance: undefined;
}
export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance";
- payStatus: PreparePayResult;
+ payStatus: PreparePayResultInsufficientBalance;
balance: AmountJson;
}
export interface Ready extends BaseInfo {
@@ -77,6 +82,11 @@ export namespace State {
balance: AmountJson;
}
+ export interface Lost {
+ status: "lost";
+ error: undefined;
+ }
+
export interface Confirmed extends BaseInfo {
status: "confirmed";
payStatus: PreparePayResultAlreadyConfirmed;
@@ -89,6 +99,7 @@ const viewMapping: StateViewMap<State> = {
"loading-uri": LoadingUriView,
"no-balance-for-currency": BaseView,
"no-enough-balance": BaseView,
+ lost: LostView,
confirmed: BaseView,
ready: BaseView,
};
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index 49d022320..b90b1e495 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -82,6 +82,14 @@ export function useComponentState(
};
}
const { payStatus } = hook.response;
+
+ if (payStatus.status === PreparePayResultType.Lost) {
+ return {
+ status: "lost",
+ error: undefined,
+ };
+ }
+
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
const foundBalance = hook.response.balance.balances.find(
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index 7d5a7694e..fd437d5d2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -44,6 +44,7 @@ export const NoBalance = createExample(BaseView, {
uri: "",
payStatus: {
status: PreparePayResultType.InsufficientBalance,
+ talerUri: "taler://pay/..",
noncePriv: "",
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
@@ -73,6 +74,7 @@ export const NoEnoughBalance = createExample(BaseView, {
uri: "",
payStatus: {
status: PreparePayResultType.InsufficientBalance,
+ talerUri: "taler://pay/..",
noncePriv: "",
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
@@ -102,6 +104,7 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
uri: "",
payStatus: {
status: PreparePayResultType.InsufficientBalance,
+ talerUri: "taler://pay/..",
noncePriv: "",
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
contractTerms: {
@@ -136,6 +139,7 @@ export const PaymentPossible = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
amountEffective: "USD:10",
amountRaw: "USD:10",
noncePriv: "",
@@ -176,6 +180,7 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
amountEffective: "USD:10.20",
amountRaw: "USD:10",
noncePriv: "",
@@ -213,6 +218,7 @@ export const TicketWithAProductList = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
amountEffective: "USD:10.20",
amountRaw: "USD:10",
noncePriv: "",
@@ -269,6 +275,7 @@ export const TicketWithShipping = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
amountEffective: "USD:10.20",
amountRaw: "USD:10",
noncePriv: "",
@@ -315,6 +322,7 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
+ talerUri: "taler://pay/..",
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: {
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index d9b6eaa02..6b502a87f 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -26,6 +26,7 @@ import {
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
@@ -43,6 +44,7 @@ import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { ButtonHandler } from "../../mui/handlers.js";
+import { assertUnreachable } from "../../utils/index.js";
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
@@ -63,8 +65,24 @@ type SupportedStates =
| State.NoBalanceForCurrency
| State.NoEnoughBalance;
+export function LostView(state: State.Lost): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <ErrorMessage
+ title={<i18n.Translate>Could not load pay status</i18n.Translate>}
+ description={
+ <i18n.Translate>
+ The proposal was lost, another should be downloaded
+ </i18n.Translate>
+ }
+ />
+ );
+}
+
export function BaseView(state: SupportedStates): VNode {
const { i18n } = useTranslationContext();
+
const contractTerms: ContractTerms = state.payStatus.contractTerms;
const price = {
@@ -399,8 +417,9 @@ export function ButtonsSection({
</Fragment>
);
}
+ if (payStatus.status === PreparePayResultType.Lost) {
+ return <Fragment />;
+ }
- const error: never = payStatus;
-
- return <Fragment />;
+ assertUnreachable(payStatus);
}
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx
index 457f26cfd..8186c6790 100644
--- a/packages/taler-wallet-webextension/src/popup/Application.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -150,6 +150,10 @@ export function Application(): VNode {
component={RedirectToWalletPage}
/>
<Route path={Pages.dev} component={RedirectToWalletPage} />
+ <Route
+ path={Pages.notifications}
+ component={RedirectToWalletPage}
+ />
<Route default component={Redirect} to={Pages.balance} />
</Router>
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
index 0b3c17902..504ee4678 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -171,7 +171,11 @@ export function useComponentState(
switch (resp.status) {
case "payment-required":
- return onPaymentRequired(resp.talerUri);
+ if (resp.talerUri) {
+ return onPaymentRequired(resp.talerUri);
+ } else {
+ return onComplete(url);
+ }
case "error":
return setOperationError(resp.error);
case "ok":
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 6b265c1ba..6362f1924 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -66,6 +66,7 @@ import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js";
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
+import { NotificationsPage } from "./Notifications/index.js";
export function Application(): VNode {
const [globalNotification, setGlobalNotification] = useState<
@@ -206,6 +207,7 @@ export function Application(): VNode {
/>
<Route path={Pages.settings} component={SettingsPage} />
+ <Route path={Pages.notifications} component={NotificationsPage} />
{/**
* BACKUP
@@ -218,6 +220,12 @@ export function Application(): VNode {
<Route
path={Pages.backupProviderDetail.pattern}
component={ProviderDetailPage}
+ onPayProvider={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onWithdraw={(amount: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
onBack={() => redirectTo(Pages.backup)}
/>
<Route
@@ -254,7 +262,7 @@ export function Application(): VNode {
path={Pages.ctaPay}
component={PaymentPage}
goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.ctaWithdrawManual({ amount }))
+ redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
@@ -321,7 +329,7 @@ export function Application(): VNode {
path={Pages.ctaInvoicePay}
component={InvoicePayPage}
goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.ctaWithdrawManual({ amount }))
+ redirectTo(Pages.receiveCash({ amount }))
}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index b12f5e5f6..2e19d3944 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -89,6 +89,7 @@ export const LotOfProviders = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
+ talerUri: "taler://",
},
terms: {
annualFee: "KUDOS:0.1",
@@ -103,6 +104,7 @@ export const LotOfProviders = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
+ amount: "KUDOS:10",
},
terms: {
annualFee: "KUDOS:0.1",
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
new file mode 100644
index 000000000..253a0e629
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { UserAttentionUnreadList } from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { wxApi } from "../../wxApi.js";
+import { useComponentState } from "./state.js";
+import { LoadingUriView, ReadyView } from "./views.js";
+
+export interface Props {}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: HookError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ list: UserAttentionUnreadList;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": LoadingUriView,
+ ready: ReadyView,
+};
+
+export const NotificationsPage = compose(
+ "NotificationsPage",
+ (p: Props) => useComponentState(p, wxApi),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
new file mode 100644
index 000000000..093722cf0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { wxApi } from "../../wxApi.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({}: Props, api: typeof wxApi): State {
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionRequests,
+ {},
+ );
+ });
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "loading-error",
+ error: hook,
+ };
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ list: hook.response.pending,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
new file mode 100644
index 000000000..e4c7105e9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, AttentionType } from "@gnu-taler/taler-util";
+import { createExample } from "../../test-utils.js";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "wallet/notifications",
+};
+
+export const Ready = createExample(ReadyView, {
+ list: [
+ {
+ when: AbsoluteTime.now(),
+ read: false,
+ info: {
+ type: AttentionType.KycWithdrawal,
+ transactionId: "123",
+ },
+ },
+ {
+ when: AbsoluteTime.now(),
+ read: false,
+ info: {
+ type: AttentionType.MerchantRefund,
+ transactionId: "123",
+ },
+ },
+ {
+ when: AbsoluteTime.now(),
+ read: false,
+ info: {
+ type: AttentionType.BackupUnpaid,
+ provider_base_url: "http://sync.taler.net",
+ talerUri: "taler://payment/asdasdasd",
+ },
+ },
+ ],
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
new file mode 100644
index 000000000..eae4d4ca2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("test description", () => {
+ it("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
new file mode 100644
index 000000000..9146d8837
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
@@ -0,0 +1,220 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 {
+ AbsoluteTime,
+ AttentionInfo,
+ AttentionType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { LoadingError } from "../../components/LoadingError.js";
+import {
+ Column,
+ DateSeparator,
+ HistoryRow,
+ LargeText,
+ SmallLightText,
+} from "../../components/styled/index.js";
+import { Time } from "../../components/Time.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { Avatar } from "../../mui/Avatar.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import { Pages } from "../../NavigationBar.js";
+import { assertUnreachable } from "../../utils/index.js";
+import { State } from "./index.js";
+
+export function LoadingUriView({ error }: State.LoadingUriError): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <LoadingError
+ title={<i18n.Translate>Could not load notifications</i18n.Translate>}
+ error={error}
+ />
+ );
+}
+
+const term = 1000 * 60 * 60 * 24;
+function normalizeToDay(x: number): number {
+ return Math.round(x / term) * term;
+}
+
+export function ReadyView({ list }: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ if (list.length < 1) {
+ return (
+ <section>
+ <i18n.Translate>No notification left</i18n.Translate>
+ </section>
+ );
+ }
+
+ const byDate = list.reduce((rv, x) => {
+ const theDate = x.when.t_ms === "never" ? 0 : normalizeToDay(x.when.t_ms);
+ if (theDate) {
+ (rv[theDate] = rv[theDate] || []).push(x);
+ }
+
+ return rv;
+ }, {} as { [x: string]: typeof list });
+ const datesWithNotifications = Object.keys(byDate);
+
+ return (
+ <section>
+ {datesWithNotifications.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={{ t_ms: Number.parseInt(d, 10) }}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {byDate[d].map((n, i) => (
+ <NotificationItem
+ key={i}
+ info={n.info}
+ isRead={n.read}
+ timestamp={n.when}
+ />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ );
+}
+
+function NotificationItem({
+ info,
+ isRead,
+ timestamp,
+}: {
+ info: AttentionInfo;
+ timestamp: AbsoluteTime;
+ isRead: boolean;
+}): VNode {
+ switch (info.type) {
+ case AttentionType.KycWithdrawal:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={Pages.balanceTransaction({ tid: info.transactionId })}
+ title="Withdrawal on hold"
+ subtitle="Know-your-customer validation is required"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.MerchantRefund:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={Pages.balanceTransaction({ tid: info.transactionId })}
+ title="Merchant has refund your payment"
+ subtitle="Accept or deny refund"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.BackupUnpaid:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
+ title="Backup provider is unpaid"
+ subtitle="Complete the payment or remove the service provider"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.AuditorDenominationsExpires:
+ return <div>not implemented</div>;
+ case AttentionType.AuditorKeyExpires:
+ return <div>not implemented</div>;
+ case AttentionType.AuditorTosChanged:
+ return <div>not implemented</div>;
+ case AttentionType.ExchangeDenominationsExpired:
+ return <div>not implemented</div>;
+ // case AttentionType.ExchangeDenominationsExpiresSoon:
+ // return <div>not implemented</div>;
+ case AttentionType.ExchangeKeyExpired:
+ return <div>not implemented</div>;
+ // case AttentionType.ExchangeKeyExpiresSoon:
+ // return <div>not implemented</div>;
+ case AttentionType.ExchangeTosChanged:
+ return <div>not implemented</div>;
+ case AttentionType.BackupExpiresSoon:
+ return <div>not implemented</div>;
+ case AttentionType.PushPaymentReceived:
+ return <div>not implemented</div>;
+ case AttentionType.PullPaymentPaid:
+ return <div>not implemented</div>;
+ default:
+ assertUnreachable(info);
+ }
+}
+
+function NotificationLayout(props: {
+ title: string;
+ href: string;
+ subtitle?: string;
+ timestamp: AbsoluteTime;
+ iconPath: string;
+ isRead: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <HistoryRow
+ href={props.href}
+ style={{
+ backgroundColor: props.isRead ? "lightcyan" : "inherit",
+ alignItems: "center",
+ }}
+ >
+ <Avatar
+ style={{
+ border: "solid gray 1px",
+ color: "gray",
+ boxSizing: "border-box",
+ }}
+ >
+ {props.iconPath}
+ </Avatar>
+ <Column>
+ <LargeText>
+ <div>{props.title}</div>
+ {props.subtitle && (
+ <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
+ {props.subtitle}
+ </div>
+ )}
+ </LargeText>
+ <SmallLightText style={{ marginTop: 5 }}>
+ <Time timestamp={props.timestamp} format="HH:mm" />
+ </SmallLightText>
+ </Column>
+ <Column>
+ <Grid>
+ <Button variant="outlined">
+ <i18n.Translate>Ignore</i18n.Translate>
+ </Button>
+ </Grid>
+ </Column>
+ </HistoryRow>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index d55a25e78..854c14ac1 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -174,6 +174,7 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
+ amount: "EUR:123",
},
terms: {
annualFee: "EUR:0.1",
@@ -191,6 +192,7 @@ export const InactivePending = createExample(TestedComponent, {
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
+ talerUri: "taler://pay/sad",
},
terms: {
annualFee: "EUR:0.1",
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index d9dd1d746..6dde30b39 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -36,9 +36,16 @@ import { wxApi } from "../wxApi.js";
interface Props {
pid: string;
onBack: () => Promise<void>;
+ onPayProvider: (uri: string) => Promise<void>;
+ onWithdraw: (amount: string) => Promise<void>;
}
-export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
+export function ProviderDetailPage({
+ pid: providerURL,
+ onBack,
+ onPayProvider,
+ onWithdraw,
+}: Props): VNode {
const { i18n } = useTranslationContext();
async function getProviderInfo(): Promise<ProviderInfo | null> {
//create a first list of backup info by currency
@@ -71,11 +78,30 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
/>
);
}
+ const info = state.response;
+ if (info === null) {
+ return (
+ <Fragment>
+ <section>
+ <p>
+ <i18n.Translate>
+ There is not known provider with url &quot;{providerURL}&quot;.
+ </i18n.Translate>
+ </p>
+ </section>
+ <footer>
+ <Button variant="contained" color="secondary" onClick={onBack}>
+ <i18n.Translate>See providers</i18n.Translate>
+ </Button>
+ <div />
+ </footer>
+ </Fragment>
+ );
+ }
return (
<ProviderView
- url={providerURL}
- info={state.response}
+ info={info}
onSync={async () =>
wxApi.wallet
.call(WalletApiOperation.RunBackupCycle, {
@@ -83,6 +109,16 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
})
.then()
}
+ onPayProvider={async () => {
+ if (info.paymentStatus.type !== ProviderPaymentType.Pending) return;
+ if (!info.paymentStatus.talerUri) return;
+ onPayProvider(info.paymentStatus.talerUri);
+ }}
+ onWithdraw={async () => {
+ if (info.paymentStatus.type !== ProviderPaymentType.InsufficientBalance)
+ return;
+ onWithdraw(info.paymentStatus.amount);
+ }}
onDelete={() =>
wxApi.wallet
.call(WalletApiOperation.RemoveBackupProvider, {
@@ -99,42 +135,25 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
}
export interface ViewProps {
- url: string;
- info: ProviderInfo | null;
+ info: ProviderInfo;
onDelete: () => Promise<void>;
onSync: () => Promise<void>;
onBack: () => Promise<void>;
onExtend: () => Promise<void>;
+ onPayProvider: () => Promise<void>;
+ onWithdraw: () => Promise<void>;
}
export function ProviderView({
info,
- url,
onDelete,
+ onPayProvider,
+ onWithdraw,
onSync,
onBack,
onExtend,
}: ViewProps): VNode {
const { i18n } = useTranslationContext();
- if (info === null) {
- return (
- <Fragment>
- <section>
- <p>
- <i18n.Translate>
- There is not known provider with url &quot;{url}&quot;.
- </i18n.Translate>
- </p>
- </section>
- <footer>
- <Button variant="contained" color="secondary" onClick={onBack}>
- <i18n.Translate>See providers</i18n.Translate>
- </Button>
- <div />
- </footer>
- </Fragment>
- );
- }
const lb = info.lastSuccessfulBackupTimestamp
? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
: undefined;
@@ -230,6 +249,18 @@ export function ProviderView({
<Button variant="contained" color="error" onClick={onDelete}>
<i18n.Translate>Remove provider</i18n.Translate>
</Button>
+ {info.paymentStatus.type === ProviderPaymentType.Pending &&
+ info.paymentStatus.talerUri ? (
+ <Button variant="contained" color="primary" onClick={onPayProvider}>
+ <i18n.Translate>Pay</i18n.Translate>
+ </Button>
+ ) : undefined}
+ {info.paymentStatus.type ===
+ ProviderPaymentType.InsufficientBalance ? (
+ <Button variant="contained" color="primary" onClick={onWithdraw}>
+ <i18n.Translate>Withdraw</i18n.Translate>
+ </Button>
+ ) : undefined}
</div>
</footer>
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
index ef1295846..20de1a3c3 100644
--- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -36,6 +36,7 @@ import * as a17 from "./QrReader.stories.js";
import * as a18 from "./DestinationSelection.stories.js";
import * as a19 from "./ExchangeSelection/stories.js";
import * as a20 from "./ManageAccount/stories.js";
+import * as a21 from "./Notifications/stories.js";
export default [
a1,
@@ -55,4 +56,5 @@ export default [
a18,
a19,
a20,
+ a21,
];