diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet')
21 files changed, 1700 insertions, 1167 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx index 9a53fefe2..b2771bc2a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx @@ -15,179 +15,184 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @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'; +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: 'wallet/backup/list', + title: "wallet/backup/list", component: TestedComponent, argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } + 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 + 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", + }, }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" - ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": addDays(new Date(), 13).getTime() - } + { + 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", + }, }, - "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, + { + 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", + }, }, - "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, + { + 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", + }, }, - "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', + { + 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: '1', - + supportedProtocolVersion: "0.0", }, - 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, + { + 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", + }, }, - "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, + { + 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", + }, }, - "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 - } + 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", + }, }, - "terms": { - "annualFee": "ARS:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - }] + ], }); - export const Empty = createExample(TestedComponent, { - providers: [] + providers: [], }); - diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx index 712329bf8..c3be0203e 100644 --- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx @@ -14,15 +14,29 @@ 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 { + ProviderInfo, + ProviderPaymentStatus, +} from "@gnu-taler/taler-wallet-core"; +import { + differenceInMonths, + formatDuration, + intervalToDuration, +} from "date-fns"; import { Fragment, JSX, VNode, h } from "preact"; import { - BoldLight, ButtonPrimary, ButtonSuccess, Centered, - CenteredText, CenteredBoldText, PopupBox, RowBorderGray, - SmallText, SmallLightText, WalletBox + BoldLight, + ButtonPrimary, + ButtonSuccess, + Centered, + CenteredText, + CenteredBoldText, + PopupBox, + RowBorderGray, + SmallText, + SmallLightText, + WalletBox, } from "../components/styled"; import { useBackupStatus } from "../hooks/useBackupStatus"; import { Pages } from "../NavigationBar"; @@ -32,49 +46,68 @@ interface Props { } export function BackupPage({ onAddProvider }: Props): VNode { - const status = useBackupStatus() + const status = useBackupStatus(); if (!status) { - return <div>Loading...</div> + return <div>Loading...</div>; } - return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />; + return ( + <BackupView + providers={status.providers} + onAddProvider={onAddProvider} + onSyncAll={status.sync} + /> + ); } export interface ViewProps { - providers: ProviderInfo[], + providers: ProviderInfo[]; onAddProvider: () => void; onSyncAll: () => Promise<void>; } -export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode { +export function BackupView({ + providers, + onAddProvider, + onSyncAll, +}: ViewProps): VNode { return ( <WalletBox> <section> - {providers.map((provider) => <BackupLayout - status={provider.paymentStatus} - timestamp={provider.lastSuccessfulBackupTimestamp} - id={provider.syncProviderBaseUrl} - active={provider.active} - title={provider.name} - /> + {providers.map((provider) => ( + <BackupLayout + 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> )} - {!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>} + {!!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> + )} </WalletBox> - ) + ); } interface TransactionLayoutProps { @@ -92,55 +125,73 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element { 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>} + <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} /> : + {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> + 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)'; + 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 + if (d.t_ms === "never") return undefined; const duration = intervalToDuration({ start: d.t_ms, end: new Date(), - }) + }); const str = formatDuration(duration, { - delimiter: ', ', + delimiter: ", ", format: [ - duration?.years ? 'years' : ( - duration?.months ? 'months' : ( - duration?.days ? 'days' : ( - duration.hours ? 'hours' : 'minutes' - ) - ) - ) - ] - }) - return `${str}` -}
\ No newline at end of file + duration?.years + ? "years" + : duration?.months + ? "months" + : duration?.days + ? "days" + : duration.hours + ? "hours" + : "minutes", + ], + }); + return `${str}`; +} diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx index cccda203e..2432c31eb 100644 --- a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx @@ -15,28 +15,25 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample, NullLink } from '../test-utils'; -import { BalanceView as TestedComponent } from './BalancePage'; +import { createExample, NullLink } from "../test-utils"; +import { BalanceView as TestedComponent } from "./BalancePage"; export default { - title: 'wallet/balance', + title: "wallet/balance", component: TestedComponent, - argTypes: { - } + argTypes: {}, }; - -export const NotYetLoaded = createExample(TestedComponent, { -}); +export const NotYetLoaded = createExample(TestedComponent, {}); export const GotError = createExample(TestedComponent, { balance: { hasError: true, - message: 'Network error' + message: "Network error", }, Linker: NullLink, }); @@ -45,7 +42,7 @@ export const EmptyBalance = createExample(TestedComponent, { balance: { hasError: false, response: { - balances: [] + balances: [], }, }, Linker: NullLink, @@ -55,13 +52,15 @@ export const SomeCoins = createExample(TestedComponent, { balance: { hasError: false, response: { - balances: [{ - available: 'USD:10.5', - hasPendingTransactions: false, - pendingIncoming: 'USD:0', - pendingOutgoing: 'USD:0', - requiresUserInput: false - }] + balances: [ + { + available: "USD:10.5", + hasPendingTransactions: false, + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + ], }, }, Linker: NullLink, @@ -71,13 +70,15 @@ export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { balance: { hasError: false, response: { - balances: [{ - available: 'USD:2.23', - hasPendingTransactions: false, - pendingIncoming: 'USD:5.11', - pendingOutgoing: 'USD:0', - requiresUserInput: false - }] + balances: [ + { + available: "USD:2.23", + hasPendingTransactions: false, + pendingIncoming: "USD:5.11", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + ], }, }, Linker: NullLink, @@ -87,19 +88,22 @@ export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { balance: { hasError: false, response: { - balances: [{ - available: 'USD:2', - hasPendingTransactions: false, - pendingIncoming: 'USD:5', - pendingOutgoing: 'USD:0', - requiresUserInput: false - },{ - available: 'EUR:4', - hasPendingTransactions: false, - pendingIncoming: 'EUR:5', - pendingOutgoing: 'EUR:0', - requiresUserInput: false - }] + balances: [ + { + available: "USD:2", + hasPendingTransactions: false, + pendingIncoming: "USD:5", + pendingOutgoing: "USD:0", + requiresUserInput: false, + }, + { + available: "EUR:4", + hasPendingTransactions: false, + pendingIncoming: "EUR:5", + pendingOutgoing: "EUR:0", + requiresUserInput: false, + }, + ], }, }, Linker: NullLink, diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx index eb5a0447c..f3c08a3e8 100644 --- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -15,19 +15,30 @@ */ import { - amountFractionalBase, Amounts, - Balance, BalancesResponse, - i18n + amountFractionalBase, + Amounts, + Balance, + BalancesResponse, + i18n, } from "@gnu-taler/taler-util"; -import { JSX } from "preact"; +import { JSX, h } from "preact"; import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index"; import { BalancesHook, useBalances } from "../hooks/useBalances"; import { PageLink, renderAmount } from "../renderHtml"; - -export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) { - const balance = useBalances() - return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} /> +export function BalancePage({ + goToWalletManualWithdraw, +}: { + goToWalletManualWithdraw: () => void; +}) { + const balance = useBalances(); + return ( + <BalanceView + balance={balance} + Linker={PageLink} + goToWalletManualWithdraw={goToWalletManualWithdraw} + /> + ); } export interface BalanceViewProps { @@ -36,9 +47,13 @@ export interface BalanceViewProps { goToWalletManualWithdraw: () => void; } -export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) { +export function BalanceView({ + balance, + Linker, + goToWalletManualWithdraw, +}: BalanceViewProps) { if (!balance) { - return <span /> + return <span />; } if (balance.hasError) { @@ -50,19 +65,24 @@ export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: Balan diagnostics. </p> </div> - ) + ); } if (balance.response.balances.length === 0) { return ( - <p><i18n.Translate> - You have no balance to show. Need some{" "} - <Linker pageName="/welcome">help</Linker> getting started? - </i18n.Translate></p> - ) + <p> + <i18n.Translate> + You have no balance to show. Need some{" "} + <Linker pageName="/welcome">help</Linker> getting started? + </i18n.Translate> + </p> + ); } - return <ShowBalances wallet={balance.response} - onWithdraw={goToWalletManualWithdraw} - /> + return ( + <ShowBalances + wallet={balance.response} + onWithdraw={goToWalletManualWithdraw} + /> + ); } function formatPending(entry: Balance): JSX.Element { @@ -75,13 +95,15 @@ function formatPending(entry: Balance): JSX.Element { if (!Amounts.isZero(pendingIncoming)) { incoming = ( - <span><i18n.Translate> - <span style={{ color: "darkgreen" }}> - {"+"} - {renderAmount(entry.pendingIncoming)} - </span>{" "} - incoming - </i18n.Translate></span> + <span> + <i18n.Translate> + <span style={{ color: "darkgreen" }}> + {"+"} + {renderAmount(entry.pendingIncoming)} + </span>{" "} + incoming + </i18n.Translate> + </span> ); } @@ -100,27 +122,36 @@ function formatPending(entry: Balance): JSX.Element { ); } - -function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) { - return <WalletBox> - <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> - </section> - <footer> - <div /> - <ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary> - </footer> - </WalletBox> +function ShowBalances({ + wallet, + onWithdraw, +}: { + wallet: BalancesResponse; + onWithdraw: () => void; +}) { + return ( + <WalletBox> + <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> + </section> + <footer> + <div /> + <ButtonPrimary onClick={onWithdraw}>Withdraw</ButtonPrimary> + </footer> + </WalletBox> + ); } diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx index 35da52392..6eab8dc3a 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx @@ -15,42 +15,39 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { CreateManualWithdraw as TestedComponent } from './CreateManualWithdraw'; +import { createExample } from "../test-utils"; +import { CreateManualWithdraw as TestedComponent } from "./CreateManualWithdraw"; export default { - title: 'wallet/manual withdraw/creation', + title: "wallet/manual withdraw/creation", component: TestedComponent, - argTypes: { - } + argTypes: {}, }; - -export const InitialState = createExample(TestedComponent, { -}); +export const InitialState = createExample(TestedComponent, {}); export const WithExchangeFilled = createExample(TestedComponent, { - currency: 'COL', - initialExchange: 'http://exchange.taler:8081', + currency: "COL", + initialExchange: "http://exchange.taler:8081", }); export const WithExchangeAndAmountFilled = createExample(TestedComponent, { - currency: 'COL', - initialExchange: 'http://exchange.taler:8081', - initialAmount: '10' + currency: "COL", + initialExchange: "http://exchange.taler:8081", + initialAmount: "10", }); export const WithExchangeError = createExample(TestedComponent, { - initialExchange: 'http://exchange.tal', - error: 'The exchange url seems invalid' + initialExchange: "http://exchange.tal", + error: "The exchange url seems invalid", }); export const WithAmountError = createExample(TestedComponent, { - currency: 'COL', - initialExchange: 'http://exchange.taler:8081', - initialAmount: 'e' + currency: "COL", + initialExchange: "http://exchange.taler:8081", + initialAmount: "e", }); diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index be2cbe41d..b48dcbaf2 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -1,8 +1,35 @@ +/* + 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 { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { VNode } from "preact"; +import { VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { ErrorMessage } from "../components/ErrorMessage"; -import { ButtonPrimary, Input, InputWithLabel, LightText, WalletBox } from "../components/styled"; +import { + ButtonPrimary, + Input, + InputWithLabel, + LightText, + WalletBox, +} from "../components/styled"; export interface Props { error: string | undefined; @@ -13,44 +40,73 @@ export interface Props { onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>; } -export function CreateManualWithdraw({ onExchangeChange, initialExchange, initialAmount, error, currency, onCreate }: Props): VNode { +export function CreateManualWithdraw({ + onExchangeChange, + initialExchange, + initialAmount, + error, + currency, + onCreate, +}: Props): VNode { const [exchange, setExchange] = useState(initialExchange || ""); const [amount, setAmount] = useState(initialAmount || ""); - const parsedAmount = Amounts.parse(`${currency}:${amount}`) + const parsedAmount = Amounts.parse(`${currency}:${amount}`); let timeout = useRef<number | undefined>(undefined); useEffect(() => { - if (timeout) window.clearTimeout(timeout.current) + if (timeout) window.clearTimeout(timeout.current); timeout.current = window.setTimeout(async () => { - onExchangeChange(exchange) + onExchangeChange(exchange); }, 1000); - }, [exchange]) - + }, [exchange]); return ( <WalletBox> <section> - <ErrorMessage title={error && "Can't create the reserve"} description={error} /> + <ErrorMessage + title={error && "Can't create the reserve"} + description={error} + /> <h2>Manual Withdrawal</h2> - <LightText>Choose a exchange to create a reserve and then fill the reserve to withdraw the coins</LightText> + <LightText> + Choose a exchange to create a reserve and then fill the reserve to + withdraw the coins + </LightText> <p> <Input invalid={!!exchange && !currency}> <label>Exchange</label> - <input type="text" placeholder="https://" value={exchange} onChange={(e) => setExchange(e.currentTarget.value)} /> + <input + type="text" + placeholder="https://" + value={exchange} + onChange={(e) => setExchange(e.currentTarget.value)} + /> <small>http://exchange.taler:8081</small> </Input> - {currency && <InputWithLabel invalid={!!amount && !parsedAmount}> - <label>Amount</label> - <div> - <div>{currency}</div> - <input type="number" style={{ paddingLeft: `${currency.length}em` }} value={amount} onChange={e => setAmount(e.currentTarget.value)} /> - </div> - </InputWithLabel>} + {currency && ( + <InputWithLabel invalid={!!amount && !parsedAmount}> + <label>Amount</label> + <div> + <div>{currency}</div> + <input + type="number" + style={{ paddingLeft: `${currency.length}em` }} + value={amount} + onChange={(e) => setAmount(e.currentTarget.value)} + /> + </div> + </InputWithLabel> + )} </p> </section> <footer> <div /> - <ButtonPrimary disabled={!parsedAmount || !exchange} onClick={() => onCreate(exchange, parsedAmount!)}>Create</ButtonPrimary> + <ButtonPrimary + disabled={!parsedAmount || !exchange} + onClick={() => onCreate(exchange, parsedAmount!)} + > + Create + </ButtonPrimary> </footer> </WalletBox> ); diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 0ac4be9a6..9ae3ac3bd 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -15,133 +15,146 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { PaymentStatus, - TransactionCommon, TransactionDeposit, TransactionPayment, - TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, + TransactionCommon, + TransactionDeposit, + TransactionPayment, + TransactionRefresh, + TransactionRefund, + TransactionTip, + TransactionType, TransactionWithdrawal, - WithdrawalType -} from '@gnu-taler/taler-util'; -import { HistoryView as TestedComponent } from './History'; -import { createExample } from '../test-utils'; - + WithdrawalType, +} from "@gnu-taler/taler-util"; +import { HistoryView as TestedComponent } from "./History"; +import { createExample } from "../test-utils"; export default { - title: 'wallet/history/list', + title: "wallet/history/list", component: TestedComponent, }; -let count = 0 -const commonTransaction = () => ({ - amountRaw: 'USD:10', - amountEffective: 'USD:9', - pending: false, - timestamp: { - t_ms: new Date().getTime() - (count++ * 1000 * 60 * 60 * 7) - }, - transactionId: '12', -} as TransactionCommon) +let count = 0; +const commonTransaction = () => + ({ + amountRaw: "USD:10", + amountEffective: "USD:9", + pending: false, + timestamp: { + t_ms: new Date().getTime() - count++ * 1000 * 60 * 60 * 7, + }, + transactionId: "12", + } as TransactionCommon); const exampleData = { withdraw: { ...commonTransaction(), type: TransactionType.Withdrawal, - exchangeBaseUrl: 'http://exchange.demo.taler.net', + exchangeBaseUrl: "http://exchange.demo.taler.net", withdrawalDetails: { confirmed: false, - exchangePaytoUris: ['payto://x-taler-bank/bank/account'], + exchangePaytoUris: ["payto://x-taler-bank/bank/account"], type: WithdrawalType.ManualTransfer, - } + }, } as TransactionWithdrawal, payment: { ...commonTransaction(), - amountEffective: 'USD:11', + amountEffective: "USD:11", type: TransactionType.Payment, info: { - contractTermsHash: 'ASDZXCASD', + contractTermsHash: "ASDZXCASD", merchant: { - name: 'Blog', + name: "Blog", }, - orderId: '2021.167-03NPY6MCYMVGT', + orderId: "2021.167-03NPY6MCYMVGT", products: [], - summary: 'the summary', - fulfillmentMessage: '', + summary: "the summary", + fulfillmentMessage: "", }, - proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", status: PaymentStatus.Accepted, } as TransactionPayment, deposit: { ...commonTransaction(), type: TransactionType.Deposit, - depositGroupId: '#groupId', - targetPaytoUri: 'payto://x-taler-bank/bank/account', + depositGroupId: "#groupId", + targetPaytoUri: "payto://x-taler-bank/bank/account", } as TransactionDeposit, refresh: { ...commonTransaction(), type: TransactionType.Refresh, - exchangeBaseUrl: 'http://exchange.taler', + exchangeBaseUrl: "http://exchange.taler", } as TransactionRefresh, tip: { ...commonTransaction(), type: TransactionType.Tip, - merchantBaseUrl: 'http://ads.merchant.taler.net/', + merchantBaseUrl: "http://ads.merchant.taler.net/", } as TransactionTip, refund: { ...commonTransaction(), type: TransactionType.Refund, - refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + refundedTransactionId: + "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", info: { - contractTermsHash: 'ASDZXCASD', + contractTermsHash: "ASDZXCASD", merchant: { - name: 'the merchant', + name: "the merchant", }, - orderId: '2021.167-03NPY6MCYMVGT', + orderId: "2021.167-03NPY6MCYMVGT", products: [], - summary: 'the summary', - fulfillmentMessage: '', + summary: "the summary", + fulfillmentMessage: "", }, } as TransactionRefund, -} +}; export const Empty = createExample(TestedComponent, { list: [], - balances: [{ - available: 'TESTKUDOS:10', - pendingIncoming: 'TESTKUDOS:0', - pendingOutgoing: 'TESTKUDOS:0', - hasPendingTransactions: false, - requiresUserInput: false, - }] + balances: [ + { + available: "TESTKUDOS:10", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); - export const One = createExample(TestedComponent, { list: [exampleData.withdraw], - balances: [{ - available: 'USD:10', - pendingIncoming: 'USD:0', - pendingOutgoing: 'USD:0', - hasPendingTransactions: false, - requiresUserInput: false, - }] + balances: [ + { + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); export const OnePending = createExample(TestedComponent, { - list: [{ - ...exampleData.withdraw, - pending: true - }], - balances: [{ - available: 'USD:10', - pendingIncoming: 'USD:0', - pendingOutgoing: 'USD:0', - hasPendingTransactions: false, - requiresUserInput: false, - }] + list: [ + { + ...exampleData.withdraw, + pending: true, + }, + ], + balances: [ + { + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); export const Several = createExample(TestedComponent, { @@ -154,20 +167,23 @@ export const Several = createExample(TestedComponent, { ...exampleData.payment, info: { ...exampleData.payment.info, - summary: 'this is a long summary that may be cropped because its too long', + summary: + "this is a long summary that may be cropped because its too long", }, }, exampleData.refund, exampleData.tip, exampleData.deposit, ], - balances: [{ - available: 'TESTKUDOS:10', - pendingIncoming: 'TESTKUDOS:0', - pendingOutgoing: 'TESTKUDOS:0', - hasPendingTransactions: false, - requiresUserInput: false, - }] + balances: [ + { + available: "TESTKUDOS:10", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); export const SeveralWithTwoCurrencies = createExample(TestedComponent, { @@ -181,18 +197,20 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, { exampleData.tip, exampleData.deposit, ], - balances: [{ - available: 'TESTKUDOS:10', - pendingIncoming: 'TESTKUDOS:0', - pendingOutgoing: 'TESTKUDOS:0', - hasPendingTransactions: false, - requiresUserInput: false, - }, { - available: 'USD:10', - pendingIncoming: 'USD:0', - pendingOutgoing: 'USD:0', - hasPendingTransactions: false, - requiresUserInput: false, - }] + balances: [ + { + available: "TESTKUDOS:10", + pendingIncoming: "TESTKUDOS:0", + pendingOutgoing: "TESTKUDOS:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + { + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], }); - diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 8160f8574..aabe50a29 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -14,7 +14,12 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountString, Balance, Transaction, TransactionsResponse } from "@gnu-taler/taler-util"; +import { + AmountString, + Balance, + Transaction, + TransactionsResponse, +} from "@gnu-taler/taler-util"; import { format } from "date-fns"; import { Fragment, h, JSX } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -23,13 +28,14 @@ import { TransactionItem } from "../components/TransactionItem"; import { useBalances } from "../hooks/useBalances"; import * as wxApi from "../wxApi"; - export function HistoryPage(props: any): JSX.Element { const [transactions, setTransactions] = useState< TransactionsResponse | undefined >(undefined); - const balance = useBalances() - const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || []) + const balance = useBalances(); + const balanceWithoutError = balance?.hasError + ? [] + : balance?.response.balances || []; useEffect(() => { const fetchData = async (): Promise<void> => { @@ -43,45 +49,74 @@ export function HistoryPage(props: any): JSX.Element { return <div>Loading ...</div>; } - return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />; + return ( + <HistoryView + balances={balanceWithoutError} + list={[...transactions.transactions].reverse()} + /> + ); } function amountToString(c: AmountString) { - const idx = c.indexOf(':') - return `${c.substring(idx + 1)} ${c.substring(0, idx)}` + const idx = c.indexOf(":"); + return `${c.substring(idx + 1)} ${c.substring(0, idx)}`; } - - -export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) { +export function HistoryView({ + list, + balances, +}: { + list: Transaction[]; + balances: Balance[]; +}) { const byDate = list.reduce(function (rv, x) { - const theDate = x.timestamp.t_ms === "never" ? "never" : format(x.timestamp.t_ms, 'dd MMMM yyyy'); + const theDate = + x.timestamp.t_ms === "never" + ? "never" + : format(x.timestamp.t_ms, "dd MMMM yyyy"); (rv[theDate] = rv[theDate] || []).push(x); return rv; }, {} as { [x: string]: Transaction[] }); - const multiCurrency = balances.length > 1 + const multiCurrency = balances.length > 1; - return <WalletBox noPadding> - {balances.length > 0 && <header> - {balances.length === 1 && <div class="title"> - Balance: <span>{amountToString(balances[0].available)}</span> - </div>} - {balances.length > 1 && <div class="title"> - Balance: <ul style={{ margin: 0 }}> - {balances.map(b => <li>{b.available}</li>)} - </ul> - </div>} - </header>} - <section> - {Object.keys(byDate).map((d,i) => { - return <Fragment key={i}> - <DateSeparator>{d}</DateSeparator> - {byDate[d].map((tx, i) => ( - <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency}/> - ))} - </Fragment> - })} - </section> - </WalletBox> + return ( + <WalletBox noPadding> + {balances.length > 0 && ( + <header> + {balances.length === 1 && ( + <div class="title"> + Balance: <span>{amountToString(balances[0].available)}</span> + </div> + )} + {balances.length > 1 && ( + <div class="title"> + Balance:{" "} + <ul style={{ margin: 0 }}> + {balances.map((b) => ( + <li>{b.available}</li> + ))} + </ul> + </div> + )} + </header> + )} + <section> + {Object.keys(byDate).map((d, i) => { + return ( + <Fragment key={i}> + <DateSeparator>{d}</DateSeparator> + {byDate[d].map((tx, i) => ( + <TransactionItem + key={i} + tx={tx} + multiCurrency={multiCurrency} + /> + ))} + </Fragment> + ); + })} + </section> + </WalletBox> + ); } diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx index dcc0002e6..102978f9e 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx @@ -14,68 +14,84 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - -import { VNode } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { VNode, h } from "preact"; +import { useState } from "preact/hooks"; import { CreateManualWithdraw } from "./CreateManualWithdraw"; -import * as wxApi from '../wxApi' -import { AcceptManualWithdrawalResult, AmountJson, Amounts } from "@gnu-taler/taler-util"; +import * as wxApi from "../wxApi"; +import { + AcceptManualWithdrawalResult, + AmountJson, + Amounts, +} from "@gnu-taler/taler-util"; import { ReserveCreated } from "./ReserveCreated.js"; -import { route } from 'preact-router'; +import { route } from "preact-router"; import { Pages } from "../NavigationBar.js"; -interface Props { - -} +interface Props {} -export function ManualWithdrawPage({ }: Props): VNode { - const [success, setSuccess] = useState<AcceptManualWithdrawalResult | undefined>(undefined) - const [currency, setCurrency] = useState<string | undefined>(undefined) - const [error, setError] = useState<string | undefined>(undefined) +export function ManualWithdrawPage({}: Props): VNode { + const [success, setSuccess] = useState< + AcceptManualWithdrawalResult | undefined + >(undefined); + const [currency, setCurrency] = useState<string | undefined>(undefined); + const [error, setError] = useState<string | undefined>(undefined); - async function onExchangeChange(exchange: string | undefined) { - if (!exchange) return + async function onExchangeChange(exchange: string | undefined): Promise<void> { + if (!exchange) return; try { - const r = await fetch(`${exchange}/keys`) - const j = await r.json() + const r = await fetch(`${exchange}/keys`); + const j = await r.json(); if (j.currency) { await wxApi.addExchange({ exchangeBaseUrl: `${exchange}/`, - forceUpdate: true - }) - setCurrency(j.currency) + forceUpdate: true, + }); + setCurrency(j.currency); } } catch (e) { - setError('The exchange url seems invalid') - setCurrency(undefined) + setError("The exchange url seems invalid"); + setCurrency(undefined); } } - async function doCreate(exchangeBaseUrl: string, amount: AmountJson) { + async function doCreate( + exchangeBaseUrl: string, + amount: AmountJson, + ): Promise<void> { try { - const resp = await wxApi.acceptManualWithdrawal(exchangeBaseUrl, Amounts.stringify(amount)) - setSuccess(resp) + const resp = await wxApi.acceptManualWithdrawal( + exchangeBaseUrl, + Amounts.stringify(amount), + ); + setSuccess(resp); } catch (e) { if (e instanceof Error) { - setError(e.message) + setError(e.message); } else { - setError('unexpected error') + setError("unexpected error"); } - setSuccess(undefined) + setSuccess(undefined); } } if (success) { - return <ReserveCreated reservePub={success.reservePub} paytos={success.exchangePaytoUris} onBack={() => { - route(Pages.balance) - }}/> + return ( + <ReserveCreated + reservePub={success.reservePub} + paytos={success.exchangePaytoUris} + onBack={() => { + route(Pages.balance); + }} + /> + ); } - return <CreateManualWithdraw - error={error} currency={currency} - onCreate={doCreate} onExchangeChange={onExchangeChange} - />; + return ( + <CreateManualWithdraw + error={error} + currency={currency} + onCreate={doCreate} + onExchangeChange={onExchangeChange} + /> + ); } - - - diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx index d1e76c053..5c4e56b15 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx @@ -15,38 +15,37 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { ConfirmProviderView as TestedComponent } from './ProviderAddPage'; +import { createExample } from "../test-utils"; +import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage"; export default { - title: 'wallet/backup/confirm', + title: "wallet/backup/confirm", component: TestedComponent, argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } + onRetry: { action: "onRetry" }, + onDelete: { action: "onDelete" }, + onBack: { action: "onBack" }, + }, }; - export const DemoService = createExample(TestedComponent, { - url: 'https://sync.demo.taler.net/', + url: "https://sync.demo.taler.net/", provider: { - annual_fee: 'KUDOS:0.1', - storage_limit_in_megabytes: 20, - supported_protocol_version: '1' - } + annual_fee: "KUDOS:0.1", + storage_limit_in_megabytes: 20, + supported_protocol_version: "1", + }, }); export const FreeService = createExample(TestedComponent, { - url: 'https://sync.taler:9667/', + url: "https://sync.taler:9667/", provider: { - annual_fee: 'ARS:0', - storage_limit_in_megabytes: 20, - supported_protocol_version: '1' - } + annual_fee: "ARS:0", + storage_limit_in_megabytes: 20, + supported_protocol_version: "1", + }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx index 4890e5e9c..75292b7e4 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx @@ -15,39 +15,37 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { SetUrlView as TestedComponent } from './ProviderAddPage'; +import { createExample } from "../test-utils"; +import { SetUrlView as TestedComponent } from "./ProviderAddPage"; export default { - title: 'wallet/backup/add', + title: "wallet/backup/add", component: TestedComponent, argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } + onRetry: { action: "onRetry" }, + onDelete: { action: "onDelete" }, + onBack: { action: "onBack" }, + }, }; - -export const Initial = createExample(TestedComponent, { -}); +export const Initial = createExample(TestedComponent, {}); export const WithValue = createExample(TestedComponent, { - initialValue: 'sync.demo.taler.net' -}); + initialValue: "sync.demo.taler.net", +}); export const WithConnectionError = createExample(TestedComponent, { - withError: 'Network error' -}); + withError: "Network error", +}); export const WithClientError = createExample(TestedComponent, { - withError: 'URL may not be right: (404) Not Found' -}); + withError: "URL may not be right: (404) Not Found", +}); export const WithServerError = createExample(TestedComponent, { - withError: 'Try another server: (500) Internal Server Error' -}); + withError: "Try another server: (500) Internal Server Error", +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx index 67ff83442..a170620a3 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx @@ -15,224 +15,221 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; -import { createExample } from '../test-utils'; -import { ProviderView as TestedComponent } from './ProviderDetailPage'; +import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; +import { createExample } from "../test-utils"; +import { ProviderView as TestedComponent } from "./ProviderDetailPage"; export default { - title: 'wallet/backup/details', + title: "wallet/backup/details", component: TestedComponent, argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } + 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" + 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" - } - } + 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 + active: true, + name: "sync.demo", + syncProviderBaseUrl: "http://sync.taler:9967/", + lastSuccessfulBackupTimestamp: { + t_ms: 1625063925078, }, lastAttemptedBackupTimestamp: { - "t_ms": 1625063925078 + t_ms: 1625063925078, }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + paymentProposalIds: [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } + 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" - } - } + 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" + active: true, + name: "sync.demo", + syncProviderBaseUrl: "http://sync.taler:9967/", + lastSuccessfulBackupTimestamp: { + t_ms: 1625063925078, + }, + paymentProposalIds: [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } + paymentStatus: { + type: ProviderPaymentType.Paid, + paidUntil: { + t_ms: 1656599921000, + }, }, backupProblem: { - type: 'backup-unreadable' - }, - "terms": { - "annualFee": "EUR:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } + 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" + active: true, + name: "sync.demo", + syncProviderBaseUrl: "http://sync.taler:9967/", + lastSuccessfulBackupTimestamp: { + t_ms: 1625063925078, + }, + paymentProposalIds: [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG", ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } + paymentStatus: { + type: ProviderPaymentType.Paid, + paidUntil: { + t_ms: 1656599921000, + }, }, backupProblem: { - type: 'backup-conflicting-device', - myDeviceId: 'my-device-id', - otherDeviceId: 'other-device-id', + 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" - } - } + 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" - } - } + 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" - } - } + 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" - } - } + 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, + active: true, + name: "sync.demo", + syncProviderBaseUrl: "http://sync.demo.taler.net/", + paymentProposalIds: [], + paymentStatus: { + type: ProviderPaymentType.TermsChanged, paidUntil: { - t_ms: 1656599921000 + t_ms: 1656599921000, }, newTerms: { - "annualFee": "EUR:10", - "storageLimitInMegabytes": 8, - "supportedProtocolVersion": "0.0" + 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" - } - } + 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/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx index c45458eb7..bd64b0760 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx @@ -14,13 +14,23 @@ 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 { + 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, WalletBox, SmallLightText } from "../components/styled"; +import { + Button, + ButtonDestructive, + ButtonPrimary, + PaymentStatus, + WalletBox, + SmallLightText, +} from "../components/styled"; import { useProviderStatus } from "../hooks/useProviderStatus"; interface Props { @@ -29,20 +39,29 @@ interface Props { } export function ProviderDetailPage({ pid, onBack }: Props): VNode { - const status = useProviderStatus(pid) + const status = useProviderStatus(pid); if (!status) { - return <div><i18n.Translate>Loading...</i18n.Translate></div> + return ( + <div> + <i18n.Translate>Loading...</i18n.Translate> + </div> + ); } if (!status.info) { - onBack() - return <div /> + onBack(); + return <div />; } - return <ProviderView info={status.info} - onSync={status.sync} - onDelete={() => status.remove().then(onBack)} - onBack={onBack} - onExtend={() => { null }} - />; + return ( + <ProviderView + info={status.info} + onSync={status.sync} + onDelete={() => status.remove().then(onBack)} + onBack={onBack} + onExtend={() => { + null; + }} + /> + ); } export interface ViewProps { @@ -53,124 +72,185 @@ export interface ViewProps { 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 +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 ( <WalletBox> <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> + <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> + <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>} + <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> + <Button onClick={onBack}> + <i18n.Translate> < back</i18n.Translate> + </Button> <div> - <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive> + <ButtonDestructive onClick={onDelete}> + <i18n.Translate>remove provider</i18n.Translate> + </ButtonDestructive> </div> </footer> </WalletBox> - ) + ); } function daysSince(d?: Timestamp) { - if (!d || d.t_ms === 'never') return 'never synced' + if (!d || d.t_ms === "never") return "never synced"; const duration = intervalToDuration({ start: d.t_ms, end: new Date(), - }) + }); const str = formatDuration(duration, { - delimiter: ', ', + 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` + 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} /> + 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>} /> + 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" /> + 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 ( + <ErrorMessage + title={ + <Fragment> + <i18n.Translate> + Unknown backup problem: {JSON.stringify(info.backupProblem)} + </i18n.Translate> + </Fragment> + } + /> + ); } } - return null + return null; } function colorByStatus(status: ProviderPaymentType) { switch (status) { case ProviderPaymentType.InsufficientBalance: - return 'rgb(223, 117, 20)' + return "rgb(223, 117, 20)"; case ProviderPaymentType.Unpaid: - return 'rgb(202, 60, 60)' + return "rgb(202, 60, 60)"; case ProviderPaymentType.Paid: - return 'rgb(28, 184, 65)' + return "rgb(28, 184, 65)"; case ProviderPaymentType.Pending: - return 'gray' + return "gray"; case ProviderPaymentType.InsufficientBalance: - return 'rgb(202, 60, 60)' + return "rgb(202, 60, 60)"; case ProviderPaymentType.TermsChanged: - return 'rgb(202, 60, 60)' + return "rgb(202, 60, 60)"; } } @@ -180,16 +260,19 @@ function descriptionByStatus(status: ProviderPaymentStatus) { // return i18n.str`not paid yet` case ProviderPaymentType.Paid: case ProviderPaymentType.TermsChanged: - if (status.paidUntil.t_ms === 'never') { - return i18n.str`service paid` + 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> + 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 '' + return ""; } } diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx index ca524f4e2..c552b19ba 100644 --- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx @@ -15,26 +15,23 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { ReserveCreated as TestedComponent } from './ReserveCreated'; +import { createExample } from "../test-utils"; +import { ReserveCreated as TestedComponent } from "./ReserveCreated"; export default { - title: 'wallet/manual withdraw/reserve created', + title: "wallet/manual withdraw/reserve created", component: TestedComponent, - argTypes: { - } + argTypes: {}, }; - export const InitialState = createExample(TestedComponent, { - reservePub: 'ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ', + reservePub: "ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ", paytos: [ - 'payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG', - 'payto://x-taler-bank/international-bank.com/myaccount?amount=COL%3A1&message=Taler+Withdrawal+TYQTE7VA4M9GZQ4TR06YBNGA05AJGMFNSK4Q62NXR2FKNDB1J4EX', - ] + "payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", + "payto://x-taler-bank/international-bank.com/myaccount?amount=COL%3A1&message=Taler+Withdrawal+TYQTE7VA4M9GZQ4TR06YBNGA05AJGMFNSK4Q62NXR2FKNDB1J4EX", + ], }); - diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx index e01336e02..9008e9751 100644 --- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx @@ -1,8 +1,7 @@ -import { Fragment, VNode } from "preact"; +import { h, Fragment, VNode } from "preact"; import { useState } from "preact/hooks"; import { QR } from "../components/QR"; import { ButtonBox, FontIcon, WalletBox } from "../components/styled"; - export interface Props { reservePub: string; paytos: string[]; @@ -10,30 +9,57 @@ export interface Props { } export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode { - const [opened, setOpened] = useState(-1) + const [opened, setOpened] = useState(-1); return ( <WalletBox> <section> <h2>Reserve created!</h2> - <p>Now you need to send money to the exchange to one of the following accounts</p> - <p>To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.</p> + <p> + Now you need to send money to the exchange to one of the following + accounts + </p> + <p> + To complete the setup of the reserve, you must now initiate a wire + transfer using the given wire transfer subject and crediting the + specified amount to the indicated account of the exchange. + </p> </section> <section> <ul> {paytos.map((href, idx) => { - const url = new URL(href) - return <li key={idx}><p> - <a href="" onClick={(e) => { setOpened(o => o === idx ? -1 : idx); e.preventDefault() }}>{url.pathname}</a> - {opened === idx && <Fragment> - <p>If your system supports RFC 8905, you can do this by opening <a href={href}>this URI</a> or scan the QR with your wallet</p> - <QR text={href} /> - </Fragment>} - </p></li> + const url = new URL(href); + return ( + <li key={idx}> + <p> + <a + href="" + onClick={(e) => { + setOpened((o) => (o === idx ? -1 : idx)); + e.preventDefault(); + }} + > + {url.pathname} + </a> + {opened === idx && ( + <Fragment> + <p> + If your system supports RFC 8905, you can do this by + opening <a href={href}>this URI</a> or scan the QR with + your wallet + </p> + <QR text={href} /> + </Fragment> + )} + </p> + </li> + ); })} </ul> </section> <footer> - <ButtonBox onClick={onBack}><FontIcon>←</FontIcon></ButtonBox> + <ButtonBox onClick={onBack}> + <FontIcon>←</FontIcon> + </ButtonBox> <div /> </footer> </WalletBox> diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx index a04a0b4fd..6cc1368d5 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx @@ -15,39 +15,41 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { SettingsView as TestedComponent } from './Settings'; +import { createExample } from "../test-utils"; +import { SettingsView as TestedComponent } from "./Settings"; export default { - title: 'wallet/settings', + title: "wallet/settings", component: TestedComponent, argTypes: { setDeviceName: () => Promise.resolve(), - } + }, }; export const AllOff = createExample(TestedComponent, { - deviceName: 'this-is-the-device-name', + deviceName: "this-is-the-device-name", setDeviceName: () => Promise.resolve(), }); export const OneChecked = createExample(TestedComponent, { - deviceName: 'this-is-the-device-name', + deviceName: "this-is-the-device-name", permissionsEnabled: true, setDeviceName: () => Promise.resolve(), }); export const WithOneExchange = createExample(TestedComponent, { - deviceName: 'this-is-the-device-name', + deviceName: "this-is-the-device-name", permissionsEnabled: true, setDeviceName: () => Promise.resolve(), - knownExchanges: [{ - currency: 'USD', - exchangeBaseUrl: 'http://exchange.taler', - paytoUris: ['payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator'] - }] + knownExchanges: [ + { + currency: "USD", + exchangeBaseUrl: "http://exchange.taler", + paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"], + }, + ], }); diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 8d18586b1..8d8f3cdbc 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -14,7 +14,6 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - import { ExchangeListItem, i18n } from "@gnu-taler/taler-util"; import { VNode, h, Fragment } from "preact"; import { Checkbox } from "../components/Checkbox"; @@ -30,18 +29,28 @@ 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 { 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} togglePermissions={togglePermissions} - developerMode={devMode} toggleDeveloperMode={toggleDevMode} - />; + return ( + <SettingsView + lang={lang} + changeLang={changeLang} + knownExchanges={ + !exchangesHook || exchangesHook.hasError + ? [] + : exchangesHook.response.exchanges + } + deviceName={name} + setDeviceName={update} + permissionsEnabled={permissionsEnabled} + togglePermissions={togglePermissions} + developerMode={devMode} + toggleDeveloperMode={toggleDevMode} + /> + ); } export interface ViewProps { @@ -56,52 +65,72 @@ export interface ViewProps { knownExchanges: Array<ExchangeListItem>; } -import { strings as messages } from '../i18n/strings' +import { strings as messages } from "../i18n/strings"; type LangsNames = { - [P in keyof typeof messages]: string -} + [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]', -} + 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, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { +export function SettingsView({ + knownExchanges, + lang, + changeLang, + deviceName, + setDeviceName, + permissionsEnabled, + togglePermissions, + developerMode, + toggleDeveloperMode, +}: ViewProps): VNode { return ( <WalletBox> <section> - - <h2><i18n.Translate>Known exchanges</i18n.Translate></h2> - {!knownExchanges || !knownExchanges.length ? <div> - No exchange yet! - </div> : + <h2> + <i18n.Translate>Known exchanges</i18n.Translate> + </h2> + {!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>)} + {knownExchanges.map((e) => ( + <tr> + <td>{e.currency}</td> + <td> + <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a> + </td> + </tr> + ))} </table> - } - - <h2><i18n.Translate>Permissions</i18n.Translate></h2> - <Checkbox label="Automatically open wallet based on page content" + )} + + <h2> + <i18n.Translate>Permissions</i18n.Translate> + </h2> + <Checkbox + label="Automatically open wallet based on page content" name="perm" description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)" - enabled={permissionsEnabled} onToggle={togglePermissions} + enabled={permissionsEnabled} + onToggle={togglePermissions} /> <h2>Config</h2> - <Checkbox label="Developer mode" + <Checkbox + label="Developer mode" name="devMode" description="(More options and information useful for debugging)" - enabled={developerMode} onToggle={toggleDeveloperMode} + enabled={developerMode} + onToggle={toggleDeveloperMode} /> </section> </WalletBox> - ) + ); } diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index 535509cef..c9a3f47cb 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -15,110 +15,116 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { PaymentStatus, - TransactionCommon, TransactionDeposit, TransactionPayment, - TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, + TransactionCommon, + TransactionDeposit, + TransactionPayment, + TransactionRefresh, + TransactionRefund, + TransactionTip, + TransactionType, TransactionWithdrawal, - WithdrawalType -} from '@gnu-taler/taler-util'; -import { createExample } from '../test-utils'; -import { TransactionView as TestedComponent } from './Transaction'; + WithdrawalType, +} from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils"; +import { TransactionView as TestedComponent } from "./Transaction"; export default { - title: 'wallet/history/details', + title: "wallet/history/details", component: TestedComponent, argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } + onRetry: { action: "onRetry" }, + onDelete: { action: "onDelete" }, + onBack: { action: "onBack" }, + }, }; const commonTransaction = { - amountRaw: 'KUDOS:11', - amountEffective: 'KUDOS:9.2', + amountRaw: "KUDOS:11", + amountEffective: "KUDOS:9.2", pending: false, timestamp: { - t_ms: new Date().getTime() + t_ms: new Date().getTime(), }, - transactionId: '12', -} as TransactionCommon + transactionId: "12", +} as TransactionCommon; const exampleData = { withdraw: { ...commonTransaction, type: TransactionType.Withdrawal, - exchangeBaseUrl: 'http://exchange.taler', + exchangeBaseUrl: "http://exchange.taler", withdrawalDetails: { confirmed: false, - exchangePaytoUris: ['payto://x-taler-bank/bank/account'], + exchangePaytoUris: ["payto://x-taler-bank/bank/account"], type: WithdrawalType.ManualTransfer, - } + }, } as TransactionWithdrawal, payment: { ...commonTransaction, - amountEffective: 'KUDOS:11', + amountEffective: "KUDOS:11", type: TransactionType.Payment, info: { - contractTermsHash: 'ASDZXCASD', + contractTermsHash: "ASDZXCASD", merchant: { - name: 'the merchant', + name: "the merchant", }, - orderId: '2021.167-03NPY6MCYMVGT', + orderId: "2021.167-03NPY6MCYMVGT", products: [], summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth", - fulfillmentMessage: '', + fulfillmentMessage: "", }, - proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", status: PaymentStatus.Accepted, } as TransactionPayment, deposit: { ...commonTransaction, type: TransactionType.Deposit, - depositGroupId: '#groupId', - targetPaytoUri: 'payto://x-taler-bank/bank/account', + depositGroupId: "#groupId", + targetPaytoUri: "payto://x-taler-bank/bank/account", } as TransactionDeposit, refresh: { ...commonTransaction, type: TransactionType.Refresh, - exchangeBaseUrl: 'http://exchange.taler', + exchangeBaseUrl: "http://exchange.taler", } as TransactionRefresh, tip: { ...commonTransaction, type: TransactionType.Tip, - merchantBaseUrl: 'http://merchant.taler', + merchantBaseUrl: "http://merchant.taler", } as TransactionTip, refund: { ...commonTransaction, type: TransactionType.Refund, - refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + refundedTransactionId: + "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", info: { - contractTermsHash: 'ASDZXCASD', + contractTermsHash: "ASDZXCASD", merchant: { - name: 'the merchant', + name: "the merchant", }, - orderId: '2021.167-03NPY6MCYMVGT', + orderId: "2021.167-03NPY6MCYMVGT", products: [], - summary: 'the summary', - fulfillmentMessage: '', + summary: "the summary", + fulfillmentMessage: "", }, } as TransactionRefund, -} +}; const transactionError = { code: 2000, details: "details", hint: "this is a hint for the error", - message: 'message' -} + message: "message", +}; export const Withdraw = createExample(TestedComponent, { - transaction: exampleData.withdraw + transaction: exampleData.withdraw, }); export const WithdrawError = createExample(TestedComponent, { @@ -132,24 +138,22 @@ export const WithdrawPending = createExample(TestedComponent, { transaction: { ...exampleData.withdraw, pending: true }, }); - export const Payment = createExample(TestedComponent, { - transaction: exampleData.payment + transaction: exampleData.payment, }); export const PaymentError = createExample(TestedComponent, { transaction: { ...exampleData.payment, - error: transactionError + error: transactionError, }, }); export const PaymentWithoutFee = createExample(TestedComponent, { transaction: { ...exampleData.payment, - amountRaw: 'KUDOS:11', - - } + amountRaw: "KUDOS:11", + }, }); export const PaymentPending = createExample(TestedComponent, { @@ -161,27 +165,33 @@ export const PaymentWithProducts = createExample(TestedComponent, { ...exampleData.payment, info: { ...exampleData.payment.info, - summary: 'this order has 5 products', - products: [{ - description: 't-shirt', - unit: 'shirts', - quantity: 1, - }, { - description: 't-shirt', - unit: 'shirts', - quantity: 1, - }, { - description: 'e-book', - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }] - } + summary: "this order has 5 products", + products: [ + { + description: "t-shirt", + unit: "shirts", + quantity: 1, + }, + { + description: "t-shirt", + unit: "shirts", + quantity: 1, + }, + { + description: "e-book", + }, + { + description: "beer", + unit: "pint", + quantity: 15, + }, + { + description: "beer", + unit: "pint", + quantity: 15, + }, + ], + }, } as TransactionPayment, }); @@ -190,75 +200,79 @@ export const PaymentWithLongSummary = createExample(TestedComponent, { ...exampleData.payment, info: { ...exampleData.payment.info, - summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ', - products: [{ - description: 'an xl sized t-shirt with some drawings on it, color pink', - unit: 'shirts', - quantity: 1, - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }] - } + summary: + "this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ", + products: [ + { + description: + "an xl sized t-shirt with some drawings on it, color pink", + unit: "shirts", + quantity: 1, + }, + { + description: "beer", + unit: "pint", + quantity: 15, + }, + ], + }, } as TransactionPayment, }); - export const Deposit = createExample(TestedComponent, { - transaction: exampleData.deposit + transaction: exampleData.deposit, }); export const DepositError = createExample(TestedComponent, { transaction: { ...exampleData.deposit, - error: transactionError + error: transactionError, }, }); export const DepositPending = createExample(TestedComponent, { - transaction: { ...exampleData.deposit, pending: true } + transaction: { ...exampleData.deposit, pending: true }, }); export const Refresh = createExample(TestedComponent, { - transaction: exampleData.refresh + transaction: exampleData.refresh, }); export const RefreshError = createExample(TestedComponent, { transaction: { ...exampleData.refresh, - error: transactionError + error: transactionError, }, }); export const Tip = createExample(TestedComponent, { - transaction: exampleData.tip + transaction: exampleData.tip, }); export const TipError = createExample(TestedComponent, { transaction: { ...exampleData.tip, - error: transactionError + error: transactionError, }, }); export const TipPending = createExample(TestedComponent, { - transaction: { ...exampleData.tip, pending: true } + transaction: { ...exampleData.tip, pending: true }, }); export const Refund = createExample(TestedComponent, { - transaction: exampleData.refund + transaction: exampleData.refund, }); export const RefundError = createExample(TestedComponent, { transaction: { ...exampleData.refund, - error: transactionError + error: transactionError, }, }); export const RefundPending = createExample(TestedComponent, { - transaction: { ...exampleData.refund, pending: true } + transaction: { ...exampleData.refund, pending: true }, }); export const RefundWithProducts = createExample(TestedComponent, { @@ -266,11 +280,14 @@ export const RefundWithProducts = createExample(TestedComponent, { ...exampleData.refund, info: { ...exampleData.refund.info, - products: [{ - description: 't-shirt', - }, { - description: 'beer', - }] - } + products: [ + { + description: "t-shirt", + }, + { + description: "beer", + }, + ], + }, } as TransactionRefund, }); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index cf41efb59..7de6982e7 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -14,27 +14,43 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util"; +import { + AmountLike, + Amounts, + i18n, + Transaction, + TransactionType, +} from "@gnu-taler/taler-util"; import { format } from "date-fns"; -import { JSX, VNode } from "preact"; -import { route } from 'preact-router'; +import { JSX, VNode, h } from "preact"; +import { route } from "preact-router"; import { useEffect, useState } from "preact/hooks"; import emptyImg from "../../static/img/empty.png"; import { ErrorMessage } from "../components/ErrorMessage"; import { Part } from "../components/Part"; -import { ButtonBox, ButtonBoxDestructive, ButtonPrimary, FontIcon, ListOfProducts, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled"; +import { + ButtonBox, + ButtonBoxDestructive, + ButtonPrimary, + FontIcon, + ListOfProducts, + RowBorderGray, + SmallLightText, + WalletBox, + WarningBox, +} from "../components/styled"; import { Pages } from "../NavigationBar"; import * as wxApi from "../wxApi"; -export function TransactionPage({ tid }: { tid: string; }): JSX.Element { - const [transaction, setTransaction] = useState< - Transaction | undefined - >(undefined); +export function TransactionPage({ tid }: { tid: string }): JSX.Element { + const [transaction, setTransaction] = useState<Transaction | undefined>( + undefined, + ); useEffect(() => { const fetchData = async (): Promise<void> => { const res = await wxApi.getTransactions(); - const ts = res.transactions.filter(t => t.transactionId === tid); + const ts = res.transactions.filter((t) => t.transactionId === tid); if (ts.length === 1) { setTransaction(ts[0]); } else { @@ -45,13 +61,22 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element { }, [tid]); if (!transaction) { - return <div><i18n.Translate>Loading ...</i18n.Translate></div>; + return ( + <div> + <i18n.Translate>Loading ...</i18n.Translate> + </div> + ); } - return <TransactionView - transaction={transaction} - onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))} - onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))} - onBack={() => { 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); + }} + /> + ); } export interface WalletTransactionProps { @@ -61,173 +86,310 @@ export interface WalletTransactionProps { onBack: () => void; } -export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) { - +export function TransactionView({ + transaction, + onDelete, + onRetry, + onBack, +}: WalletTransactionProps) { function TransactionTemplate({ children }: { children: VNode[] }) { - return <WalletBox> - <section style={{ padding: 8, textAlign: 'center'}}> - <ErrorMessage title={transaction?.error?.hint} /> - {transaction.pending && <WarningBox>This transaction is not completed</WarningBox>} - </section> - <section> - <div style={{ textAlign: 'center' }}> - {children} - </div> - </section> - <footer> - <ButtonBox onClick={onBack}><i18n.Translate> <FontIcon>←</FontIcon> </i18n.Translate></ButtonBox> - <div> - {transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null} - <ButtonBoxDestructive onClick={onDelete}><i18n.Translate>🗑</i18n.Translate></ButtonBoxDestructive> - </div> - </footer> - </WalletBox> + return ( + <WalletBox> + <section style={{ padding: 8, textAlign: "center" }}> + <ErrorMessage title={transaction?.error?.hint} /> + {transaction.pending && ( + <WarningBox>This transaction is not completed</WarningBox> + )} + </section> + <section> + <div style={{ textAlign: "center" }}>{children}</div> + </section> + <footer> + <ButtonBox onClick={onBack}> + <i18n.Translate> + {" "} + <FontIcon>←</FontIcon>{" "} + </i18n.Translate> + </ButtonBox> + <div> + {transaction?.error ? ( + <ButtonPrimary onClick={onRetry}> + <i18n.Translate>retry</i18n.Translate> + </ButtonPrimary> + ) : null} + <ButtonBoxDestructive onClick={onDelete}> + <i18n.Translate>🗑</i18n.Translate> + </ButtonBoxDestructive> + </div> + </footer> + </WalletBox> + ); } function amountToString(text: AmountLike) { - const aj = Amounts.jsonifyAmount(text) - const amount = Amounts.stringifyValue(aj) - return `${amount} ${aj.currency}` + const aj = Amounts.jsonifyAmount(text); + const amount = Amounts.stringifyValue(aj); + return `${amount} ${aj.currency}`; } - if (transaction.type === TransactionType.Withdrawal) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return <TransactionTemplate> - <h2>Withdrawal</h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part title="Total withdrawn" text={amountToString(transaction.amountEffective)} kind='positive' /> - <Part title="Chosen amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part title="Exchange fee" text={amountToString(fee)} kind='negative' /> - <Part title="Exchange" text={new URL(transaction.exchangeBaseUrl).hostname} kind='neutral' /> - </TransactionTemplate> + ).amount; + return ( + <TransactionTemplate> + <h2>Withdrawal</h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + title="Total withdrawn" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + title="Chosen amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part title="Exchange fee" text={amountToString(fee)} kind="negative" /> + <Part + title="Exchange" + text={new URL(transaction.exchangeBaseUrl).hostname} + kind="neutral" + /> + </TransactionTemplate> + ); } - const showLargePic = () => { - - } + const showLargePic = () => {}; if (transaction.type === TransactionType.Payment) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountEffective), Amounts.parseOrThrow(transaction.amountRaw), - ).amount - - return <TransactionTemplate> - <h2>Payment </h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part big title="Total paid" text={amountToString(transaction.amountEffective)} kind='negative' /> - <Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part big title="Fee" text={amountToString(fee)} kind='negative' /> - <Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' /> - <Part title="Purchase" text={transaction.info.summary} kind='neutral' /> - <Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' /> + ).amount; - <div> - {transaction.info.products && transaction.info.products.length > 0 && - <ListOfProducts> - {transaction.info.products.map((p, k) => <RowBorderGray key={k}> - <a href="#" onClick={showLargePic}> - <img src={p.image ? p.image : emptyImg} /> - </a> - <div> - {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>} - <div>{p.description}</div> - </div> - </RowBorderGray>)} - </ListOfProducts> - } - </div> - </TransactionTemplate> + return ( + <TransactionTemplate> + <h2>Payment </h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + big + title="Total paid" + text={amountToString(transaction.amountEffective)} + kind="negative" + /> + <Part + big + title="Purchase amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part big title="Fee" text={amountToString(fee)} kind="negative" /> + <Part + title="Merchant" + text={transaction.info.merchant.name} + kind="neutral" + /> + <Part title="Purchase" text={transaction.info.summary} kind="neutral" /> + <Part + title="Receipt" + text={`#${transaction.info.orderId}`} + kind="neutral" + /> + + <div> + {transaction.info.products && transaction.info.products.length > 0 && ( + <ListOfProducts> + {transaction.info.products.map((p, k) => ( + <RowBorderGray key={k}> + <a href="#" onClick={showLargePic}> + <img src={p.image ? p.image : emptyImg} /> + </a> + <div> + {p.quantity && p.quantity > 0 && ( + <SmallLightText> + x {p.quantity} {p.unit} + </SmallLightText> + )} + <div>{p.description}</div> + </div> + </RowBorderGray> + ))} + </ListOfProducts> + )} + </div> + </TransactionTemplate> + ); } if (transaction.type === TransactionType.Deposit) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return <TransactionTemplate> - <h2>Deposit </h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part big title="Total deposit" text={amountToString(transaction.amountEffective)} kind='negative' /> - <Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part big title="Fee" text={amountToString(fee)} kind='negative' /> - </TransactionTemplate> + ).amount; + return ( + <TransactionTemplate> + <h2>Deposit </h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + big + title="Total deposit" + text={amountToString(transaction.amountEffective)} + kind="negative" + /> + <Part + big + title="Purchase amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part big title="Fee" text={amountToString(fee)} kind="negative" /> + </TransactionTemplate> + ); } if (transaction.type === TransactionType.Refresh) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return <TransactionTemplate> - <h2>Refresh</h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part big title="Total refresh" text={amountToString(transaction.amountEffective)} kind='negative' /> - <Part big title="Refresh amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part big title="Fee" text={amountToString(fee)} kind='negative' /> - </TransactionTemplate> + ).amount; + return ( + <TransactionTemplate> + <h2>Refresh</h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + big + title="Total refresh" + text={amountToString(transaction.amountEffective)} + kind="negative" + /> + <Part + big + title="Refresh amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part big title="Fee" text={amountToString(fee)} kind="negative" /> + </TransactionTemplate> + ); } if (transaction.type === TransactionType.Tip) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return <TransactionTemplate> - <h2>Tip</h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part big title="Total tip" text={amountToString(transaction.amountEffective)} kind='positive' /> - <Part big title="Received amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part big title="Fee" text={amountToString(fee)} kind='negative' /> - </TransactionTemplate> + ).amount; + return ( + <TransactionTemplate> + <h2>Tip</h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + big + title="Total tip" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + big + title="Received amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part big title="Fee" text={amountToString(fee)} kind="negative" /> + </TransactionTemplate> + ); } if (transaction.type === TransactionType.Refund) { const fee = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return <TransactionTemplate> - <h2>Refund</h2> - <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div> - <br /> - <Part big title="Total refund" text={amountToString(transaction.amountEffective)} kind='positive' /> - <Part big title="Refund amount" text={amountToString(transaction.amountRaw)} kind='neutral' /> - <Part big title="Fee" text={amountToString(fee)} kind='negative' /> - <Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' /> - <Part title="Purchase" text={transaction.info.summary} kind='neutral' /> - <Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' /> - - <p> - {transaction.info.summary} - </p> - <div> - {transaction.info.products && transaction.info.products.length > 0 && - <ListOfProducts> - {transaction.info.products.map((p, k) => <RowBorderGray key={k}> - <a href="#" onClick={showLargePic}> - <img src={p.image ? p.image : emptyImg} /> - </a> - <div> - {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>} - <div>{p.description}</div> - </div> - </RowBorderGray>)} - </ListOfProducts> - } - </div> - </TransactionTemplate> - } + ).amount; + return ( + <TransactionTemplate> + <h2>Refund</h2> + <div> + {transaction.timestamp.t_ms === "never" + ? "never" + : format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")} + </div> + <br /> + <Part + big + title="Total refund" + text={amountToString(transaction.amountEffective)} + kind="positive" + /> + <Part + big + title="Refund amount" + text={amountToString(transaction.amountRaw)} + kind="neutral" + /> + <Part big title="Fee" text={amountToString(fee)} kind="negative" /> + <Part + title="Merchant" + text={transaction.info.merchant.name} + kind="neutral" + /> + <Part title="Purchase" text={transaction.info.summary} kind="neutral" /> + <Part + title="Receipt" + text={`#${transaction.info.orderId}`} + kind="neutral" + /> + <p>{transaction.info.summary}</p> + <div> + {transaction.info.products && transaction.info.products.length > 0 && ( + <ListOfProducts> + {transaction.info.products.map((p, k) => ( + <RowBorderGray key={k}> + <a href="#" onClick={showLargePic}> + <img src={p.image ? p.image : emptyImg} /> + </a> + <div> + {p.quantity && p.quantity > 0 && ( + <SmallLightText> + x {p.quantity} {p.unit} + </SmallLightText> + )} + <div>{p.description}</div> + </div> + </RowBorderGray> + ))} + </ListOfProducts> + )} + </div> + </TransactionTemplate> + ); + } - return <div></div> + return <div></div>; } diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx index 6579450b3..7e6588fac 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx @@ -15,16 +15,15 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { createExample } from '../test-utils'; -import { View as TestedComponent } from './Welcome'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { createExample } from "../test-utils"; +import { View as TestedComponent } from "./Welcome"; export default { - title: 'wallet/welcome', + title: "wallet/welcome", component: TestedComponent, }; @@ -32,11 +31,11 @@ export const Normal = createExample(TestedComponent, { permissionsEnabled: true, diagnostics: { errors: [], - walletManifestVersion: '1.0', - walletManifestDisplayVersion: '1.0', + walletManifestVersion: "1.0", + walletManifestDisplayVersion: "1.0", firefoxIdbProblem: false, dbOutdated: false, - } + }, }); export const TimedoutDiagnostics = createExample(TestedComponent, { @@ -47,4 +46,3 @@ export const TimedoutDiagnostics = createExample(TestedComponent, { export const RunningDiagnostics = createExample(TestedComponent, { permissionsEnabled: false, }); - diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx index d11070d9a..0b8e5c609 100644 --- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -27,43 +27,55 @@ import { Diagnostics } from "../components/Diagnostics"; import { WalletBox } from "../components/styled"; import { useDiagnostics } from "../hooks/useDiagnostics"; import { WalletDiagnostics } from "@gnu-taler/taler-util"; -import { h } from 'preact'; +import { h } from "preact"; export function WelcomePage() { - const [permissionsEnabled, togglePermissions] = useExtendedPermissions() - const [diagnostics, timedOut] = useDiagnostics() - return <View - permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions} - diagnostics={diagnostics} timedOut={timedOut} - /> + const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); + const [diagnostics, timedOut] = useDiagnostics(); + return ( + <View + permissionsEnabled={permissionsEnabled} + togglePermissions={togglePermissions} + diagnostics={diagnostics} + timedOut={timedOut} + /> + ); } export interface ViewProps { - permissionsEnabled: boolean, - togglePermissions: () => void, - diagnostics: WalletDiagnostics | undefined, - timedOut: boolean, + permissionsEnabled: boolean; + togglePermissions: () => void; + diagnostics: WalletDiagnostics | undefined; + timedOut: boolean; } -export function View({ permissionsEnabled, togglePermissions, diagnostics, timedOut }: ViewProps): JSX.Element { - return (<WalletBox> - <h1>Browser Extension Installed!</h1> - <div> - <p>Thank you for installing the wallet.</p> - <Diagnostics diagnostics={diagnostics} timedOut={timedOut} /> - <h2>Permissions</h2> - <Checkbox label="Automatically open wallet based on page content" - name="perm" - description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)" - enabled={permissionsEnabled} onToggle={togglePermissions} - /> - <h2>Next Steps</h2> - <a href="https://demo.taler.net/" style={{ display: "block" }}> - Try the demo » - </a> - <a href="https://demo.taler.net/" style={{ display: "block" }}> - Learn how to top up your wallet balance » - </a> - </div> - </WalletBox> +export function View({ + permissionsEnabled, + togglePermissions, + diagnostics, + timedOut, +}: ViewProps): JSX.Element { + return ( + <WalletBox> + <h1>Browser Extension Installed!</h1> + <div> + <p>Thank you for installing the wallet.</p> + <Diagnostics diagnostics={diagnostics} timedOut={timedOut} /> + <h2>Permissions</h2> + <Checkbox + label="Automatically open wallet based on page content" + name="perm" + description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)" + enabled={permissionsEnabled} + onToggle={togglePermissions} + /> + <h2>Next Steps</h2> + <a href="https://demo.taler.net/" style={{ display: "block" }}> + Try the demo » + </a> + <a href="https://demo.taler.net/" style={{ display: "block" }}> + Learn how to top up your wallet balance » + </a> + </div> + </WalletBox> ); } |