From e9bb85a212dbd9b86875e89a0aca5d805e2ad61b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 13 Aug 2021 18:04:05 -0300 Subject: new wallet UI and more tests --- .../.storybook/preview.js | 2 +- .../src/browserWorkerEntry.ts | 2 +- .../src/components/Diagnostics.tsx | 28 +- .../src/components/styled/index.tsx | 66 ++++- .../src/hooks/useDiagnostics.ts | 29 +++ .../src/hooks/useTalerActionURL.ts | 8 +- .../src/popup/BalancePage.tsx | 2 +- .../taler-wallet-webextension/src/popup/Debug.tsx | 4 +- .../src/popup/History.stories.tsx | 55 +++- .../src/popup/History.tsx | 52 +++- .../src/popup/ProviderDetailPage.tsx | 7 +- .../src/popup/Settings.tsx | 2 +- .../src/popup/Transaction.stories.tsx | 2 +- .../taler-wallet-webextension/src/popup/popup.tsx | 2 +- .../src/popupEntryPoint.tsx | 2 +- .../src/wallet/Pay.stories.tsx | 103 ++++++++ .../taler-wallet-webextension/src/wallet/Pay.tsx | 287 ++++++++++++--------- .../src/wallet/Refund.stories.tsx | 83 ++++++ .../src/wallet/Refund.tsx | 48 ++-- .../src/wallet/Tip.stories.tsx | 66 +++++ .../taler-wallet-webextension/src/wallet/Tip.tsx | 63 +++-- .../src/wallet/Welcome.stories.tsx | 56 ++++ .../src/wallet/Welcome.tsx | 34 ++- .../src/wallet/Withdraw.stories.tsx | 16 -- .../src/wallet/Withdraw.tsx | 155 ++++++----- .../src/walletEntryPoint.tsx | 68 +---- .../taler-wallet-webextension/src/wxBackend.ts | 8 +- .../taler-wallet-webextension/static/wallet.html | 1 + 28 files changed, 851 insertions(+), 400 deletions(-) create mode 100644 packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts create mode 100644 packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx (limited to 'packages') diff --git a/packages/taler-wallet-webextension/.storybook/preview.js b/packages/taler-wallet-webextension/.storybook/preview.js index 169b726f9..02a4e43d4 100644 --- a/packages/taler-wallet-webextension/.storybook/preview.js +++ b/packages/taler-wallet-webextension/.storybook/preview.js @@ -56,7 +56,7 @@ export const decorators = [ // add a fake header so it looks similar return -
+
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts index d8dff72fb..b5c26a7bb 100644 --- a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts +++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts @@ -68,6 +68,6 @@ worker.onmessage = (msg: MessageEvent) => { } handleRequest(operation, id, args).catch((e) => { - console.error("error in browsere worker", e); + console.error("error in browser worker", e); }); }; diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx index 146b0dd3e..b36525dbf 100644 --- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx +++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx @@ -4,30 +4,12 @@ import { PageLink } from "../renderHtml"; import { WalletDiagnostics } from "@gnu-taler/taler-util"; import { JSX } from "preact/jsx-runtime"; +interface Props { + timedOut: boolean; + diagnostics: WalletDiagnostics | undefined +} -export function Diagnostics(): JSX.Element | null { - const [timedOut, setTimedOut] = useState(false); - const [diagnostics, setDiagnostics] = useState( - undefined - ); - - useEffect(() => { - let gotDiagnostics = false; - setTimeout(() => { - if (!gotDiagnostics) { - console.error("timed out"); - setTimedOut(true); - } - }, 1000); - const doFetch = async (): Promise => { - const d = await getDiagnostics(); - console.log("got diagnostics", d); - gotDiagnostics = true; - setDiagnostics(d); - }; - console.log("fetching diagnostics"); - doFetch(); - }, []); +export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null { if (timedOut) { return

Diagnostics timed out. Could not talk to the wallet backend.

; diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index cf7f3e06a..7f709db46 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -11,13 +11,32 @@ export const PaymentStatus = styled.div<{ color: string }>` background-color: ${p => p.color}; ` -export const PopupBox = styled.div` - height: calc(320px - 34px - 16px); +export const WalletPage = styled.section` + border: solid 5px black; + border-radius: 10px; + margin-left: auto; + margin-right: auto; + padding-top: 2em; + max-width: 50%; + padding: 2em; + + margin: auto; + height: 100%; + + & h1:first-child { + margin-top: 0; + } +` + +export const PopupBox = styled.div<{ noPadding?: boolean }>` + height: 290px; display: flex; flex-direction: column; justify-content: space-between; & > section { + padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'}; + padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'}; // this margin will send the section up when used with a header margin-bottom: auto; overflow: auto; @@ -35,6 +54,7 @@ export const PopupBox = styled.div` flex-direction: row; justify-content: space-between; display: flex; + padding: 8px; margin-bottom: 5px; & > div { @@ -44,15 +64,23 @@ export const PopupBox = styled.div` & > h3 { margin: 0px; } + + & > .title { + /* margin: 1em; */ + font-size: large; + color: #3c4e92; + } } & > footer { - padding-top: 5px; + padding-top: 8px; + padding-bottom: 8px; flex-direction: row; justify-content: space-between; display: flex; & button { - margin-left: 5px; + margin-right: 8px; + margin-left: 8px; } } @@ -145,6 +173,13 @@ export const Row = styled.div` padding: 0.5em; ` +export const Row2 = styled.div` + display: flex; + /* margin: 0.5em 0; */ + justify-content: space-between; + padding: 0.5em; +` + export const Column = styled.div` display: flex; flex-direction: column; @@ -154,10 +189,15 @@ export const Column = styled.div` export const RowBorderGray = styled(Row)` border: 1px solid gray; - border-radius: 0.5em; + /* border-radius: 0.5em; */ ` -export const HistoryRow = styled(RowBorderGray)` +export const RowLightBorderGray = styled(Row2)` + border: 1px solid lightgray; + /* border-radius: 0.5em; */ +` + +export const HistoryRow = styled(RowLightBorderGray)` & > ${Column}:last-of-type { margin-left: auto; align-self: center; @@ -244,24 +284,24 @@ export const ErrorBox = styled.div` } } ` -export const PopupNavigation = styled.div` - background-color: #033; +export const PopupNavigation = styled.div<{devMode?:boolean}>` + background-color:#0042b2; + height: 35px; & > a { color: #f8faf7; - padding-top: 0.7em; display: inline-block; - width: calc(400px / 5); - padding-bottom: 0.7em; + width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5}); text-align: center; text-decoration: none; + vertical-align: middle; + line-height: 35px; } & > a.active { background-color: #f8faf7; - color: #000; + color: #0042b2; font-weight: bold; - } `; diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts new file mode 100644 index 000000000..e2c62f998 --- /dev/null +++ b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts @@ -0,0 +1,29 @@ +import { WalletDiagnostics } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; + +export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] { + const [timedOut, setTimedOut] = useState(false); + const [diagnostics, setDiagnostics] = useState( + undefined + ); + + useEffect(() => { + let gotDiagnostics = false; + setTimeout(() => { + if (!gotDiagnostics) { + console.error("timed out"); + setTimedOut(true); + } + }, 1000); + const doFetch = async (): Promise => { + const d = await wxApi.getDiagnostics(); + console.log("got diagnostics", d); + gotDiagnostics = true; + setDiagnostics(d); + }; + console.log("fetching diagnostics"); + doFetch(); + }, []); + return [diagnostics, timedOut] +} \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts index b884ca943..1c8504a8e 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts @@ -57,12 +57,8 @@ function makeExtensionUrlWithParams( ): string { const innerUrl = new URL(chrome.extension.getURL("/" + url)); if (params) { - for (const key in params) { - const p = params[key]; - if (p) { - innerUrl.searchParams.set(key, p); - } - } + const hParams = Object.keys(params).map(k => `${k}=${params[k]}`).join('&') + innerUrl.hash = innerUrl.hash + '?' + hParams } return innerUrl.href; } diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index cff17af1a..5a2b9f747 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -43,7 +43,7 @@ export function BalanceView({ balance, Linker }: BalanceViewProps) {

{i18n.str`Error: could not retrieve balance information.`}

- Click here for help and + Click here for help and diagnostics.

diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx index 1f6014e8e..33b82b05b 100644 --- a/packages/taler-wallet-webextension/src/popup/Debug.tsx +++ b/packages/taler-wallet-webextension/src/popup/Debug.tsx @@ -16,10 +16,12 @@ import { JSX } from "preact"; import { Diagnostics } from "../components/Diagnostics"; +import { useDiagnostics } from "../hooks/useDiagnostics.js"; import * as wxApi from "../wxApi"; export function DeveloperPage(props: any): JSX.Element { + const [status, timedOut] = useDiagnostics(); return (

Debug tools:

@@ -27,7 +29,7 @@ export function DeveloperPage(props: any): JSX.Element {
- +
); } diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx index 8eef7dc31..5337a6c1c 100644 --- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.stories.tsx @@ -30,7 +30,7 @@ import { FunctionalComponent } from 'preact'; import { HistoryView as TestedComponent } from './History'; export default { - title: 'popup/transaction/list', + title: 'popup/history/list', component: TestedComponent, }; @@ -112,12 +112,26 @@ function createExample(Component: FunctionalComponent, props: Part } export const Empty = createExample(TestedComponent, { - list: [] + list: [], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] }); export const One = createExample(TestedComponent, { - list: [exampleData.withdraw] + list: [exampleData.withdraw], + balances: [{ + available: 'USD:10', + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] }); export const Several = createExample(TestedComponent, { @@ -130,7 +144,40 @@ export const Several = createExample(TestedComponent, { exampleData.refund, exampleData.tip, exampleData.deposit, - ] + ], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] +}); + +export const SeveralWithTwoCurrencies = createExample(TestedComponent, { + list: [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + exampleData.refresh, + exampleData.refund, + exampleData.tip, + exampleData.deposit, + ], + balances: [{ + available: 'TESTKUDOS:10', + pendingIncoming: 'TESTKUDOS:0', + pendingOutgoing: 'TESTKUDOS:0', + hasPendingTransactions: false, + requiresUserInput: false, + },{ + available: 'USD:10', + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + hasPendingTransactions: false, + requiresUserInput: false, + }] }); // export const WithdrawPending = createExample(TestedComponent, { diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx index 57fc10c26..b6b65314e 100644 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see */ -import { AmountString, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, AmountString, Balance, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util"; import { JSX } from "preact"; import { useEffect, useState } from "preact/hooks"; import * as wxApi from "../wxApi"; @@ -25,6 +25,8 @@ export function HistoryPage(props: any): JSX.Element { const [transactions, setTransactions] = useState< TransactionsResponse | undefined >(undefined); + const balance = useBalances() + const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) useEffect(() => { const fetchData = async (): Promise => { @@ -38,16 +40,36 @@ export function HistoryPage(props: any): JSX.Element { return
Loading ...
; } - return ; + return ; } -export function HistoryView({ list }: { list: Transaction[] }) { - return +function amountToString(c: AmountString) { + const idx = c.indexOf(':') + return `${c.substring(idx+1)} ${c.substring(0,idx)}` +} + + + +export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) { + return + {balances.length > 0 &&
+ {balances.length === 1 &&
+ Balance: {amountToString(balances[0].available)} +
} + {balances.length > 1 &&
+ Balance:
    + {balances.map(b =>
  • {b.available}
  • )} +
+
} +
}
- {list.map((tx, i) => ( + {list.slice(0, 3).map((tx, i) => ( ))}
+
} @@ -57,6 +79,8 @@ import imageRefund from '../../static/img/ri-refund-2-line.svg'; import imageHandHeart from '../../static/img/ri-hand-heart-line.svg'; import imageRefresh from '../../static/img/ri-refresh-line.svg'; import { Column, ExtraLargeText, HistoryRow, PopupBox, Row, RowBorderGray, SmallTextLight } from "../components/styled"; +import { useBalances } from "../hooks/useBalances"; +import { formatDistance } from "date-fns"; function TransactionItem(props: { tx: Transaction }): JSX.Element { const tx = props.tx; @@ -144,23 +168,21 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element { function TransactionLayout(props: TransactionLayoutProps): JSX.Element { const date = new Date(props.timestamp.t_ms); - const dateStr = date.toLocaleString([], { - dateStyle: "medium", - timeStyle: "short", - } as any); + const now = new Date(); + const dateStr = formatDistance(date, now, { addSuffix: true }) return ( - {dateStr} {props.title} {props.pending ? ( (Pending) ) : null} + {dateStr} -
{props.subtitle}
+ {/*
{props.subtitle}
*/}
+ {sign} {amount} diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx index c92137ee3..707e6c33a 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx @@ -58,13 +58,12 @@ export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewP const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged return ( -
+ {info.backupProblem || info.lastError ?
- -
+
: undefined }

{info.name} {info.syncProviderBaseUrl}

- {isPaid ? 'Paid': 'Unpaid' } + {isPaid ? 'Paid' : 'Unpaid'}

Last backup: {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')}

diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx index 18afcd100..40ab51561 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -68,7 +68,7 @@ const names: LangsNames = { export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { return (
-
+

Wallet

+ return {i18n.str`Balance`} {i18n.str`History`} {i18n.str`Backup`} diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index 39c25d508..faa5149ac 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -93,7 +93,7 @@ function Application() {
-
+
diff --git a/packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx new file mode 100644 index 000000000..0297d6264 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Pay.stories.tsx @@ -0,0 +1,103 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util'; +import { FunctionalComponent, h } from 'preact'; +import { PaymentRequestView as TestedComponent } from './Pay'; + + +export default { + title: 'wallet/pay', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const InsufficientBalance = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.InsufficientBalance, + proposalId: "proposal1234", + contractTerms: { + merchant: { + name: 'someone' + }, + amount: 'USD:10', + } as Partial as any, + amountRaw: 'USD:10', + } +}); + +export const PaymentPossible = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.PaymentPossible, + amountEffective: 'USD:10', + amountRaw: 'USD:10', + contractTerms: { + merchant: { + name: 'someone' + }, + amount: 'USD:10', + } as Partial as any, + contractTermsHash: '123456', + proposalId: 'proposal1234' + } +}); + +export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.AlreadyConfirmed, + amountEffective: 'USD:10', + amountRaw: 'USD:10', + contractTerms: { + merchant: { + name: 'someone' + }, + fulfillment_message: 'congratulations! you are looking at the fulfillment message! ', + amount: 'USD:10', + } as Partial as any, + contractTermsHash: '123456', + proposalId: 'proposal1234', + paid: false, + } +}); + +export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent, { + payStatus: { + status: PreparePayResultType.AlreadyConfirmed, + amountEffective: 'USD:10', + amountRaw: 'USD:10', + contractTerms: { + merchant: { + name: 'someone' + }, + amount: 'USD:10', + } as Partial as any, + contractTermsHash: '123456', + proposalId: 'proposal1234', + paid: false, + } +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Pay.tsx b/packages/taler-wallet-webextension/src/wallet/Pay.tsx index bd06656c7..a5849bb28 100644 --- a/packages/taler-wallet-webextension/src/wallet/Pay.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Pay.tsx @@ -29,7 +29,7 @@ import * as wxApi from "../wxApi"; import { useState, useEffect } from "preact/hooks"; -import { getJsonI18n, i18n } from "@gnu-taler/taler-util"; +import { ConfirmPayResultDone, getJsonI18n, i18n } from "@gnu-taler/taler-util"; import { PreparePayResult, ConfirmPayResult, @@ -45,13 +45,54 @@ interface Props { talerPayUri?: string } +export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) { + const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; + let message; + if (fulfillmentUrl) { + message = ( + + You have already paid for this article. Click{" "} + here to view it again. + + ); + } else { + message = + You have already paid for this article:{" "} + + {payStatus.contractTerms.fulfillment_message ?? "no message given"} + + ; + } + return
+

GNU Taler Wallet

+
+ {message} +
+
+} + +const doPayment = async (payStatus: PreparePayResult): Promise => { + if (payStatus.status !== "payment-possible") { + throw Error(`invalid state: ${payStatus.status}`); + } + const proposalId = payStatus.proposalId; + const res = await wxApi.confirmPay(proposalId, undefined); + if (res.type !== ConfirmPayResultType.Done) { + throw Error("payment pending"); + } + const fu = res.contractTerms.fulfillment_url; + if (fu) { + document.location.href = fu; + } + return res; +}; + + + export function PayPage({ talerPayUri }: Props): JSX.Element { const [payStatus, setPayStatus] = useState(undefined); const [payResult, setPayResult] = useState(undefined); const [payErrMsg, setPayErrMsg] = useState(""); - const [numTries, setNumTries] = useState(0); - const [loading, setLoading] = useState(false); - let totalFees: AmountJson | undefined = undefined; useEffect(() => { if (!talerPayUri) return; @@ -60,53 +101,67 @@ export function PayPage({ talerPayUri }: Props): JSX.Element { setPayStatus(p); }; doFetch(); - }, [numTries, talerPayUri]); + }, [talerPayUri]); if (!talerPayUri) { return missing pay uri } - + if (!payStatus) { return Loading payment information ...; } - let insufficientBalance = false; - if (payStatus.status == PreparePayResultType.InsufficientBalance) { - insufficientBalance = true; - } - - if (payStatus.status === PreparePayResultType.PaymentPossible) { - const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw); - const amountEffective: AmountJson = Amounts.parseOrThrow( - payStatus.amountEffective, - ); - totalFees = Amounts.sub(amountEffective, amountRaw).amount; - } - - if ( - payStatus.status === PreparePayResultType.AlreadyConfirmed && - numTries === 0 - ) { - const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; - if (fulfillmentUrl) { + if (payResult && payResult.type === ConfirmPayResultType.Done) { + if (payResult.contractTerms.fulfillment_message) { + const obj = { + fulfillment_message: payResult.contractTerms.fulfillment_message, + fulfillment_message_i18n: + payResult.contractTerms.fulfillment_message_i18n, + }; + const msg = getJsonI18n(obj, "fulfillment_message"); return ( - - You have already paid for this article. Click{" "} - here to view it again. - +
+

Payment succeeded.

+

{msg}

+
); } else { - - You have already paid for this article:{" "} - - {payStatus.contractTerms.fulfillment_message ?? "no message given"} - - ; + return Redirecting ...; } } + const onClick = async () => { + try { + const res = await doPayment(payStatus) + setPayResult(res); + } catch (e) { + console.error(e); + setPayErrMsg(e.message); + } + + } + + return ; +} + +export interface PaymentRequestViewProps { + payStatus: PreparePayResult; + onClick: () => void; + payErrMsg?: string; + +} +export function PaymentRequestView({ payStatus, onClick, payErrMsg }: PaymentRequestViewProps) { + let totalFees: AmountJson | undefined = undefined; + let insufficientBalance = false; + const [loading, setLoading] = useState(false); const contractTerms: ContractTerms = payStatus.contractTerms; + if ( + payStatus.status === PreparePayResultType.AlreadyConfirmed + ) { + return + } + if (!contractTerms) { return ( @@ -115,6 +170,18 @@ export function PayPage({ talerPayUri }: Props): JSX.Element { ); } + if (payStatus.status == PreparePayResultType.InsufficientBalance) { + insufficientBalance = true; + } + + if (payStatus.status === PreparePayResultType.PaymentPossible) { + const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw); + const amountEffective: AmountJson = Amounts.parseOrThrow( + payStatus.amountEffective, + ); + totalFees = Amounts.sub(amountEffective, amountRaw).amount; + } + let merchantName: VNode; if (contractTerms.merchant && contractTerms.merchant.name) { merchantName = {contractTerms.merchant.name}; @@ -126,99 +193,61 @@ export function PayPage({ talerPayUri }: Props): JSX.Element { {renderAmount(Amounts.parseOrThrow(contractTerms.amount))} ); - const doPayment = async (): Promise => { - if (payStatus.status !== "payment-possible") { - throw Error(`invalid state: ${payStatus.status}`); - } - const proposalId = payStatus.proposalId; - setNumTries(numTries + 1); - try { - setLoading(true); - const res = await wxApi.confirmPay(proposalId, undefined); - if (res.type !== ConfirmPayResultType.Done) { - throw Error("payment pending"); - } - const fu = res.contractTerms.fulfillment_url; - if (fu) { - document.location.href = fu; - } - setPayResult(res); - } catch (e) { - console.error(e); - setPayErrMsg(e.message); - } - }; - - if (payResult && payResult.type === ConfirmPayResultType.Done) { - if (payResult.contractTerms.fulfillment_message) { - const obj = { - fulfillment_message: payResult.contractTerms.fulfillment_message, - fulfillment_message_i18n: - payResult.contractTerms.fulfillment_message_i18n, - }; - const msg = getJsonI18n(obj, "fulfillment_message"); - return ( -
-

Payment succeeded.

-

{msg}

-
- ); - } else { - return Redirecting ...; - } - } - - return ( -
-

- - The merchant {merchantName} offers you to purchase: - -

- {contractTerms.summary} -
- {totalFees ? ( + return
+

GNU Taler Wallet

+
+
+

- The total price is {amount} - (plus {renderAmount(totalFees)} fees). - + The merchant {merchantName} offers you to purchase: + +

+ {contractTerms.summary} +
+ {totalFees ? ( + + The total price is {amount} + (plus {renderAmount(totalFees)} fees). + + ) : ( + + The total price is {amount}. + + )} +

+ + {insufficientBalance ? ( +
+

+ Unable to pay: Your balance is insufficient. +

+
+ ) : null} + + {payErrMsg ? ( +
+

Payment failed: {payErrMsg}

+ +
) : ( - - The total price is {amount}. - - )} -

- - {insufficientBalance ? ( -
-

- Unable to pay: Your balance is insufficient. -

-
- ) : null} - - {payErrMsg ? ( -
-

Payment failed: {payErrMsg}

- -
- ) : ( -
- doPayment()} - > - {i18n.str`Confirm payment`} - -
- )} -
- ); -} - +
+ + {i18n.str`Confirm payment`} + +
+ )} +
+ +
+ + +} \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx new file mode 100644 index 000000000..044141f0c --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Refund.stories.tsx @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ContractTerms, OrderShortInfo, PreparePayResultType } from '@gnu-taler/taler-util'; +import { FunctionalComponent, h } from 'preact'; +import { View as TestedComponent } from './Refund'; + + +export default { + title: 'wallet/refund', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const Complete = createExample(TestedComponent, { + applyResult: { + amountEffectivePaid: 'USD:10', + amountRefundGone: 'USD:0', + amountRefundGranted: 'USD:2', + contractTermsHash: 'QWEASDZXC', + info: { + summary: 'tasty cold beer', + contractTermsHash: 'QWEASDZXC', + } as Partial as any, + pendingAtExchange: false, + proposalId: "proposal123", + } +}); + +export const Partial = createExample(TestedComponent, { + applyResult: { + amountEffectivePaid: 'USD:10', + amountRefundGone: 'USD:1', + amountRefundGranted: 'USD:2', + contractTermsHash: 'QWEASDZXC', + info: { + summary: 'tasty cold beer', + contractTermsHash: 'QWEASDZXC', + } as Partial as any, + pendingAtExchange: false, + proposalId: "proposal123", + } +}); + +export const InProgress = createExample(TestedComponent, { + applyResult: { + amountEffectivePaid: 'USD:10', + amountRefundGone: 'USD:1', + amountRefundGranted: 'USD:2', + contractTermsHash: 'QWEASDZXC', + info: { + summary: 'tasty cold beer', + contractTermsHash: 'QWEASDZXC', + } as Partial as any, + pendingAtExchange: true, + proposalId: "proposal123", + } +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Refund.tsx b/packages/taler-wallet-webextension/src/wallet/Refund.tsx index 702217415..bb26d933b 100644 --- a/packages/taler-wallet-webextension/src/wallet/Refund.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Refund.tsx @@ -32,7 +32,32 @@ import { JSX } from "preact/jsx-runtime"; interface Props { talerRefundUri?: string } - +export interface ViewProps { + applyResult: ApplyRefundResponse; +} +export function View({ applyResult }: ViewProps) { + return
+

GNU Taler Wallet

+
+

Refund Status

+

+ The product {applyResult.info.summary} has received a total + effective refund of{" "} + . +

+ {applyResult.pendingAtExchange ? ( +

Refund processing is still in progress.

+ ) : null} + {!Amounts.isZero(applyResult.amountRefundGone) ? ( +

+ The refund amount of{" "} + {" "} + could not be applied. +

+ ) : null} +
+
+} export function RefundPage({ talerRefundUri }: Props): JSX.Element { const [applyResult, setApplyResult] = useState(undefined); const [errMsg, setErrMsg] = useState(undefined); @@ -66,24 +91,5 @@ export function RefundPage({ talerRefundUri }: Props): JSX.Element { return Updating refund status; } - return ( - <> -

Refund Status

-

- The product {applyResult.info.summary} has received a total - effective refund of{" "} - . -

- {applyResult.pendingAtExchange ? ( -

Refund processing is still in progress.

- ) : null} - {!Amounts.isZero(applyResult.amountRefundGone) ? ( -

- The refund amount of{" "} - - could not be applied. -

- ) : null} - - ); + return ; } diff --git a/packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx new file mode 100644 index 000000000..ffd976144 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Tip.stories.tsx @@ -0,0 +1,66 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util'; +import { FunctionalComponent, h } from 'preact'; +import { View as TestedComponent } from './Tip'; + + +export default { + title: 'wallet/tip', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const Accepted = createExample(TestedComponent, { + prepareTipResult: { + accepted: true, + merchantBaseUrl: '', + exchangeBaseUrl: '', + expirationTimestamp : { + t_ms: 0 + }, + tipAmountEffective: 'USD:10', + tipAmountRaw: 'USD:5', + walletTipId: 'id' + } +}); + +export const NotYetAccepted = createExample(TestedComponent, { + prepareTipResult: { + accepted: false, + merchantBaseUrl: 'http://merchant.url/', + exchangeBaseUrl: 'http://exchange.url/', + expirationTimestamp : { + t_ms: 0 + }, + tipAmountEffective: 'USD:10', + tipAmountRaw: 'USD:5', + walletTipId: 'id' + } +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Tip.tsx b/packages/taler-wallet-webextension/src/wallet/Tip.tsx index 708e8940b..69886668b 100644 --- a/packages/taler-wallet-webextension/src/wallet/Tip.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Tip.tsx @@ -26,8 +26,41 @@ import { AmountView } from "../renderHtml"; import * as wxApi from "../wxApi"; import { JSX } from "preact/jsx-runtime"; -interface Props { - talerTipUri?: string +interface Props { + talerTipUri?: string +} +export interface ViewProps { + prepareTipResult: PrepareTipResult; + onAccept: () => void; + onIgnore: () => void; + +} +export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) { + return
+

GNU Taler Wallet

+
+ {prepareTipResult.accepted ? ( + + Tip from {prepareTipResult.merchantBaseUrl} accepted. Check + your transactions list for more details. + + ) : ( +
+

+ The merchant {prepareTipResult.merchantBaseUrl} is + offering you a tip of{" "} + + + {" "} + via the exchange {prepareTipResult.exchangeBaseUrl} +

+ + +
+ )} +
+
+ } export function TipPage({ talerTipUri }: Props): JSX.Element { @@ -71,27 +104,7 @@ export function TipPage({ talerTipUri }: Props): JSX.Element { return Loading ...; } - if (prepareTipResult.accepted) { - return ( - - Tip from {prepareTipResult.merchantBaseUrl} accepted. Check - your transactions list for more details. - - ); - } else { - return ( -
-

- The merchant {prepareTipResult.merchantBaseUrl} is - offering you a tip of{" "} - - - {" "} - via the exchange {prepareTipResult.exchangeBaseUrl} -

- - -
- ); - } + return } diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx new file mode 100644 index 000000000..4fa87a137 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx @@ -0,0 +1,56 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { FunctionalComponent, h } from 'preact'; +import { View as TestedComponent } from './Welcome'; + + +export default { + title: 'wallet/welcome', + component: TestedComponent, +}; + +function createExample(Component: FunctionalComponent, props: Partial) { + const r = (args: any) => + r.args = props + return r +} + +export const Normal = createExample(TestedComponent, { + permissionsEnabled: true, + diagnostics: { + errors: [], + walletManifestVersion: '1.0', + walletManifestDisplayVersion: '1.0', + firefoxIdbProblem: false, + dbOutdated: false, + } +}); + +export const TimedoutDiagnostics = createExample(TestedComponent, { + timedOut: true, + permissionsEnabled: false, +}); + +export const RunningDiagnostics = createExample(TestedComponent, { + permissionsEnabled: false, +}); + diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index c74384596..4c33e1c72 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -24,13 +24,36 @@ import { JSX } from "preact/jsx-runtime"; import { Checkbox } from "../components/Checkbox"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { Diagnostics } from "../components/Diagnostics"; +import { WalletPage } from "../components/styled"; +import { useDiagnostics } from "../hooks/useDiagnostics"; +import { WalletDiagnostics } from "@gnu-taler/taler-util"; -export function WelcomePage(): JSX.Element { +export function WelcomePage() { const [permissionsEnabled, togglePermissions] = useExtendedPermissions() - return ( - <> + const [diagnostics, timedOut] = useDiagnostics() + return +} + +export interface ViewProps { + permissionsEnabled: boolean, + togglePermissions: () => void, + diagnostics: WalletDiagnostics | undefined, + timedOut: boolean, +} +export function View({ permissionsEnabled, togglePermissions, diagnostics, timedOut }: ViewProps): JSX.Element { + return ( +
+

+ Taler Wallet +

+
+

Browser Extension Installed!

+

Thank you for installing the wallet.

- +

Permissions

Learn how to top up your wallet balance » - +
+
); } diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx index 24fb17dfa..fef36b820 100644 --- a/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx @@ -30,27 +30,12 @@ export default { }, }; -export const WithoutURI = (a: any) => ; -WithoutURI.args = { -} as ViewProps - export const WithoutDetails = (a: any) => ; WithoutDetails.args = { - talerWithdrawUri: 'http://something' -} as ViewProps - -export const Cancelled = (a: any) => ; -Cancelled.args = { - talerWithdrawUri: 'http://something', - details: { - amount: 'USD:2', - }, - cancelled: true } as ViewProps export const CompleteWithExchange = (a: any) => ; CompleteWithExchange.args = { - talerWithdrawUri: 'http://something', details: { amount: 'USD:2', }, @@ -59,7 +44,6 @@ CompleteWithExchange.args = { export const CompleteWithoutExchange = (a: any) => ; CompleteWithoutExchange.args = { - talerWithdrawUri: 'http://something', details: { amount: 'USD:2', }, diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx b/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx index 4cb8ebfa1..442ee7dae 100644 --- a/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx @@ -32,6 +32,7 @@ import { } from "../wxApi"; import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util"; import { JSX } from "preact/jsx-runtime"; +import { WalletPage } from '../components/styled'; interface Props { talerWithdrawUri?: string; @@ -39,79 +40,72 @@ interface Props { export interface ViewProps { talerWithdrawUri?: string; - details?: WithdrawUriInfoResponse; - cancelled?: boolean; + details: WithdrawUriInfoResponse; selectedExchange?: string; accept: () => Promise; setCancelled: (b: boolean) => void; setSelecting: (b: boolean) => void; }; -export function View({ talerWithdrawUri, details, cancelled, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { - const [state, setState] = useState(1) - setTimeout(() => { - setState(s => s + 1) - }, 1000); - if (!talerWithdrawUri) { - return missing withdraw uri; - } - - if (!details) { - return Loading...; - } - - if (cancelled) { - return Withdraw operation has been cancelled.{state}; - } +export function View({ details, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { return ( -
-

Digital Cash Withdrawal

-

- You are about to withdraw{" "} - {renderAmount(details.amount)} from your bank account - into your wallet. -

- {selectedExchange ? ( -

- The exchange {selectedExchange} will be used as the - Taler payment service provider. -

- ) : null} - -
- -

- setSelecting(true)} - > - {i18n.str`Chose different exchange provider`} - -
- setCancelled(true)} - > - {i18n.str`Cancel withdraw operation`} - -

+ +
+

+ Taler Wallet +

+
+
+
+

Digital Cash Withdrawal

+

+ You are about to withdraw{" "} + {renderAmount(details.amount)} from your bank account + into your wallet. +

+ {selectedExchange ? ( +

+ The exchange {selectedExchange} will be used as the + Taler payment service provider. +

+ ) : null} + +
+ +

+ setSelecting(true)} + > + {i18n.str`Chose different exchange provider`} + +
+ setCancelled(true)} + > + {i18n.str`Cancel withdraw operation`} + +

+
+
-
+ ) } -export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { +export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element { const [details, setDetails] = useState(undefined); const [selectedExchange, setSelectedExchange] = useState< string | undefined @@ -120,27 +114,44 @@ export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { const [selecting, setSelecting] = useState(false); const [errMsg, setErrMsg] = useState(""); const [updateCounter, setUpdateCounter] = useState(1); + const [state, setState] = useState(1) + + // setTimeout(() => { + // console.log('tick...') + // setState(s => s + 1) + // }, 1000); useEffect(() => { return onUpdateNotification(() => { + console.log('updating...') setUpdateCounter(updateCounter + 1); }); }, []); useEffect(() => { + console.log('on effect yes', talerWithdrawUri) if (!talerWithdrawUri) return const fetchData = async (): Promise => { - const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); - setDetails(res); - if (res.defaultExchangeBaseUrl) { - setSelectedExchange(res.defaultExchangeBaseUrl); + console.log('que pasa') + try { + const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); + console.log('res', res) + setDetails(res); + if (res.defaultExchangeBaseUrl) { + setSelectedExchange(res.defaultExchangeBaseUrl); + } + } catch (e) { + console.error(e) } }; fetchData(); - }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]); + }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter, state]); + + if (!talerWithdrawUri) { + return missing withdraw uri; + } const accept = async (): Promise => { - if (!talerWithdrawUri) return if (!selectedExchange) { throw Error("can't accept, no exchange selected"); } @@ -152,10 +163,16 @@ export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { } }; + if (!details) { + return Loading...; + } + if (cancelled) { + return Withdraw operation has been cancelled.; + } + return } diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index 004fcc717..f487e54fc 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -32,7 +32,6 @@ import { RefundPage } from "./wallet/Refund"; import { TipPage } from './wallet/Tip'; import Router, { route, Route } from "preact-router"; - function main(): void { try { const container = document.getElementById("container"); @@ -67,64 +66,15 @@ enum Pages { } function Application() { - const sp = new URL(document.location.href).searchParams - const queryParams: any = {} - sp.forEach((v, k) => { queryParams[k] = v; }); - - return - - { - return
-
-

- Taler Wallet -

-
-

Browser Extension Installed!

-
- -
-
- }} /> - - { - return
-

GNU Taler Wallet

-
- -
-
- }} /> - - { - return
-

GNU Taler Wallet

-
- -
-
- }} /> - - { - return
-

GNU Taler Wallet

-
- -
-
- }} /> - { - return
-
-

- Taler Wallet -

-
-
- -
-
- }} /> + const h = createHashHistory(); + return + + + + + + +
no yet implemented
} />
no yet implemented
} /> diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index e1517c4cf..c474c940c 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -216,12 +216,8 @@ function makeSyncWalletRedirect( ): Record { const innerUrl = new URL(chrome.extension.getURL(url)); if (params) { - for (const key in params) { - const p = params[key]; - if (p) { - innerUrl.searchParams.set(key, p); - } - } + const hParams = Object.keys(params).map(k => `${k}=${params[k]}`).join('&') + innerUrl.hash = innerUrl.hash + '?' + hParams } if (isFirefox()) { // Some platforms don't support the sync redirect (yet), so fall back to diff --git a/packages/taler-wallet-webextension/static/wallet.html b/packages/taler-wallet-webextension/static/wallet.html index 2b500b56f..817e8bfb8 100644 --- a/packages/taler-wallet-webextension/static/wallet.html +++ b/packages/taler-wallet-webextension/static/wallet.html @@ -4,6 +4,7 @@ + -- cgit v1.2.3