diff options
40 files changed, 953 insertions, 1961 deletions
diff --git a/packages/taler-wallet-webextension/.storybook/preview.js b/packages/taler-wallet-webextension/.storybook/preview.js index 0efb96308..25f9f46ba 100644 --- a/packages/taler-wallet-webextension/.storybook/preview.js +++ b/packages/taler-wallet-webextension/.storybook/preview.js @@ -18,7 +18,7 @@ import { h, Fragment } from "preact" import { NavBar } from '../src/NavigationBar' import { LogoHeader } from '../src/components/LogoHeader' import { TranslationProvider } from '../src/context/translation' - +import { PopupBox, WalletBox } from '../src/components/styled' export const parameters = { controls: { expanded: true }, actions: { argTypesRegex: "^on[A-Z].*" }, @@ -58,9 +58,9 @@ export const decorators = [ // add a fake header so it looks similar return <Fragment> <NavBar path={path} devMode={path === '/dev'} /> - <div style={{ width: 400, height: 290 }}> + <PopupBox> <Story /> - </div> + </PopupBox> </Fragment> } @@ -125,7 +125,7 @@ export const decorators = [ <link key="1" rel="stylesheet" type="text/css" href="/static/style/pure.css" /> <link key="2" rel="stylesheet" type="text/css" href="/static/style/wallet.css" /> <Story /> - </div> + </div> } if (kind.startsWith('wallet')) { const path = /wallet(\/.*).*/.exec(kind)[1]; @@ -157,7 +157,9 @@ export const decorators = [ </style> <LogoHeader /> <NavBar path={path} devMode={path === '/dev'} /> - <Story /> + <WalletBox> + <Story /> + </WalletBox> </div> } return <div> diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx new file mode 100644 index 000000000..e1c19cc23 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx @@ -0,0 +1,48 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util"; +import { h, VNode } from "preact"; +import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index"; + +export function BalanceTable({ balances }: { balances: Balance[] }): VNode { + const currencyFormatter = new Intl.NumberFormat("en-US"); + return ( + <TableWithRoundedRows> + {balances.map((entry, idx) => { + const av = Amounts.parseOrThrow(entry.available); + + const v = currencyFormatter.format( + av.value + av.fraction / amountFractionalBase, + ); + return ( + <tr key={idx}> + <td>{av.currency}</td> + <td + style={{ + fontSize: "2em", + textAlign: "right", + width: "100%", + }} + > + {v} + </td> + </tr> + ); + })} + </TableWithRoundedRows> + ); +} diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx new file mode 100644 index 000000000..71365e089 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx @@ -0,0 +1,109 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems SA + + 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 { PaytoUri } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { CopiedIcon, CopyIcon } from "../svg"; +import { ButtonBox, TooltipRight } from "./styled"; + +export interface BankDetailsProps { + payto: PaytoUri | undefined; + exchangeBaseUrl: string; + subject: string; + amount: string; +} + +export function BankDetailsByPaytoType({ + payto, + subject, + exchangeBaseUrl, + amount, +}: BankDetailsProps): VNode { + const firstPart = !payto ? undefined : !payto.isKnown ? ( + <Row name="Account" value={payto.targetPath} /> + ) : payto.targetType === "x-taler-bank" ? ( + <Fragment> + <Row name="Bank host" value={payto.host} /> + <Row name="Bank account" value={payto.account} /> + </Fragment> + ) : payto.targetType === "iban" ? ( + <Row name="IBAN" value={payto.iban} /> + ) : undefined; + return ( + <div style={{ textAlign: "left" }}> + <p>Bank transfer details</p> + <table> + {firstPart} + <Row name="Exchange" value={exchangeBaseUrl} /> + <Row name="Chosen amount" value={amount} /> + <Row name="Subject" value={subject} literal /> + </table> + </div> + ); +} + +function Row({ + name, + value, + literal, +}: { + name: string; + value: string; + literal?: boolean; +}): VNode { + const [copied, setCopied] = useState(false); + function copyText(): void { + navigator.clipboard.writeText(value); + setCopied(true); + } + useEffect(() => { + if (copied) { + setTimeout(() => { + setCopied(false); + }, 1000); + } + }, [copied]); + return ( + <tr> + <td> + {!copied ? ( + <ButtonBox onClick={copyText}> + <CopyIcon /> + </ButtonBox> + ) : ( + <TooltipRight content="Copied"> + <ButtonBox disabled> + <CopiedIcon /> + </ButtonBox> + </TooltipRight> + )} + </td> + <td> + <b>{name}</b> + </td> + {literal ? ( + <td> + <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {value} + </pre> + </td> + ) : ( + <td>{value}</td> + )} + </tr> + ); +} diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 2db7c61f8..b2ca13801 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -15,6 +15,8 @@ */ // need to import linaria types, otherwise compiler will complain +// eslint-disable-next-line @typescript-eslint/no-unused-vars +// eslint-disable-next-line no-unused-vars import type * as Linaria from "@linaria/core"; import { styled } from "@linaria/react"; @@ -78,9 +80,8 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>` width: 400px; } & > 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 + padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")}; + margin-bottom: auto; overflow: auto; @@ -202,6 +203,152 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>` } `; +export const TableWithRoundRows = styled.table` + border-collapse: separate; + border-spacing: 0px 10px; + margin-top: -10px; + + td { + border: solid 1px #000; + border-style: solid none; + padding: 10px; + } + td:first-child { + border-left-style: solid; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + td:last-child { + border-right-style: solid; + border-bottom-right-radius: 5px; + border-top-right-radius: 5px; + } +`; + +const Tooltip = styled.div<{ content: string }>` + display: block; + position: relative; + + ::before { + position: absolute; + z-index: 1000001; + width: 0; + height: 0; + color: darkgray; + pointer-events: none; + content: ""; + border: 6px solid transparent; + + border-bottom-color: darkgray; + } + + ::after { + position: absolute; + z-index: 1000001; + padding: 0.5em 0.75em; + font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", + Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + -webkit-font-smoothing: subpixel-antialiased; + color: white; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(content); + background: darkgray; + border-radius: 6px; + } +`; + +export const TooltipBottom = styled(Tooltip)` + ::before { + top: auto; + right: 50%; + bottom: -7px; + margin-right: -6px; + } + ::after { + top: 100%; + right: -50%; + margin-top: 6px; + } +`; + +export const TooltipRight = styled(Tooltip)` + ::before { + top: 0px; + left: 16px; + transform: rotate(-90deg); + } + ::after { + top: -50%; + left: 28px; + margin-top: 6px; + } +`; + +export const Overlay = styled.div` + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2; + cursor: pointer; +`; + +export const CenteredDialog = styled.div` + position: absolute; + text-align: left; + + display: flex; + flex-direction: column; + justify-content: space-between; + + top: 50%; + left: 50%; + /* font-size: 50px; */ + color: black; + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + cursor: initial; + background-color: white; + border-radius: 10px; + + max-height: 70%; + + & > header { + border-top-right-radius: 6px; + border-top-left-radius: 6px; + padding: 10px; + background-color: #f5f5f5; + border-bottom: 1px solid #dbdbdb; + font-weight: bold; + } + & > section { + padding: 10px; + flex-grow: 1; + flex-shrink: 1; + overflow: auto; + } + & > footer { + border-top: 1px solid #dbdbdb; + border-bottom-right-radius: 6px; + border-bottom-left-radius: 6px; + padding: 10px; + background-color: #f5f5f5; + display: flex; + justify-content: space-between; + } +`; + export const Button = styled.button<{ upperCased?: boolean }>` display: inline-block; zoom: 1; @@ -217,7 +364,7 @@ export const Button = styled.button<{ upperCased?: boolean }>` font-family: inherit; font-size: 100%; padding: 0.5em 1em; - color: #444; /* rgba not supported (IE 8) */ + /* color: #444; rgba not supported (IE 8) */ color: rgba(0, 0, 0, 0.8); /* rgba supported */ border: 1px solid #999; /*IE 6/7/8*/ border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ @@ -305,8 +452,7 @@ export const FontIcon = styled.div` `; export const ButtonBox = styled(Button)` padding: 0.5em; - width: fit-content; - height: 2em; + font-size: x-small; & > ${FontIcon} { width: 1em; @@ -320,6 +466,8 @@ export const ButtonBox = styled(Button)` border-radius: 4px; border-color: black; color: black; + /* -webkit-border-horizontal-spacing: 0px; + -webkit-border-vertical-spacing: 0px; */ `; const ButtonVariant = styled(Button)` @@ -377,6 +525,7 @@ export const Centered = styled.div` margin-top: 15px; } `; + export const Row = styled.div` display: flex; margin: 0.5em 0; @@ -566,6 +715,12 @@ export const ErrorBox = styled.div` } `; +export const InfoBox = styled(ErrorBox)` + color: black; + background-color: #d1e7dd; + border-color: #badbcc; +`; + export const SuccessBox = styled(ErrorBox)` color: #0f5132; background-color: #d1e7dd; diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index d5861c47c..30b571f01 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -49,7 +49,7 @@ import { WalletAction, WarningBox, } from "../components/styled"; -import { useBalances } from "../hooks/useBalances"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import * as wxApi from "../wxApi"; interface Props { @@ -109,7 +109,7 @@ export function PayPage({ talerPayUri }: Props): VNode { ); const [payErrMsg, setPayErrMsg] = useState<string | undefined>(undefined); - const balance = useBalances(); + const balance = useAsyncAsHook(wxApi.getBalance); const balanceWithoutError = balance?.hasError ? [] : balance?.response.balances || []; diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts index aa6695c3e..38ec5bf72 100644 --- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts +++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { ExchangesListRespose } from "@gnu-taler/taler-util"; +import { ExchangesListRespose, NotificationType } from "@gnu-taler/taler-util"; import { useEffect, useState } from "preact/hooks"; import * as wxApi from "../wxApi"; @@ -29,7 +29,8 @@ interface HookError { export type HookResponse<T> = HookOk<T> | HookError | undefined; -export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> { +//"withdraw-group-finished" +export function useAsyncAsHook<T>(fn: () => Promise<T>, updateOnNotification?: Array<NotificationType>): HookResponse<T> { const [result, setHookResponse] = useState<HookResponse<T>>(undefined); useEffect(() => { async function doAsync() { @@ -43,6 +44,11 @@ export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> { } } doAsync(); + if (updateOnNotification && updateOnNotification.length > 0) { + return wxApi.onUpdateNotification(updateOnNotification, () => { + doAsync() + }); + } }, []); return result; } diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.ts b/packages/taler-wallet-webextension/src/hooks/useBalances.ts deleted file mode 100644 index 403ce7b87..000000000 --- a/packages/taler-wallet-webextension/src/hooks/useBalances.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { BalancesResponse } from "@gnu-taler/taler-util"; -import { useEffect, useState } from "preact/hooks"; -import * as wxApi from "../wxApi"; - -interface BalancesHookOk { - hasError: false; - response: BalancesResponse; -} - -interface BalancesHookError { - hasError: true; - message: string; -} - -export type BalancesHook = BalancesHookOk | BalancesHookError | undefined; - -export function useBalances(): BalancesHook { - const [balance, setBalance] = useState<BalancesHook>(undefined); - useEffect(() => { - async function checkBalance() { - try { - const response = await wxApi.getBalance(); - console.log("got balance", balance); - setBalance({ hasError: false, response }); - } catch (e) { - console.error("could not retrieve balances", e); - if (e instanceof Error) { - setBalance({ hasError: true, message: e.message }); - } - } - } - checkBalance(); - return wxApi.onUpdateNotification(checkBalance); - }, []); - - return balance; -} diff --git a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx deleted file mode 100644 index 232b0da73..000000000 --- a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; -import { addDays } from "date-fns"; -import { BackupView as TestedComponent } from "./BackupPage"; -import { createExample } from "../test-utils"; - -export default { - title: "popup/backup/list", - component: TestedComponent, - argTypes: { - onRetry: { action: "onRetry" }, - onDelete: { action: "onDelete" }, - onBack: { action: "onBack" }, - }, -}; - -export const LotOfProviders = createExample(TestedComponent, { - providers: [ - { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - terms: { - annualFee: "ARS:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: addDays(new Date(), 13).getTime(), - }, - }, - terms: { - annualFee: "ARS:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.Pending, - }, - terms: { - annualFee: "KUDOS:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.InsufficientBalance, - }, - terms: { - annualFee: "KUDOS:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.TermsChanged, - newTerms: { - annualFee: "USD:2", - storageLimitInMegabytes: 8, - supportedProtocolVersion: "2", - }, - oldTerms: { - annualFee: "USD:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "1", - }, - paidUntil: { - t_ms: "never", - }, - }, - terms: { - annualFee: "KUDOS:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.Unpaid, - }, - terms: { - annualFee: "KUDOS:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.Unpaid, - }, - terms: { - annualFee: "KUDOS:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - ], -}); - -export const OneProvider = createExample(TestedComponent, { - providers: [ - { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - terms: { - annualFee: "ARS:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - ], -}); - -export const Empty = createExample(TestedComponent, { - providers: [], -}); diff --git a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx deleted file mode 100644 index ae93f8a40..000000000 --- a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ - -import { i18n, Timestamp } from "@gnu-taler/taler-util"; -import { - ProviderInfo, - ProviderPaymentStatus, -} from "@gnu-taler/taler-wallet-core"; -import { - differenceInMonths, - formatDuration, - intervalToDuration, -} from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { - BoldLight, - ButtonPrimary, - ButtonSuccess, - Centered, - CenteredBoldText, - CenteredText, - PopupBox, - RowBorderGray, - SmallLightText, - SmallText, -} from "../components/styled"; -import { useBackupStatus } from "../hooks/useBackupStatus"; -import { Pages } from "../NavigationBar"; - -interface Props { - onAddProvider: () => void; -} - -export function BackupPage({ onAddProvider }: Props): VNode { - const status = useBackupStatus(); - if (!status) { - return <div>Loading...</div>; - } - return ( - <BackupView - providers={status.providers} - onAddProvider={onAddProvider} - onSyncAll={status.sync} - /> - ); -} - -export interface ViewProps { - providers: ProviderInfo[]; - onAddProvider: () => void; - onSyncAll: () => Promise<void>; -} - -export function BackupView({ - providers, - onAddProvider, - onSyncAll, -}: ViewProps): VNode { - return ( - <PopupBox> - <section> - {providers.map((provider, idx) => ( - <BackupLayout - key={idx} - status={provider.paymentStatus} - timestamp={provider.lastSuccessfulBackupTimestamp} - id={provider.syncProviderBaseUrl} - active={provider.active} - title={provider.name} - /> - ))} - {!providers.length && ( - <Centered style={{ marginTop: 100 }}> - <BoldLight>No backup providers configured</BoldLight> - <ButtonSuccess onClick={onAddProvider}> - <i18n.Translate>Add provider</i18n.Translate> - </ButtonSuccess> - </Centered> - )} - </section> - {!!providers.length && ( - <footer> - <div /> - <div> - <ButtonPrimary onClick={onSyncAll}> - {providers.length > 1 ? ( - <i18n.Translate>Sync all backups</i18n.Translate> - ) : ( - <i18n.Translate>Sync now</i18n.Translate> - )} - </ButtonPrimary> - <ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess> - </div> - </footer> - )} - </PopupBox> - ); -} - -interface TransactionLayoutProps { - status: ProviderPaymentStatus; - timestamp?: Timestamp; - title: string; - id: string; - active: boolean; -} - -function BackupLayout(props: TransactionLayoutProps): VNode { - const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms); - const dateStr = date?.toLocaleString([], { - dateStyle: "medium", - timeStyle: "short", - } as any); - - return ( - <RowBorderGray> - <div style={{ color: !props.active ? "grey" : undefined }}> - <a - href={Pages.provider_detail.replace( - ":pid", - encodeURIComponent(props.id), - )} - > - <span>{props.title}</span> - </a> - - {dateStr && ( - <SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText> - )} - {!dateStr && ( - <SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText> - )} - </div> - <div> - {props.status?.type === "paid" ? ( - <ExpirationText until={props.status.paidUntil} /> - ) : ( - <div>{props.status.type}</div> - )} - </div> - </RowBorderGray> - ); -} - -function ExpirationText({ until }: { until: Timestamp }) { - return ( - <Fragment> - <CenteredText> Expires in </CenteredText> - <CenteredBoldText {...{ color: colorByTimeToExpire(until) }}> - {" "} - {daysUntil(until)}{" "} - </CenteredBoldText> - </Fragment> - ); -} - -function colorByTimeToExpire(d: Timestamp) { - if (d.t_ms === "never") return "rgb(28, 184, 65)"; - const months = differenceInMonths(d.t_ms, new Date()); - return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)"; -} - -function daysUntil(d: Timestamp) { - if (d.t_ms === "never") return undefined; - const duration = intervalToDuration({ - start: d.t_ms, - end: new Date(), - }); - const str = formatDuration(duration, { - delimiter: ", ", - format: [ - duration?.years - ? "years" - : duration?.months - ? "months" - : duration?.days - ? "days" - : duration.hours - ? "hours" - : "minutes", - ], - }); - return `${str}`; -} diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx index 80203f6d3..a4988cf2d 100644 --- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx @@ -158,7 +158,7 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, { requiresUserInput: false, }, { - available: "COL:2000", + available: "TESTKUDOS:2000", hasPendingTransactions: false, pendingIncoming: "USD:0", pendingOutgoing: "USD:0", diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index a23c81cd1..008f30cb6 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -14,194 +14,77 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - amountFractionalBase, - Amounts, - Balance, - i18n, -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { - ButtonPrimary, - ErrorBox, - Middle, - PopupBox, -} from "../components/styled/index"; -import { BalancesHook, useBalances } from "../hooks/useBalances"; -import { PageLink, renderAmount } from "../renderHtml"; +import { BalancesResponse, i18n } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { BalanceTable } from "../components/BalanceTable"; +import { ButtonPrimary, ErrorBox } from "../components/styled/index"; +import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { PageLink } from "../renderHtml"; +import * as wxApi from "../wxApi"; export function BalancePage({ goToWalletManualWithdraw, }: { goToWalletManualWithdraw: () => void; }): VNode { - const balance = useBalances(); + const state = useAsyncAsHook(wxApi.getBalance); return ( <BalanceView - balance={balance} + balance={state} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} /> ); } export interface BalanceViewProps { - balance: BalancesHook; + balance: HookResponse<BalancesResponse>; Linker: typeof PageLink; goToWalletManualWithdraw: () => void; } -function formatPending(entry: Balance): VNode { - let incoming: VNode | undefined; - let payment: VNode | undefined; - - // const available = Amounts.parseOrThrow(entry.available); - const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming); - const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing); - - if (!Amounts.isZero(pendingIncoming)) { - incoming = ( - <span> - <i18n.Translate> - <span style={{ color: "darkgreen" }} title="incoming amount"> - {"+"} - {renderAmount(entry.pendingIncoming)} - </span>{" "} - </i18n.Translate> - </span> - ); - } - if (!Amounts.isZero(pendingOutgoing)) { - payment = ( - <span> - <i18n.Translate> - <span style={{ color: "darkred" }} title="outgoing amount"> - {"-"} - {renderAmount(entry.pendingOutgoing)} - </span>{" "} - </i18n.Translate> - </span> - ); - } - - const l = [incoming, payment].filter((x) => x !== undefined); - if (l.length === 0) { - return <span />; - } - - if (l.length === 1) { - return <span>{l}</span>; - } - return ( - <span> - {l[0]}, {l[1]} - </span> - ); -} - export function BalanceView({ balance, Linker, goToWalletManualWithdraw, }: BalanceViewProps): VNode { - function Content(): VNode { - if (!balance) { - return <span />; - } + if (!balance) { + return <div>Loading...</div>; + } - if (balance.hasError) { - return ( - <section> - <ErrorBox>{balance.message}</ErrorBox> - <p> - Click <Linker pageName="welcome">here</Linker> for help and - diagnostics. - </p> - </section> - ); - } - if (balance.response.balances.length === 0) { - return ( - <section data-expanded> - <Middle> - <p> - <i18n.Translate> - You have no balance to show. Need some{" "} - <Linker pageName="/welcome">help</Linker> getting started? - </i18n.Translate> - </p> - </Middle> - </section> - ); - } + if (balance.hasError) { return ( - <section data-expanded data-centered> - <table style={{ width: "100%" }}> - {balance.response.balances.map((entry, idx) => { - const av = Amounts.parseOrThrow(entry.available); - // Create our number formatter. - let formatter; - try { - formatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: av.currency, - currencyDisplay: "symbol", - // These options are needed to round to whole numbers if that's what you want. - //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) - //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) - }); - } catch { - formatter = new Intl.NumberFormat("en-US", { - // style: 'currency', - // currency: av.currency, - // These options are needed to round to whole numbers if that's what you want. - //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) - //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) - }); - } - - const v = formatter.format( - av.value + av.fraction / amountFractionalBase, - ); - const fontSize = - v.length < 8 ? "3em" : v.length < 13 ? "2em" : "1em"; - return ( - <tr key={idx}> - <td - style={{ - height: 50, - fontSize, - width: "60%", - textAlign: "right", - padding: 0, - }} - > - {v} - </td> - <td style={{ maxWidth: "2em", overflowX: "hidden" }}> - {av.currency} - </td> - <td style={{ fontSize: "small", color: "gray" }}> - {formatPending(entry)} - </td> - </tr> - ); - })} - </table> - </section> + <Fragment> + <ErrorBox>{balance.message}</ErrorBox> + <p> + Click <Linker pageName="welcome">here</Linker> for help and + diagnostics. + </p> + </Fragment> + ); + } + if (balance.response.balances.length === 0) { + return ( + <Fragment> + <p> + <i18n.Translate> + You have no balance to show. Need some{" "} + <Linker pageName="/welcome">help</Linker> getting started? + </i18n.Translate> + </p> + </Fragment> ); } return ( - <PopupBox> - {/* <section> */} - <Content /> - {/* </section> */} - <footer> - <div /> + <Fragment> + <section> + <BalanceTable balances={balance.response.balances} /> + </section> + <footer style={{ justifyContent: "space-around" }}> <ButtonPrimary onClick={goToWalletManualWithdraw}> Withdraw </ButtonPrimary> </footer> - </PopupBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx index b0e8543fc..8b5d41657 100644 --- a/packages/taler-wallet-webextension/src/popup/Debug.tsx +++ b/packages/taler-wallet-webextension/src/popup/Debug.tsx @@ -16,7 +16,7 @@ import { h, VNode } from "preact"; import { Diagnostics } from "../components/Diagnostics"; -import { useDiagnostics } from "../hooks/useDiagnostics.js"; +import { useDiagnostics } from "../hooks/useDiagnostics"; import * as wxApi from "../wxApi"; export function DeveloperPage(): VNode { diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx index 95f4a547a..43d39da82 100644 --- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.stories.tsx @@ -55,6 +55,7 @@ const exampleData = { type: TransactionType.Withdrawal, exchangeBaseUrl: "http://exchange.demo.taler.net", withdrawalDetails: { + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", confirmed: false, exchangePaytoUris: ["payto://x-taler-bank/bank/account"], type: WithdrawalType.ManualTransfer, diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx index 2228271dc..b23b4781f 100644 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -21,18 +21,18 @@ import { Transaction, TransactionsResponse, } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { PopupBox } from "../components/styled"; import { TransactionItem } from "../components/TransactionItem"; -import { useBalances } from "../hooks/useBalances"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import * as wxApi from "../wxApi"; export function HistoryPage(): VNode { const [transactions, setTransactions] = useState< TransactionsResponse | undefined >(undefined); - const balance = useBalances(); + const balance = useAsyncAsHook(wxApi.getBalance); const balanceWithoutError = balance?.hasError ? [] : balance?.response.balances || []; @@ -57,7 +57,7 @@ export function HistoryPage(): VNode { ); } -function amountToString(c: AmountString) { +function amountToString(c: AmountString): string { const idx = c.indexOf(":"); return `${c.substring(idx + 1)} ${c.substring(0, idx)}`; } @@ -68,18 +68,18 @@ export function HistoryView({ }: { list: Transaction[]; balances: Balance[]; -}) { +}): VNode { const multiCurrency = balances.length > 1; return ( - <PopupBox noPadding> + <Fragment> {balances.length > 0 && ( <header> {multiCurrency ? ( <div class="title"> Balance:{" "} <ul style={{ margin: 0 }}> - {balances.map((b) => ( - <li>{b.available}</li> + {balances.map((b, i) => ( + <li key={i}>{b.available}</li> ))} </ul> </div> @@ -113,8 +113,10 @@ export function HistoryView({ rel="noopener noreferrer" style={{ color: "darkgreen", textDecoration: "none" }} href={ + // eslint-disable-next-line no-undef chrome.extension - ? chrome.extension.getURL(`/static/wallet.html#/history`) + ? // eslint-disable-next-line no-undef + chrome.extension.getURL(`/static/wallet.html#/history`) : "#" } > @@ -122,6 +124,6 @@ export function HistoryView({ </a> )} </footer> - </PopupBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx deleted file mode 100644 index 0cff7f75f..000000000 --- a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { createExample } from "../test-utils"; -import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage"; - -export default { - title: "popup/backup/confirm", - component: TestedComponent, - argTypes: { - onRetry: { action: "onRetry" }, - onDelete: { action: "onDelete" }, - onBack: { action: "onBack" }, - }, -}; - -export const DemoService = createExample(TestedComponent, { - url: "https://sync.demo.taler.net/", - provider: { - annual_fee: "KUDOS:0.1", - storage_limit_in_megabytes: 20, - supported_protocol_version: "1", - }, -}); - -export const FreeService = createExample(TestedComponent, { - url: "https://sync.taler:9667/", - provider: { - annual_fee: "ARS:0", - storage_limit_in_megabytes: 20, - supported_protocol_version: "1", - }, -}); diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx deleted file mode 100644 index 55686ee97..000000000 --- a/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx +++ /dev/null @@ -1,244 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -import { - Amounts, - BackupBackupProviderTerms, - canonicalizeBaseUrl, - i18n, -} from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { Checkbox } from "../components/Checkbox"; -import { ErrorMessage } from "../components/ErrorMessage"; -import { - Button, - ButtonPrimary, - Input, - LightText, - PopupBox, - SmallLightText, -} from "../components/styled/index"; -import * as wxApi from "../wxApi"; - -interface Props { - currency: string; - onBack: () => void; -} - -function getJsonIfOk(r: Response) { - if (r.ok) { - return r.json(); - } else { - if (r.status >= 400 && r.status < 500) { - throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`); - } else { - throw new Error( - `Try another server: (${r.status}) ${ - r.statusText || "internal server error" - }`, - ); - } - } -} - -export function ProviderAddPage({ onBack }: Props): VNode { - const [verifying, setVerifying] = useState< - | { url: string; name: string; provider: BackupBackupProviderTerms } - | undefined - >(undefined); - - async function getProviderInfo( - url: string, - ): Promise<BackupBackupProviderTerms> { - return fetch(`${url}config`) - .catch((e) => { - throw new Error(`Network error`); - }) - .then(getJsonIfOk); - } - - if (!verifying) { - return ( - <SetUrlView - onCancel={onBack} - onVerify={(url) => getProviderInfo(url)} - onConfirm={(url, name) => - getProviderInfo(url) - .then((provider) => { - setVerifying({ url, name, provider }); - }) - .catch((e) => e.message) - } - /> - ); - } - return ( - <ConfirmProviderView - provider={verifying.provider} - url={verifying.url} - onCancel={() => { - setVerifying(undefined); - }} - onConfirm={() => { - wxApi.addBackupProvider(verifying.url, verifying.name).then(onBack); - }} - /> - ); -} - -export interface SetUrlViewProps { - initialValue?: string; - onCancel: () => void; - onVerify: (s: string) => Promise<BackupBackupProviderTerms | undefined>; - onConfirm: (url: string, name: string) => Promise<string | undefined>; - withError?: string; -} - -export function SetUrlView({ - initialValue, - onCancel, - onVerify, - onConfirm, - withError, -}: SetUrlViewProps) { - const [value, setValue] = useState<string>(initialValue || ""); - const [urlError, setUrlError] = useState(false); - const [name, setName] = useState<string | undefined>(undefined); - const [error, setError] = useState<string | undefined>(withError); - useEffect(() => { - try { - const url = canonicalizeBaseUrl(value); - onVerify(url) - .then((r) => { - setUrlError(false); - setName(new URL(url).hostname); - }) - .catch(() => { - setUrlError(true); - setName(undefined); - }); - } catch { - setUrlError(true); - setName(undefined); - } - }, [value]); - return ( - <PopupBox> - <section> - <h1> Add backup provider</h1> - <ErrorMessage - title={error && "Could not get provider information"} - description={error} - /> - <LightText> Backup providers may charge for their service</LightText> - <p> - <Input invalid={urlError}> - <label>URL</label> - <input - type="text" - placeholder="https://" - value={value} - onChange={(e) => setValue(e.currentTarget.value)} - /> - </Input> - <Input> - <label>Name</label> - <input - type="text" - disabled={name === undefined} - value={name} - onChange={(e) => setName(e.currentTarget.value)} - /> - </Input> - </p> - </section> - <footer> - <Button onClick={onCancel}> - <i18n.Translate> < Back</i18n.Translate> - </Button> - <ButtonPrimary - disabled={!value && !urlError} - onClick={() => { - const url = canonicalizeBaseUrl(value); - return onConfirm(url, name!).then((r) => - r ? setError(r) : undefined, - ); - }} - > - <i18n.Translate>Next</i18n.Translate> - </ButtonPrimary> - </footer> - </PopupBox> - ); -} - -export interface ConfirmProviderViewProps { - provider: BackupBackupProviderTerms; - url: string; - onCancel: () => void; - onConfirm: () => void; -} -export function ConfirmProviderView({ - url, - provider, - onCancel, - onConfirm, -}: ConfirmProviderViewProps) { - const [accepted, setAccepted] = useState(false); - - return ( - <PopupBox> - <section> - <h1>Review terms of service</h1> - <div> - Provider URL:{" "} - <a href={url} target="_blank"> - {url} - </a> - </div> - <SmallLightText> - Please review and accept this provider's terms of service - </SmallLightText> - <h2>1. Pricing</h2> - <p> - {Amounts.isZero(provider.annual_fee) - ? "free of charge" - : `${provider.annual_fee} per year of service`} - </p> - <h2>2. Storage</h2> - <p> - {provider.storage_limit_in_megabytes} megabytes of storage per year of - service - </p> - <Checkbox - label="Accept terms of service" - name="terms" - onToggle={() => setAccepted((old) => !old)} - enabled={accepted} - /> - </section> - <footer> - <Button onClick={onCancel}> - <i18n.Translate> < Back</i18n.Translate> - </Button> - <ButtonPrimary disabled={!accepted} onClick={onConfirm}> - <i18n.Translate>Add provider</i18n.Translate> - </ButtonPrimary> - </footer> - </PopupBox> - ); -} diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx deleted file mode 100644 index 9a2f97051..000000000 --- a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { createExample } from "../test-utils"; -import { SetUrlView as TestedComponent } from "./ProviderAddPage"; - -export default { - title: "popup/backup/add", - component: TestedComponent, - argTypes: { - onRetry: { action: "onRetry" }, - onDelete: { action: "onDelete" }, - onBack: { action: "onBack" }, - }, -}; - -export const Initial = createExample(TestedComponent, {}); - -export const WithValue = createExample(TestedComponent, { - initialValue: "sync.demo.taler.net", -}); - -export const WithConnectionError = createExample(TestedComponent, { - withError: "Network error", -}); - -export const WithClientError = createExample(TestedComponent, { - withError: "URL may not be right: (404) Not Found", -}); - -export const WithServerError = createExample(TestedComponent, { - withError: "Try another server: (500) Internal Server Error", -}); diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx deleted file mode 100644 index fab21398a..000000000 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx +++ /dev/null @@ -1,235 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; -import { createExample } from "../test-utils"; -import { ProviderView as TestedComponent } from "./ProviderDetailPage"; - -export default { - title: "popup/backup/details", - component: TestedComponent, - argTypes: { - onRetry: { action: "onRetry" }, - onDelete: { action: "onDelete" }, - onBack: { action: "onBack" }, - }, -}; - -export const Active = createExample(TestedComponent, { - info: { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - terms: { - annualFee: "EUR:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const ActiveErrorSync = createExample(TestedComponent, { - info: { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - lastAttemptedBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - lastError: { - code: 2002, - details: "details", - hint: "error hint from the server", - message: "message", - }, - terms: { - annualFee: "EUR:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const ActiveBackupProblemUnreadable = createExample(TestedComponent, { - info: { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - backupProblem: { - type: "backup-unreadable", - }, - terms: { - annualFee: "EUR:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const ActiveBackupProblemDevice = createExample(TestedComponent, { - info: { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.taler:9967/", - lastSuccessfulBackupTimestamp: { - t_ms: 1625063925078, - }, - paymentProposalIds: [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", - ], - paymentStatus: { - type: ProviderPaymentType.Paid, - paidUntil: { - t_ms: 1656599921000, - }, - }, - backupProblem: { - type: "backup-conflicting-device", - myDeviceId: "my-device-id", - otherDeviceId: "other-device-id", - backupTimestamp: { - t_ms: 1656599921000, - }, - }, - terms: { - annualFee: "EUR:1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const InactiveUnpaid = createExample(TestedComponent, { - info: { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.Unpaid, - }, - terms: { - annualFee: "EUR:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const InactiveInsufficientBalance = createExample(TestedComponent, { - info: { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.InsufficientBalance, - }, - terms: { - annualFee: "EUR:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const InactivePending = createExample(TestedComponent, { - info: { - active: false, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.Pending, - }, - terms: { - annualFee: "EUR:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); - -export const ActiveTermsChanged = createExample(TestedComponent, { - info: { - active: true, - name: "sync.demo", - syncProviderBaseUrl: "http://sync.demo.taler.net/", - paymentProposalIds: [], - paymentStatus: { - type: ProviderPaymentType.TermsChanged, - paidUntil: { - t_ms: 1656599921000, - }, - newTerms: { - annualFee: "EUR:10", - storageLimitInMegabytes: 8, - supportedProtocolVersion: "0.0", - }, - oldTerms: { - annualFee: "EUR:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, - terms: { - annualFee: "EUR:0.1", - storageLimitInMegabytes: 16, - supportedProtocolVersion: "0.0", - }, - }, -}); diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx deleted file mode 100644 index 9617c9a41..000000000 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx +++ /dev/null @@ -1,278 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ - -import { i18n, Timestamp } from "@gnu-taler/taler-util"; -import { - ProviderInfo, - ProviderPaymentStatus, - ProviderPaymentType, -} from "@gnu-taler/taler-wallet-core"; -import { format, formatDuration, intervalToDuration } from "date-fns"; -import { Fragment, VNode, h } from "preact"; -import { ErrorMessage } from "../components/ErrorMessage"; -import { - Button, - ButtonDestructive, - ButtonPrimary, - PaymentStatus, - PopupBox, - SmallLightText, -} from "../components/styled"; -import { useProviderStatus } from "../hooks/useProviderStatus"; - -interface Props { - pid: string; - onBack: () => void; -} - -export function ProviderDetailPage({ pid, onBack }: Props): VNode { - const status = useProviderStatus(pid); - if (!status) { - return ( - <div> - <i18n.Translate>Loading...</i18n.Translate> - </div> - ); - } - if (!status.info) { - onBack(); - return <div />; - } - return ( - <ProviderView - info={status.info} - onSync={status.sync} - onDelete={() => status.remove().then(onBack)} - onBack={onBack} - onExtend={() => { - null; - }} - /> - ); -} - -export interface ViewProps { - info: ProviderInfo; - onDelete: () => void; - onSync: () => void; - onBack: () => void; - onExtend: () => void; -} - -export function ProviderView({ - info, - onDelete, - onSync, - onBack, - onExtend, -}: ViewProps): VNode { - const lb = info?.lastSuccessfulBackupTimestamp; - const isPaid = - info.paymentStatus.type === ProviderPaymentType.Paid || - info.paymentStatus.type === ProviderPaymentType.TermsChanged; - return ( - <PopupBox> - <Error info={info} /> - <header> - <h3> - {info.name}{" "} - <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText> - </h3> - <PaymentStatus color={isPaid ? "rgb(28, 184, 65)" : "rgb(202, 60, 60)"}> - {isPaid ? "Paid" : "Unpaid"} - </PaymentStatus> - </header> - <section> - <p> - <b>Last backup:</b>{" "} - {lb == null || lb.t_ms == "never" - ? "never" - : format(lb.t_ms, "dd MMM yyyy")}{" "} - </p> - <ButtonPrimary onClick={onSync}> - <i18n.Translate>Back up</i18n.Translate> - </ButtonPrimary> - {info.terms && ( - <Fragment> - <p> - <b>Provider fee:</b> {info.terms && info.terms.annualFee} per year - </p> - </Fragment> - )} - <p>{descriptionByStatus(info.paymentStatus)}</p> - <ButtonPrimary disabled onClick={onExtend}> - <i18n.Translate>Extend</i18n.Translate> - </ButtonPrimary> - - {info.paymentStatus.type === ProviderPaymentType.TermsChanged && ( - <div> - <p> - <i18n.Translate> - terms has changed, extending the service will imply accepting - the new terms of service - </i18n.Translate> - </p> - <table> - <thead> - <tr> - <td></td> - <td> - <i18n.Translate>old</i18n.Translate> - </td> - <td> -></td> - <td> - <i18n.Translate>new</i18n.Translate> - </td> - </tr> - </thead> - <tbody> - <tr> - <td> - <i18n.Translate>fee</i18n.Translate> - </td> - <td>{info.paymentStatus.oldTerms.annualFee}</td> - <td>-></td> - <td>{info.paymentStatus.newTerms.annualFee}</td> - </tr> - <tr> - <td> - <i18n.Translate>storage</i18n.Translate> - </td> - <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td> - <td>-></td> - <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td> - </tr> - </tbody> - </table> - </div> - )} - </section> - <footer> - <Button onClick={onBack}> - <i18n.Translate> < back</i18n.Translate> - </Button> - <div> - <ButtonDestructive onClick={onDelete}> - <i18n.Translate>remove provider</i18n.Translate> - </ButtonDestructive> - </div> - </footer> - </PopupBox> - ); -} - -function daysSince(d?: Timestamp) { - if (!d || d.t_ms === "never") return "never synced"; - const duration = intervalToDuration({ - start: d.t_ms, - end: new Date(), - }); - const str = formatDuration(duration, { - delimiter: ", ", - format: [ - duration?.years - ? i18n.str`years` - : duration?.months - ? i18n.str`months` - : duration?.days - ? i18n.str`days` - : duration?.hours - ? i18n.str`hours` - : duration?.minutes - ? i18n.str`minutes` - : i18n.str`seconds`, - ], - }); - return `synced ${str} ago`; -} - -function Error({ info }: { info: ProviderInfo }) { - if (info.lastError) { - return <ErrorMessage title={info.lastError.hint} />; - } - if (info.backupProblem) { - switch (info.backupProblem.type) { - case "backup-conflicting-device": - return ( - <ErrorMessage - title={ - <Fragment> - <i18n.Translate> - There is conflict with another backup from{" "} - <b>{info.backupProblem.otherDeviceId}</b> - </i18n.Translate> - </Fragment> - } - /> - ); - case "backup-unreadable": - return <ErrorMessage title="Backup is not readable" />; - default: - return ( - <ErrorMessage - title={ - <Fragment> - <i18n.Translate> - Unknown backup problem: {JSON.stringify(info.backupProblem)} - </i18n.Translate> - </Fragment> - } - /> - ); - } - } - return null; -} - -function colorByStatus(status: ProviderPaymentType) { - switch (status) { - case ProviderPaymentType.InsufficientBalance: - return "rgb(223, 117, 20)"; - case ProviderPaymentType.Unpaid: - return "rgb(202, 60, 60)"; - case ProviderPaymentType.Paid: - return "rgb(28, 184, 65)"; - case ProviderPaymentType.Pending: - return "gray"; - case ProviderPaymentType.InsufficientBalance: - return "rgb(202, 60, 60)"; - case ProviderPaymentType.TermsChanged: - return "rgb(202, 60, 60)"; - } -} - -function descriptionByStatus(status: ProviderPaymentStatus) { - switch (status.type) { - // return i18n.str`no enough balance to make the payment` - // return i18n.str`not paid yet` - case ProviderPaymentType.Paid: - case ProviderPaymentType.TermsChanged: - if (status.paidUntil.t_ms === "never") { - return i18n.str`service paid`; - } else { - return ( - <Fragment> - <b>Backup valid until:</b>{" "} - {format(status.paidUntil.t_ms, "dd MMM yyyy")} - </Fragment> - ); - } - case ProviderPaymentType.Unpaid: - case ProviderPaymentType.InsufficientBalance: - case ProviderPaymentType.Pending: - return ""; - } -} diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx index 3b83f0762..0a3f777d5 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -14,26 +14,34 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { i18n } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; +import { ExchangeListItem, i18n } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; import { Checkbox } from "../components/Checkbox"; -import { EditableText } from "../components/EditableText"; -import { SelectList } from "../components/SelectList"; -import { PopupBox } from "../components/styled"; +import { ButtonPrimary } from "../components/styled"; import { useDevContext } from "../context/devContext"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { useLang } from "../hooks/useLang"; +// import { strings as messages } from "../i18n/strings"; +import * as wxApi from "../wxApi"; export function SettingsPage(): VNode { const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); const { devMode, toggleDevMode } = useDevContext(); const { name, update } = useBackupDeviceName(); const [lang, changeLang] = useLang(); + const exchangesHook = useAsyncAsHook(wxApi.listExchanges); + return ( <SettingsView lang={lang} changeLang={changeLang} + knownExchanges={ + !exchangesHook || exchangesHook.hasError + ? [] + : exchangesHook.response.exchanges + } deviceName={name} setDeviceName={update} permissionsEnabled={permissionsEnabled} @@ -53,36 +61,59 @@ export interface ViewProps { togglePermissions: () => void; developerMode: boolean; toggleDeveloperMode: () => void; + knownExchanges: Array<ExchangeListItem>; } -import { strings as messages } from "../i18n/strings"; - -type LangsNames = { - [P in keyof typeof messages]: string; -}; +// type LangsNames = { +// [P in keyof typeof messages]: string; +// }; -const names: LangsNames = { - es: "Español [es]", - en: "English [en]", - fr: "Français [fr]", - de: "Deutsch [de]", - sv: "Svenska [sv]", - it: "Italiano [it]", -}; +// const names: LangsNames = { +// es: "Español [es]", +// en: "English [en]", +// fr: "Français [fr]", +// de: "Deutsch [de]", +// sv: "Svenska [sv]", +// it: "Italiano [it]", +// }; export function SettingsView({ - lang, - changeLang, - deviceName, - setDeviceName, + knownExchanges, + // lang, + // changeLang, + // deviceName, + // setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode, }: ViewProps): VNode { return ( - <PopupBox> + <Fragment> <section> + <h2> + <i18n.Translate>Known exchanges</i18n.Translate> + </h2> + {!knownExchanges || !knownExchanges.length ? ( + <div>No exchange yet!</div> + ) : ( + <Fragment> + <table> + {knownExchanges.map((e, idx) => ( + <tr key={idx}> + <td>{e.currency}</td> + <td> + <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a> + </td> + </tr> + ))} + </table> + </Fragment> + )} + <div style={{ display: "flex", justifyContent: "space-between" }}> + <div /> + <ButtonPrimary>Manage exchange</ButtonPrimary> + </div> {/* <h2><i18n.Translate>Wallet</i18n.Translate></h2> */} {/* <SelectList value={lang} @@ -124,14 +155,16 @@ export function SettingsView({ rel="noopener noreferrer" style={{ color: "darkgreen", textDecoration: "none" }} href={ + // eslint-disable-next-line no-undef chrome.extension - ? chrome.extension.getURL(`/static/wallet.html#/settings`) + ? // eslint-disable-next-line no-undef + chrome.extension.getURL(`/static/wallet.html#/settings`) : "#" } > VIEW MORE SETTINGS </a> </footer> - </PopupBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx index cbdcbeb15..b2220e37b 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx @@ -20,12 +20,8 @@ */ import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util"; -import { - ButtonPrimary, - ButtonSuccess, - PopupBox, -} from "../components/styled/index"; -import { h } from "preact"; +import { Fragment, h } from "preact"; +import { ButtonPrimary, ButtonSuccess } from "../components/styled/index"; export interface Props { url: string; @@ -35,7 +31,7 @@ export interface Props { export function TalerActionFound({ url, onDismiss }: Props) { const uriType = classifyTalerUri(url); return ( - <PopupBox> + <Fragment> <section> <h1>Taler Action </h1> {uriType === TalerUriType.TalerPay && ( @@ -109,7 +105,7 @@ export function TalerActionFound({ url, onDismiss }: Props) { <div /> <ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary> </footer> - </PopupBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index a5723ccb5..d0c79f6d4 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -25,16 +25,17 @@ import { createHashHistory } from "history"; import { render, h } from "preact"; import Router, { route, Route } from "preact-router"; import { useEffect } from "preact/hooks"; +import { PopupBox } from "./components/styled"; import { DevContextProvider } from "./context/devContext"; import { useTalerActionURL } from "./hooks/useTalerActionURL"; import { strings } from "./i18n/strings"; import { Pages, WalletNavBar } from "./NavigationBar"; -import { BackupPage } from "./popup/BackupPage"; +import { BackupPage } from "./wallet/BackupPage"; import { BalancePage } from "./popup/BalancePage"; import { DeveloperPage } from "./popup/Debug"; import { HistoryPage } from "./popup/History"; -import { ProviderAddPage } from "./popup/ProviderAddPage"; -import { ProviderDetailPage } from "./popup/ProviderDetailPage"; +import { ProviderAddPage } from "./wallet/ProviderAddPage"; +import { ProviderDetailPage } from "./wallet/ProviderDetailPage"; import { SettingsPage } from "./popup/Settings"; import { TalerActionFound } from "./popup/TalerActionFound"; @@ -72,7 +73,7 @@ function Application() { <div> <DevContextProvider> <WalletNavBar /> - <div style={{ width: 400, height: 290 }}> + <PopupBox> <Router history={createHashHistory()}> <Route path={Pages.dev} component={DeveloperPage} /> @@ -128,15 +129,17 @@ function Application() { /> <Route default component={Redirect} to={Pages.balance} /> </Router> - </div> + </PopupBox> </DevContextProvider> </div> ); } function goToWalletPage(page: Pages | string): null { + // eslint-disable-next-line no-undef chrome.tabs.create({ active: true, + // eslint-disable-next-line no-undef url: chrome.extension.getURL(`/static/wallet.html#${page}`), }); return null; diff --git a/packages/taler-wallet-webextension/src/svg/index.tsx b/packages/taler-wallet-webextension/src/svg/index.tsx new file mode 100644 index 000000000..34ff7767c --- /dev/null +++ b/packages/taler-wallet-webextension/src/svg/index.tsx @@ -0,0 +1,40 @@ +import { h, VNode } from "preact"; + +export const CopyIcon = (): VNode => ( + <svg + aria-hidden="true" + height="10" + viewBox="0 0 16 16" + version="1.1" + width="10" + data-view-component="true" + class="octicon octicon-copy" + style="display: inline-block;" + > + <path + fill-rule="evenodd" + d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z" + /> + <path + fill-rule="evenodd" + d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z" + /> + </svg> +); + +export const CopiedIcon = (): VNode => ( + <svg + aria-hidden="true" + height="8" + viewBox="0 0 16 16" + version="1.1" + width="8" + data-view-component="true" + class="octicon octicon-check color-fg-success" + > + <path + fill-rule="evenodd" + d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" + /> + </svg> +); diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx index f0ae38e0f..0b0af25ab 100644 --- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx @@ -35,7 +35,6 @@ import { RowBorderGray, SmallLightText, SmallText, - WalletBox, } from "../components/styled"; import { useBackupStatus } from "../hooks/useBackupStatus"; import { Pages } from "../NavigationBar"; @@ -70,7 +69,7 @@ export function BackupView({ onSyncAll, }: ViewProps): VNode { return ( - <WalletBox> + <Fragment> <section> {providers.map((provider, idx) => ( <BackupLayout @@ -106,7 +105,7 @@ export function BackupView({ </div> </footer> )} - </WalletBox> + </Fragment> ); } @@ -155,7 +154,7 @@ function BackupLayout(props: TransactionLayoutProps): VNode { ); } -function ExpirationText({ until }: { until: Timestamp }) { +function ExpirationText({ until }: { until: Timestamp }): VNode { return ( <Fragment> <CenteredText> Expires in </CenteredText> @@ -167,14 +166,14 @@ function ExpirationText({ until }: { until: Timestamp }) { ); } -function colorByTimeToExpire(d: Timestamp) { +function colorByTimeToExpire(d: Timestamp): string { if (d.t_ms === "never") return "rgb(28, 184, 65)"; const months = differenceInMonths(d.t_ms, new Date()); return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)"; } -function daysUntil(d: Timestamp) { - if (d.t_ms === "never") return undefined; +function daysUntil(d: Timestamp): string { + if (d.t_ms === "never") return ""; const duration = intervalToDuration({ start: d.t_ms, end: new Date(), diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx index 9a2847670..04d79a5ea 100644 --- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -14,27 +14,23 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - amountFractionalBase, - Amounts, - Balance, - BalancesResponse, - i18n, -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index"; -import { BalancesHook, useBalances } from "../hooks/useBalances"; -import { PageLink, renderAmount } from "../renderHtml"; +import { BalancesResponse, i18n } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { BalanceTable } from "../components/BalanceTable"; +import { ButtonPrimary, ErrorBox } from "../components/styled/index"; +import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import { PageLink } from "../renderHtml"; +import * as wxApi from "../wxApi"; export function BalancePage({ goToWalletManualWithdraw, }: { goToWalletManualWithdraw: () => void; }): VNode { - const balance = useBalances(); + const state = useAsyncAsHook(wxApi.getBalance); return ( <BalanceView - balance={balance} + balance={state} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} /> @@ -42,7 +38,7 @@ export function BalancePage({ } export interface BalanceViewProps { - balance: BalancesHook; + balance: HookResponse<BalancesResponse>; Linker: typeof PageLink; goToWalletManualWithdraw: () => void; } @@ -53,18 +49,18 @@ export function BalanceView({ goToWalletManualWithdraw, }: BalanceViewProps): VNode { if (!balance) { - return <span />; + return <div>Loading...</div>; } if (balance.hasError) { return ( - <div> - <p>{i18n.str`Error: could not retrieve balance information.`}</p> + <Fragment> + <ErrorBox>{balance.message}</ErrorBox> <p> Click <Linker pageName="welcome">here</Linker> for help and diagnostics. </p> - </div> + </Fragment> ); } if (balance.response.balances.length === 0) { @@ -77,81 +73,17 @@ export function BalanceView({ </p> ); } - return ( - <ShowBalances - wallet={balance.response} - onWithdraw={goToWalletManualWithdraw} - /> - ); -} - -function formatPending(entry: Balance): VNode { - let incoming: VNode | undefined; - let payment: VNode | undefined; - - // const available = Amounts.parseOrThrow(entry.available); - const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming); - // const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing); - - if (!Amounts.isZero(pendingIncoming)) { - incoming = ( - <span> - <i18n.Translate> - <span style={{ color: "darkgreen" }}> - {"+"} - {renderAmount(entry.pendingIncoming)} - </span>{" "} - incoming - </i18n.Translate> - </span> - ); - } - - const l = [incoming, payment].filter((x) => x !== undefined); - if (l.length === 0) { - return <span />; - } - - if (l.length === 1) { - return <span>({l})</span>; - } - return ( - <span> - ({l[0]}, {l[1]}) - </span> - ); -} -function ShowBalances({ - wallet, - onWithdraw, -}: { - wallet: BalancesResponse; - onWithdraw: () => void; -}): VNode { return ( - <WalletBox> + <Fragment> <section> - <Centered> - {wallet.balances.map((entry) => { - const av = Amounts.parseOrThrow(entry.available); - const v = av.value + av.fraction / amountFractionalBase; - return ( - <p key={av.currency}> - <span> - <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "} - <span>{av.currency}</span> - </span> - {formatPending(entry)} - </p> - ); - })} - </Centered> + <BalanceTable balances={balance.response.balances} /> </section> - <footer> - <div /> - <ButtonPrimary onClick={onWithdraw}>Withdraw</ButtonPrimary> + <footer style={{ justifyContent: "space-around" }}> + <ButtonPrimary onClick={goToWalletManualWithdraw}> + Withdraw + </ButtonPrimary> </footer> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx index 300e9cd57..e4955e376 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx @@ -34,6 +34,10 @@ const exchangeList = { "http://exchange.tal": "EUR", }; +export const WithoutAnyExchangeKnown = createExample(TestedComponent, { + exchangeList: {}, +}); + export const InitialState = createExample(TestedComponent, { exchangeList, }); diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 140ac2d40..1bceabd20 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -19,17 +19,19 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { AmountJson, Amounts, i18n } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { ErrorMessage } from "../components/ErrorMessage"; import { SelectList } from "../components/SelectList"; import { + BoldLight, ButtonPrimary, + ButtonSuccess, + Centered, Input, InputWithLabel, LightText, - WalletBox, } from "../components/styled"; export interface Props { @@ -82,11 +84,23 @@ export function CreateManualWithdraw({ } if (!initialExchange) { - return <div>There is no known exchange where to withdraw, add one</div>; + return ( + <Centered style={{ marginTop: 100 }}> + <BoldLight>No exchange configured</BoldLight> + <ButtonSuccess + //FIXME: add exchange feature + onClick={() => { + null; + }} + > + <i18n.Translate>Add exchange</i18n.Translate> + </ButtonSuccess> + </Centered> + ); } return ( - <WalletBox> + <Fragment> <section> <ErrorMessage title={error && "Can't create the reserve"} @@ -145,6 +159,6 @@ export function CreateManualWithdraw({ Start withdrawal </ButtonPrimary> </footer> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 9ae3ac3bd..0f471ac30 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -57,6 +57,7 @@ const exampleData = { type: TransactionType.Withdrawal, exchangeBaseUrl: "http://exchange.demo.taler.net", withdrawalDetails: { + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", confirmed: false, exchangePaytoUris: ["payto://x-taler-bank/bank/account"], type: WithdrawalType.ManualTransfer, diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 6b1a21852..bc8ef734a 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -17,42 +17,37 @@ import { AmountString, Balance, + NotificationType, Transaction, - TransactionsResponse, } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { DateSeparator, WalletBox } from "../components/styled"; +import { DateSeparator } from "../components/styled"; import { Time } from "../components/Time"; import { TransactionItem } from "../components/TransactionItem"; -import { useBalances } from "../hooks/useBalances"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import * as wxApi from "../wxApi"; export function HistoryPage(): VNode { - const [transactions, setTransactions] = useState< - TransactionsResponse | undefined - >(undefined); - const balance = useBalances(); + const balance = useAsyncAsHook(wxApi.getBalance); const balanceWithoutError = balance?.hasError ? [] : balance?.response.balances || []; - useEffect(() => { - const fetchData = async (): Promise<void> => { - const res = await wxApi.getTransactions(); - setTransactions(res); - }; - fetchData(); - }, []); + const transactionQuery = useAsyncAsHook(wxApi.getTransactions, [ + NotificationType.WithdrawGroupFinished, + ]); - if (!transactions) { + if (!transactionQuery) { return <div>Loading ...</div>; } + if (transactionQuery.hasError) { + return <div>There was an error loading the transactions.</div>; + } return ( <HistoryView balances={balanceWithoutError} - list={[...transactions.transactions].reverse()} + list={[...transactionQuery.response.transactions].reverse()} /> ); } @@ -87,7 +82,7 @@ export function HistoryView({ const multiCurrency = balances.length > 1; return ( - <WalletBox noPadding> + <Fragment> {balances.length > 0 && ( <header> {balances.length === 1 && ( @@ -128,6 +123,6 @@ export function HistoryView({ ); })} </section> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx index 1af4e8d8d..88d5f1722 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx @@ -23,9 +23,9 @@ import { AmountJson, Amounts, } from "@gnu-taler/taler-util"; -import { ReserveCreated } from "./ReserveCreated.js"; +import { ReserveCreated } from "./ReserveCreated"; import { route } from "preact-router"; -import { Pages } from "../NavigationBar.js"; +import { Pages } from "../NavigationBar"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; export function ManualWithdrawPage(): VNode { @@ -39,7 +39,7 @@ export function ManualWithdrawPage(): VNode { >(undefined); const [error, setError] = useState<string | undefined>(undefined); - const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges()); + const state = useAsyncAsHook(() => wxApi.listExchanges()); async function doCreate( exchangeBaseUrl: string, @@ -75,10 +75,13 @@ export function ManualWithdrawPage(): VNode { ); } - if (!knownExchangesHook || knownExchangesHook.hasError) { - return <div>No Known exchanges</div>; + if (!state) { + return <div>loading...</div>; } - const exchangeList = knownExchangesHook.response.exchanges.reduce( + if (state.hasError) { + return <div>There was an error getting the known exchanges</div>; + } + const exchangeList = state.response.exchanges.reduce( (p, c) => ({ ...p, [c.exchangeBaseUrl]: c.currency, diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx index 1c7fdc829..41852e38c 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx @@ -20,7 +20,7 @@ import { canonicalizeBaseUrl, i18n, } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Checkbox } from "../components/Checkbox"; import { ErrorMessage } from "../components/ErrorMessage"; @@ -29,7 +29,6 @@ import { ButtonPrimary, Input, LightText, - WalletBox, SmallLightText, } from "../components/styled/index"; import * as wxApi from "../wxApi"; @@ -64,7 +63,7 @@ export function ProviderAddPage({ onBack }: Props): VNode { async function getProviderInfo( url: string, ): Promise<BackupBackupProviderTerms> { - return fetch(`${url}config`) + return fetch(new URL("config", url).href) .catch((e) => { throw new Error(`Network error`); }) @@ -137,7 +136,7 @@ export function SetUrlView({ } }, [value]); return ( - <WalletBox> + <Fragment> <section> <h1> Add backup provider</h1> <ErrorMessage @@ -182,7 +181,7 @@ export function SetUrlView({ <i18n.Translate>Next</i18n.Translate> </ButtonPrimary> </footer> - </WalletBox> + </Fragment> ); } @@ -201,7 +200,7 @@ export function ConfirmProviderView({ const [accepted, setAccepted] = useState(false); return ( - <WalletBox> + <Fragment> <section> <h1>Review terms of service</h1> <div> @@ -239,6 +238,6 @@ export function ConfirmProviderView({ <i18n.Translate>Add provider</i18n.Translate> </ButtonPrimary> </footer> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx index 1c14c6e0a..d14429ee5 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx @@ -28,34 +28,62 @@ import { ButtonPrimary, PaymentStatus, SmallLightText, - WalletBox, } from "../components/styled"; import { Time } from "../components/Time"; -import { useProviderStatus } from "../hooks/useProviderStatus"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; +import * as wxApi from "../wxApi"; interface Props { pid: string; onBack: () => void; } -export function ProviderDetailPage({ pid, onBack }: Props): VNode { - const status = useProviderStatus(pid); - if (!status) { +export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode { + async function getProviderInfo(): Promise<ProviderInfo | null> { + //create a first list of backup info by currency + const status = await wxApi.getBackupInfo(); + + const providers = status.providers.filter( + (p) => p.syncProviderBaseUrl === providerURL, + ); + return providers.length ? providers[0] : null; + } + + const state = useAsyncAsHook(getProviderInfo); + + if (!state) { return ( <div> <i18n.Translate>Loading...</i18n.Translate> </div> ); } - if (!status.info) { + if (state.hasError) { + return ( + <div> + <i18n.Translate> + There was an error loading the provider detail for "{providerURL}" + </i18n.Translate> + </div> + ); + } + + if (state.response === null) { onBack(); - return <div />; + return ( + <div> + <i18n.Translate> + There is not known provider with url "{providerURL}". Redirecting + back... + </i18n.Translate> + </div> + ); } return ( <ProviderView - info={status.info} - onSync={status.sync} - onDelete={() => status.remove().then(onBack)} + info={state.response} + onSync={async () => wxApi.syncOneProvider(providerURL)} + onDelete={async () => wxApi.syncOneProvider(providerURL).then(onBack)} onBack={onBack} onExtend={() => { null; @@ -84,7 +112,7 @@ export function ProviderView({ info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged; return ( - <WalletBox> + <Fragment> <Error info={info} /> <header> <h3> @@ -167,35 +195,10 @@ export function ProviderView({ </ButtonDestructive> </div> </footer> - </WalletBox> + </Fragment> ); } -// function daysSince(d?: Timestamp): string { -// if (!d || d.t_ms === "never") return "never synced"; -// const duration = intervalToDuration({ -// start: d.t_ms, -// end: new Date(), -// }); -// const str = formatDuration(duration, { -// delimiter: ", ", -// format: [ -// duration?.years -// ? i18n.str`years` -// : duration?.months -// ? i18n.str`months` -// : duration?.days -// ? i18n.str`days` -// : duration?.hours -// ? i18n.str`hours` -// : duration?.minutes -// ? i18n.str`minutes` -// : i18n.str`seconds`, -// ], -// }); -// return `synced ${str} ago`; -// } - function Error({ info }: { info: ProviderInfo }): VNode { if (info.lastError) { return <ErrorMessage title={info.lastError.hint} />; @@ -234,23 +237,6 @@ function Error({ info }: { info: ProviderInfo }): VNode { return <Fragment />; } -// function colorByStatus(status: ProviderPaymentType): string { -// switch (status) { -// case ProviderPaymentType.InsufficientBalance: -// return "rgb(223, 117, 20)"; -// case ProviderPaymentType.Unpaid: -// return "rgb(202, 60, 60)"; -// case ProviderPaymentType.Paid: -// return "rgb(28, 184, 65)"; -// case ProviderPaymentType.Pending: -// return "gray"; -// // case ProviderPaymentType.InsufficientBalance: -// // return "rgb(202, 60, 60)"; -// case ProviderPaymentType.TermsChanged: -// return "rgb(202, 60, 60)"; -// } -// } - function descriptionByStatus(status: ProviderPaymentStatus): VNode { switch (status.type) { // return i18n.str`no enough balance to make the payment` diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx index a72026ab8..075126dc8 100644 --- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx @@ -1,18 +1,8 @@ -import { - AmountJson, - Amounts, - parsePaytoUri, - PaytoUri, -} from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; +import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType"; import { QR } from "../components/QR"; -import { - ButtonDestructive, - ButtonPrimary, - WalletBox, - WarningBox, -} from "../components/styled"; +import { ButtonDestructive, WarningBox } from "../components/styled"; export interface Props { reservePub: string; payto: string; @@ -21,92 +11,6 @@ export interface Props { onBack: () => void; } -interface BankDetailsProps { - payto: PaytoUri; - exchangeBaseUrl: string; - subject: string; - amount: string; -} - -function Row({ - name, - value, - literal, -}: { - name: string; - value: string; - literal?: boolean; -}): VNode { - const [copied, setCopied] = useState(false); - function copyText(): void { - navigator.clipboard.writeText(value); - setCopied(true); - } - useEffect(() => { - setTimeout(() => { - setCopied(false); - }, 1000); - }, [copied]); - return ( - <tr> - <td> - {!copied ? ( - <ButtonPrimary small onClick={copyText}> - Copy - </ButtonPrimary> - ) : ( - <ButtonPrimary small disabled> - Copied - </ButtonPrimary> - )} - </td> - <td> - <b>{name}</b> - </td> - {literal ? ( - <td> - <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> - {value} - </pre> - </td> - ) : ( - <td>{value}</td> - )} - </tr> - ); -} - -function BankDetailsByPaytoType({ - payto, - subject, - exchangeBaseUrl, - amount, -}: BankDetailsProps): VNode { - const firstPart = !payto.isKnown ? ( - <Fragment> - <Row name="Account" value={payto.targetPath} /> - <Row name="Exchange" value={exchangeBaseUrl} /> - </Fragment> - ) : payto.targetType === "x-taler-bank" ? ( - <Fragment> - <Row name="Bank host" value={payto.host} /> - <Row name="Bank account" value={payto.account} /> - <Row name="Exchange" value={exchangeBaseUrl} /> - </Fragment> - ) : payto.targetType === "iban" ? ( - <Fragment> - <Row name="IBAN" value={payto.iban} /> - <Row name="Exchange" value={exchangeBaseUrl} /> - </Fragment> - ) : undefined; - return ( - <table> - {firstPart} - <Row name="Amount" value={amount} /> - <Row name="Subject" value={subject} literal /> - </table> - ); -} export function ReserveCreated({ reservePub, payto, @@ -120,11 +24,12 @@ export function ReserveCreated({ return <div>could not parse payto uri from exchange {payto}</div>; } return ( - <WalletBox> + <Fragment> <section> - <h1>Bank transfer details</h1> + <h1>Exchange is ready for withdrawal!</h1> <p> - Please wire <b>{Amounts.stringify(amount)}</b> to: + To complete the process you need to wire{" "} + <b>{Amounts.stringify(amount)}</b> to the exchange bank account </p> <BankDetailsByPaytoType amount={Amounts.stringify(amount)} @@ -132,14 +37,14 @@ export function ReserveCreated({ payto={paytoURI} subject={reservePub} /> - </section> - <section> <p> <WarningBox> Make sure to use the correct subject, otherwise the money will not arrive in this wallet. </WarningBox> </p> + </section> + <section> <p> Alternative, you can also scan this QR code or open{" "} <a href={payto}>this link</a> if you have a banking app installed that @@ -149,8 +54,10 @@ export function ReserveCreated({ </section> <footer> <div /> - <ButtonDestructive onClick={onBack}>Cancel withdraw</ButtonDestructive> + <ButtonDestructive onClick={onBack}> + Cancel withdrawal + </ButtonDestructive> </footer> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 8d8f3cdbc..586d7b53e 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -15,16 +15,15 @@ */ import { ExchangeListItem, i18n } from "@gnu-taler/taler-util"; -import { VNode, h, Fragment } from "preact"; +import { Fragment, h, VNode } from "preact"; import { Checkbox } from "../components/Checkbox"; -import { EditableText } from "../components/EditableText"; -import { SelectList } from "../components/SelectList"; -import { ButtonPrimary, ButtonSuccess, WalletBox } from "../components/styled"; +import { ButtonPrimary } from "../components/styled"; import { useDevContext } from "../context/devContext"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { useLang } from "../hooks/useLang"; +// import { strings as messages } from "../i18n/strings"; import * as wxApi from "../wxApi"; export function SettingsPage(): VNode { @@ -32,7 +31,7 @@ export function SettingsPage(): VNode { const { devMode, toggleDevMode } = useDevContext(); const { name, update } = useBackupDeviceName(); const [lang, changeLang] = useLang(); - const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges()); + const exchangesHook = useAsyncAsHook(wxApi.listExchanges); return ( <SettingsView @@ -65,34 +64,32 @@ export interface ViewProps { knownExchanges: Array<ExchangeListItem>; } -import { strings as messages } from "../i18n/strings"; - -type LangsNames = { - [P in keyof typeof messages]: string; -}; +// type LangsNames = { +// [P in keyof typeof messages]: string; +// }; -const names: LangsNames = { - es: "Español [es]", - en: "English [en]", - fr: "Français [fr]", - de: "Deutsch [de]", - sv: "Svenska [sv]", - it: "Italiano [it]", -}; +// const names: LangsNames = { +// es: "Español [es]", +// en: "English [en]", +// fr: "Français [fr]", +// de: "Deutsch [de]", +// sv: "Svenska [sv]", +// it: "Italiano [it]", +// }; export function SettingsView({ knownExchanges, - lang, - changeLang, - deviceName, - setDeviceName, + // lang, + // changeLang, + // deviceName, + // setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode, }: ViewProps): VNode { return ( - <WalletBox> + <Fragment> <section> <h2> <i18n.Translate>Known exchanges</i18n.Translate> @@ -100,17 +97,23 @@ export function SettingsView({ {!knownExchanges || !knownExchanges.length ? ( <div>No exchange yet!</div> ) : ( - <table> - {knownExchanges.map((e) => ( - <tr> - <td>{e.currency}</td> - <td> - <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a> - </td> - </tr> - ))} - </table> + <Fragment> + <table> + {knownExchanges.map((e, idx) => ( + <tr key={idx}> + <td>{e.currency}</td> + <td> + <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a> + </td> + </tr> + ))} + </table> + </Fragment> )} + <div style={{ display: "flex", justifyContent: "space-between" }}> + <div /> + <ButtonPrimary>Manage exchange</ButtonPrimary> + </div> <h2> <i18n.Translate>Permissions</i18n.Translate> @@ -131,6 +134,6 @@ export function SettingsView({ onToggle={toggleDeveloperMode} /> </section> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index c9a3f47cb..a25e2ca80 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -61,6 +61,7 @@ const exampleData = { exchangeBaseUrl: "http://exchange.taler", withdrawalDetails: { confirmed: false, + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", exchangePaytoUris: ["payto://x-taler-bank/bank/account"], type: WithdrawalType.ManualTransfer, }, @@ -134,10 +135,49 @@ export const WithdrawError = createExample(TestedComponent, { }, }); -export const WithdrawPending = createExample(TestedComponent, { - transaction: { ...exampleData.withdraw, pending: true }, +export const WithdrawPendingManual = createExample(TestedComponent, { + transaction: { + ...exampleData.withdraw, + withdrawalDetails: { + type: WithdrawalType.ManualTransfer, + exchangePaytoUris: ["payto://iban/asdasdasd"], + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", + }, + pending: true, + }, }); +export const WithdrawPendingTalerBankUnconfirmed = createExample( + TestedComponent, + { + transaction: { + ...exampleData.withdraw, + withdrawalDetails: { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: false, + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", + bankConfirmationUrl: "http://bank.demo.taler.net", + }, + pending: true, + }, + }, +); + +export const WithdrawPendingTalerBankConfirmed = createExample( + TestedComponent, + { + transaction: { + ...exampleData.withdraw, + withdrawalDetails: { + type: WithdrawalType.TalerBankIntegrationApi, + confirmed: true, + reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", + }, + pending: true, + }, + }, +); + export const Payment = createExample(TestedComponent, { transaction: exampleData.payment, }); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 1472efb40..02c78320a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -18,62 +18,80 @@ import { AmountLike, Amounts, i18n, + NotificationType, + parsePaytoUri, Transaction, TransactionType, + WithdrawalType, } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; import { route } from "preact-router"; -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import emptyImg from "../../static/img/empty.png"; +import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType"; import { ErrorMessage } from "../components/ErrorMessage"; import { Part } from "../components/Part"; import { Button, ButtonDestructive, ButtonPrimary, + CenteredDialog, + InfoBox, ListOfProducts, + Overlay, RowBorderGray, SmallLightText, - WalletBox, WarningBox, } from "../components/styled"; import { Time } from "../components/Time"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { Pages } from "../NavigationBar"; import * as wxApi from "../wxApi"; export function TransactionPage({ tid }: { tid: string }): VNode { - const [transaction, setTransaction] = useState<Transaction | undefined>( - undefined, - ); + async function getTransaction(): Promise<Transaction> { + const res = await wxApi.getTransactions(); + const ts = res.transactions.filter((t) => t.transactionId === tid); + if (ts.length > 1) throw Error("more than one transaction with this id"); + if (ts.length === 1) { + return ts[0]; + } + throw Error("no transaction found"); + } - useEffect(() => { - const fetchData = async (): Promise<void> => { - const res = await wxApi.getTransactions(); - const ts = res.transactions.filter((t) => t.transactionId === tid); - if (ts.length === 1) { - setTransaction(ts[0]); - } else { - route(Pages.history); - } - }; - fetchData(); - }, [tid]); + const state = useAsyncAsHook(getTransaction, [ + NotificationType.WithdrawGroupFinished, + ]); - if (!transaction) { + if (!state) { return ( <div> <i18n.Translate>Loading ...</i18n.Translate> </div> ); } + + if (state.hasError) { + route(Pages.history); + return ( + <div> + <i18n.Translate> + There was an error. Redirecting into the history page + </i18n.Translate> + </div> + ); + } + + function goToHistory(): void { + route(Pages.history); + } + return ( <TransactionView - transaction={transaction} - onDelete={() => wxApi.deleteTransaction(tid).then(() => history.go(-1))} - onRetry={() => wxApi.retryTransaction(tid).then(() => history.go(-1))} - onBack={() => { - route(Pages.history); - }} + transaction={state.response} + onDelete={() => wxApi.deleteTransaction(tid).then(goToHistory)} + onRetry={() => wxApi.retryTransaction(tid).then(goToHistory)} + onBack={goToHistory} /> ); } @@ -91,16 +109,28 @@ export function TransactionView({ onRetry, onBack, }: WalletTransactionProps): VNode { - function TransactionTemplate({ children }: { children: VNode[] }): VNode { + const [confirmBeforeForget, setConfirmBeforeForget] = useState(false); + function doCheckBeforeForget(): void { + if ( + transaction.pending && + transaction.type === TransactionType.Withdrawal + ) { + setConfirmBeforeForget(true); + } else { + onDelete(); + } + } + function TransactionTemplate({ + children, + }: { + children: ComponentChildren; + }): VNode { return ( - <WalletBox> + <Fragment> <section style={{ padding: 8, textAlign: "center" }}> <ErrorMessage title={transaction?.error?.hint} /> {transaction.pending && ( - <WarningBox> - This transaction is not completed - <a href="">more info...</a> - </WarningBox> + <WarningBox>This transaction is not completed</WarningBox> )} </section> <section> @@ -116,12 +146,12 @@ export function TransactionView({ <i18n.Translate>retry</i18n.Translate> </ButtonPrimary> ) : null} - <ButtonDestructive onClick={onDelete}> + <ButtonDestructive onClick={doCheckBeforeForget}> <i18n.Translate> Forget </i18n.Translate> </ButtonDestructive> </div> </footer> - </WalletBox> + </Fragment> ); } @@ -138,27 +168,119 @@ export function TransactionView({ ).amount; return ( <TransactionTemplate> + {confirmBeforeForget ? ( + <Overlay> + <CenteredDialog> + <header>Caution!</header> + <section> + If you have already wired money to the exchange you will loose + the chance to get the coins form it. + </section> + <footer> + <Button onClick={() => setConfirmBeforeForget(false)}> + <i18n.Translate> Cancel </i18n.Translate> + </Button> + + <ButtonDestructive onClick={onDelete}> + <i18n.Translate> Confirm </i18n.Translate> + </ButtonDestructive> + </footer> + </CenteredDialog> + </Overlay> + ) : undefined} <h2>Withdrawal</h2> <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" /> - <br /> - <Part - big - title="Total withdrawn" - text={amountToString(transaction.amountEffective)} - kind="positive" - /> - <Part - big - title="Chosen amount" - text={amountToString(transaction.amountRaw)} - kind="neutral" - /> - <Part - big - title="Exchange fee" - text={amountToString(fee)} - kind="negative" - /> + {transaction.pending ? ( + transaction.withdrawalDetails.type === + WithdrawalType.ManualTransfer ? ( + <Fragment> + <BankDetailsByPaytoType + amount={amountToString(transaction.amountRaw)} + exchangeBaseUrl={transaction.exchangeBaseUrl} + payto={parsePaytoUri( + transaction.withdrawalDetails.exchangePaytoUris[0], + )} + subject={transaction.withdrawalDetails.reservePub} + /> + <p> + <WarningBox> + Make sure to use the correct subject, otherwise the money will + not arrive in this wallet. + </WarningBox> + </p> + <Part + big + title="Total withdrawn" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + big + title="Exchange fee" + text={amountToString(fee)} + kind="negative" + /> + </Fragment> + ) : ( + <Fragment> + {!transaction.withdrawalDetails.confirmed && + transaction.withdrawalDetails.bankConfirmationUrl ? ( + <InfoBox> + The bank is waiting for confirmation. Go to the + <a + href={transaction.withdrawalDetails.bankConfirmationUrl} + target="_blank" + rel="noreferrer" + > + bank site + </a> + </InfoBox> + ) : undefined} + {transaction.withdrawalDetails.confirmed && ( + <InfoBox>Waiting for the coins to arrive</InfoBox> + )} + <Part + big + title="Total withdrawn" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + big + title="Chosen amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part + big + title="Exchange fee" + text={amountToString(fee)} + kind="negative" + /> + </Fragment> + ) + ) : ( + <Fragment> + <Part + big + title="Total withdrawn" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + big + title="Chosen amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part + big + title="Exchange fee" + text={amountToString(fee)} + kind="negative" + /> + </Fragment> + )} <Part title="Exchange" text={new URL(transaction.exchangeBaseUrl).hostname} diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index a6dd040e4..b180fdd05 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -20,13 +20,12 @@ * @author Florian Dold */ +import { WalletDiagnostics } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; import { Checkbox } from "../components/Checkbox"; -import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { Diagnostics } from "../components/Diagnostics"; -import { WalletBox } from "../components/styled"; import { useDiagnostics } from "../hooks/useDiagnostics"; -import { WalletDiagnostics } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; export function WelcomePage(): VNode { const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); @@ -54,7 +53,7 @@ export function View({ timedOut, }: ViewProps): VNode { return ( - <WalletBox> + <Fragment> <h1>Browser Extension Installed!</h1> <div> <p>Thank you for installing the wallet.</p> @@ -75,6 +74,6 @@ export function View({ Learn how to top up your wallet balance » </a> </div> - </WalletBox> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index f097d58b5..a17550ff9 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -22,7 +22,7 @@ import { setupI18n } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; -import { Fragment, h, render } from "preact"; +import { Fragment, h, render, VNode } from "preact"; import Router, { route, Route } from "preact-router"; import { useEffect } from "preact/hooks"; import { LogoHeader } from "./components/LogoHeader"; @@ -39,8 +39,11 @@ import { SettingsPage } from "./wallet/Settings"; import { TransactionPage } from "./wallet/Transaction"; import { WelcomePage } from "./wallet/Welcome"; import { BackupPage } from "./wallet/BackupPage"; -import { DeveloperPage } from "./popup/Debug.js"; -import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js"; +import { DeveloperPage } from "./popup/Debug"; +import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage"; +import { WalletBox } from "./components/styled"; +import { ProviderDetailPage } from "./wallet/ProviderDetailPage"; +import { ProviderAddPage } from "./wallet/ProviderAddPage"; function main(): void { try { @@ -66,16 +69,20 @@ if (document.readyState === "loading") { } function withLogoAndNavBar(Component: any) { - return (props: any) => ( - <Fragment> - <LogoHeader /> - <WalletNavBar /> - <Component {...props} /> - </Fragment> - ); + return function withLogoAndNavBarComponent(props: any): VNode { + return ( + <Fragment> + <LogoHeader /> + <WalletNavBar /> + <WalletBox> + <Component {...props} /> + </WalletBox> + </Fragment> + ); + }; } -function Application() { +function Application(): VNode { return ( <div> <DevContextProvider> @@ -105,6 +112,23 @@ function Application() { <Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} + onAddProvider={() => { + route(Pages.provider_add); + }} + /> + <Route + path={Pages.provider_detail} + component={withLogoAndNavBar(ProviderDetailPage)} + onBack={() => { + route(Pages.backup); + }} + /> + <Route + path={Pages.provider_add} + component={withLogoAndNavBar(ProviderAddPage)} + onBack={() => { + route(Pages.backup); + }} /> <Route diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 90cfd3ed6..be0fed883 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -22,39 +22,21 @@ * Imports. */ import { - CoreApiResponse, - ConfirmPayResult, - BalancesResponse, - TransactionsResponse, - ApplyRefundResponse, - PreparePayResult, - AcceptWithdrawalResponse, - WalletDiagnostics, - GetWithdrawalDetailsForUriRequest, - WithdrawUriInfoResponse, - PrepareTipRequest, - PrepareTipResult, - AcceptTipRequest, - DeleteTransactionRequest, - RetryTransactionRequest, - SetWalletDeviceIdRequest, - GetExchangeWithdrawalInfo, AcceptExchangeTosRequest, - AcceptManualWithdrawalResult, - AcceptManualWithdrawalRequest, - AmountJson, - ExchangesListRespose, - AddExchangeRequest, - GetExchangeTosResult, + AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse, + AddExchangeRequest, ApplyRefundResponse, BalancesResponse, ConfirmPayResult, + CoreApiResponse, DeleteTransactionRequest, ExchangesListRespose, + GetExchangeTosResult, GetExchangeWithdrawalInfo, + GetWithdrawalDetailsForUriRequest, NotificationType, PreparePayResult, PrepareTipRequest, + PrepareTipResult, RetryTransactionRequest, + SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse } from "@gnu-taler/taler-util"; import { - AddBackupProviderRequest, - BackupProviderState, - OperationFailedError, - RemoveBackupProviderRequest, + AddBackupProviderRequest, BackupInfo, OperationFailedError, + RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core"; -import { BackupInfo } from "@gnu-taler/taler-wallet-core"; import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw"; +import { MessageFromBackend } from "./wxBackend.js"; export interface ExtendedPermissionsResponse { newValue: boolean; @@ -83,7 +65,9 @@ export interface UpgradeResponse { async function callBackend(operation: string, payload: any): Promise<any> { return new Promise<any>((resolve, reject) => { + // eslint-disable-next-line no-undef chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { + // eslint-disable-next-line no-undef if (chrome.runtime.lastError) { console.log("Error calling backend"); reject( @@ -366,10 +350,13 @@ export function acceptTip(req: AcceptTipRequest): Promise<void> { return callBackend("acceptTip", req); } -export function onUpdateNotification(f: () => void): () => void { +export function onUpdateNotification(messageType: Array<NotificationType>, doCallback: () => void): () => void { + // eslint-disable-next-line no-undef const port = chrome.runtime.connect({ name: "notifications" }); - const listener = (): void => { - f(); + const listener = (message: MessageFromBackend): void => { + if (messageType.includes(message.type)) { + doCallback(); + } }; port.onMessage.addListener(listener); return () => { diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 4004f04f6..df3115246 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -39,6 +39,7 @@ import { classifyTalerUri, CoreApiResponse, CoreApiResponseSuccess, + NotificationType, TalerErrorCode, TalerUriType, WalletDiagnostics, @@ -237,6 +238,10 @@ function makeSyncWalletRedirect( return { redirectUrl: innerUrl.href }; } +export type MessageFromBackend = { + type: NotificationType +} + async function reinitWallet(): Promise<void> { if (currentWallet) { currentWallet.stop(); @@ -266,9 +271,10 @@ async function reinitWallet(): Promise<void> { return; } wallet.addNotificationListener((x) => { - for (const x of notificationPorts) { + for (const notif of notificationPorts) { + const message: MessageFromBackend = { type: x.type }; try { - x.postMessage({ type: "notification" }); + notif.postMessage(message); } catch (e) { console.error(e); } |