diff options
Diffstat (limited to 'packages')
6 files changed, 211 insertions, 117 deletions
| diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts index f0434be0e..5a8c7f06f 100644 --- a/packages/taler-util/src/amounts.ts +++ b/packages/taler-util/src/amounts.ts @@ -407,7 +407,7 @@ export class Amounts {      return `${a.currency}:${s}`;    } -  static stringifyValue(a: AmountJson): string { +  static stringifyValue(a: AmountJson, minFractional: number = 0): string {      const av = a.value + Math.floor(a.fraction / amountFractionalBase);      const af = a.fraction % amountFractionalBase;      let s = av.toString(); @@ -416,7 +416,7 @@ export class Amounts {        s = s + ".";        let n = af;        for (let i = 0; i < amountFractionalLength; i++) { -        if (!n) { +        if (!n && i >= minFractional) {            break;          }          s = s + Math.floor((n / amountFractionalBase) * 10).toString(); diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx index b0e339c70..cfcef16d5 100644 --- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx +++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx @@ -22,7 +22,7 @@ export function ErrorMessage({ title, description }: { title?: string|VNode; des    const [showErrorDetail, setShowErrorDetail] = useState(false);    if (!title)      return null; -  return <ErrorBox> +  return <ErrorBox style={{paddingTop: 0, paddingBottom: 0}}>      <div>        <p>{title}</p>        { description && <button onClick={() => { setShowErrorDetail(v => !v); }}> diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 0dbf34b5c..0537621bf 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -520,8 +520,7 @@ export const ErrorBox = styled.div`    justify-content: space-between;    flex-direction: column;    /* margin: 0.5em; */ -  padding-left: 1em; -  padding-right: 1em; +  padding: 1em;    /* width: 100%; */    color: #721c24;    background: #f8d7da; @@ -539,6 +538,19 @@ export const ErrorBox = styled.div`      }    }  ` + +export const SuccessBox = styled(ErrorBox)` +  color: #0f5132; +  background-color: #d1e7dd; +  border-color: #badbcc; +` + +export const WarningBox = styled(ErrorBox)` +  color: #664d03; +  background-color: #fff3cd; +  border-color: #ffecb5; +` +  export const PopupNavigation = styled.div<{ devMode?: boolean }>`    background-color:#0042b2;    height: 35px; diff --git a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx index 3ca30ccb2..622e7950f 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx @@ -30,7 +30,7 @@ export default {    },  }; -export const InsufficientBalance = createExample(TestedComponent, { +export const NoBalance = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.InsufficientBalance,      noncePriv: '', @@ -46,6 +46,27 @@ export const InsufficientBalance = createExample(TestedComponent, {    }  }); +export const NoEnoughBalance = createExample(TestedComponent, { +  payStatus: { +    status: PreparePayResultType.InsufficientBalance, +    noncePriv: '', +    proposalId: "proposal1234", +    contractTerms: { +      merchant: { +        name: 'someone' +      }, +      summary: 'some beers', +      amount: 'USD:10', +    } as Partial<ContractTerms> as any, +    amountRaw: 'USD:10', +  }, +  balance: { +    currency: 'USD', +    fraction: 40000000, +    value: 9 +  } +}); +  export const PaymentPossible = createExample(TestedComponent, {    uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',    payStatus: { @@ -66,6 +87,26 @@ export const PaymentPossible = createExample(TestedComponent, {    }  }); +export const PaymentPossibleWithFee = createExample(TestedComponent, { +  uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0', +  payStatus: { +    status: PreparePayResultType.PaymentPossible, +    amountEffective: 'USD:10.20', +    amountRaw: 'USD:10', +    noncePriv: '', +    contractTerms: { +      nonce: '123213123', +      merchant: { +        name: 'someone' +      }, +      amount: 'USD:10', +      summary: 'some beers', +    } as Partial<ContractTerms> as any, +    contractTermsHash: '123456', +    proposalId: 'proposal1234' +  } +}); +  export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.AlreadyConfirmed, @@ -102,3 +143,22 @@ export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent,      paid: false,    }  }); + +export const AlreadyPaid = createExample(TestedComponent, { +  payStatus: { +    status: PreparePayResultType.AlreadyConfirmed, +    amountEffective: 'USD:10', +    amountRaw: 'USD:10', +    contractTerms: { +      merchant: { +        name: 'someone' +      }, +      fulfillment_message: 'congratulations! you are looking at the fulfillment message! ', +      summary: 'some beers', +      amount: 'USD:10', +    } as Partial<ContractTerms> as any, +    contractTermsHash: '123456', +    proposalId: 'proposal1234', +    paid: true, +  } +}); diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index c0038f8fd..e7a3415ac 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -24,56 +24,45 @@   */  // import * as i18n from "../i18n"; -import { renderAmount, ProgressButton } from "../renderHtml"; -import * as wxApi from "../wxApi"; - -import { useState, useEffect } from "preact/hooks"; - -import { AmountLike, ConfirmPayResultDone, getJsonI18n, i18n } from "@gnu-taler/taler-util"; -import { -  PreparePayResult, -  ConfirmPayResult, -  AmountJson, -  PreparePayResultType, -  Amounts, -  ContractTerms, -  ConfirmPayResultType, -} from "@gnu-taler/taler-util"; -import { JSX, VNode, h, Fragment } from "preact"; -import { ButtonDestructive, ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, WalletAction } from "../components/styled"; +import { AmountJson, AmountLike, Amounts, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, ContractTerms, getJsonI18n, i18n, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util"; +import { Fragment, JSX, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks";  import { LogoHeader } from "../components/LogoHeader";  import { Part } from "../components/Part";  import { QR } from "../components/QR"; +import { ButtonSuccess, LinkSuccess, SuccessBox, WalletAction, WarningBox } from "../components/styled"; +import { useBalances } from "../hooks/useBalances"; +import * as wxApi from "../wxApi";  interface Props {    talerPayUri?: string  } -export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) { -  const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; -  let message; -  if (fulfillmentUrl) { -    message = ( -      <span> -        You have already paid for this article. Click{" "} -        <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again. -      </span> -    ); -  } else { -    message = <span> -      You have already paid for this article:{" "} -      <em> -        {payStatus.contractTerms.fulfillment_message ?? "no message given"} -      </em> -    </span>; -  } -  return <section class="main"> -    <h1>GNU Taler Wallet</h1> -    <article class="fade"> -      {message} -    </article> -  </section> -} +// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) { +//   const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; +//   let message; +//   if (fulfillmentUrl) { +//     message = ( +//       <span> +//         You have already paid for this article. Click{" "} +//         <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again. +//       </span> +//     ); +//   } else { +//     message = <span> +//       You have already paid for this article:{" "} +//       <em> +//         {payStatus.contractTerms.fulfillment_message ?? "no message given"} +//       </em> +//     </span>; +//   } +//   return <section class="main"> +//     <h1>GNU Taler Wallet</h1> +//     <article class="fade"> +//       {message} +//     </article> +//   </section> +// }  const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultDone> => {    if (payStatus.status !== "payment-possible") { @@ -98,6 +87,12 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {    const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);    const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); +  const balance = useBalances() +  const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) + +  const foundBalance = balanceWithoutError.find(b => payStatus && Amounts.parseOrThrow(b.available).currency === Amounts.parseOrThrow(payStatus?.amountRaw).currency) +  const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined +    useEffect(() => {      if (!talerPayUri) return;      const doFetch = async (): Promise<void> => { @@ -115,24 +110,24 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {      return <span>Loading payment information ...</span>;    } -  if (payResult && payResult.type === ConfirmPayResultType.Done) { -    if (payResult.contractTerms.fulfillment_message) { -      const obj = { -        fulfillment_message: payResult.contractTerms.fulfillment_message, -        fulfillment_message_i18n: -          payResult.contractTerms.fulfillment_message_i18n, -      }; -      const msg = getJsonI18n(obj, "fulfillment_message"); -      return ( -        <div> -          <p>Payment succeeded.</p> -          <p>{msg}</p> -        </div> -      ); -    } else { -      return <span>Redirecting ...</span>; -    } -  } +  // if (payResult && payResult.type === ConfirmPayResultType.Done) { +  //   if (payResult.contractTerms.fulfillment_message) { +  //     const obj = { +  //       fulfillment_message: payResult.contractTerms.fulfillment_message, +  //       fulfillment_message_i18n: +  //         payResult.contractTerms.fulfillment_message_i18n, +  //     }; +  //     const msg = getJsonI18n(obj, "fulfillment_message"); +  //     return ( +  //       <div> +  //         <p>Payment succeeded.</p> +  //         <p>{msg}</p> +  //       </div> +  //     ); +  //   } else { +  //     return <span>Redirecting ...</span>; +  //   } +  // }    const onClick = async () => {      try { @@ -147,7 +142,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {    } -  return <PaymentRequestView uri={talerPayUri} payStatus={payStatus} onClick={onClick} payErrMsg={payErrMsg} />; +  return <PaymentRequestView uri={talerPayUri} payStatus={payStatus} onClick={onClick} payErrMsg={payErrMsg} balance={foundAmount} />;  }  export interface PaymentRequestViewProps { @@ -155,8 +150,9 @@ export interface PaymentRequestViewProps {    onClick: () => void;    payErrMsg?: string;    uri: string; +  balance: AmountJson | undefined;  } -export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg }: PaymentRequestViewProps) { +export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance }: PaymentRequestViewProps) {    let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);    const contractTerms: ContractTerms = payStatus.contractTerms; @@ -183,71 +179,98 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg }: Payme      merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;    } -  const [showQR, setShowQR] = useState<boolean>(false) -  const privateUri = payStatus.status !== PreparePayResultType.AlreadyConfirmed ? `${uri}&n=${payStatus.noncePriv}` : uri +  function Alternative() { +    const [showQR, setShowQR] = useState<boolean>(false) +    const privateUri = payStatus.status !== PreparePayResultType.AlreadyConfirmed ? `${uri}&n=${payStatus.noncePriv}` : uri +    return <section> +      <LinkSuccess upperCased onClick={() => setShowQR(qr => !qr)}> +        {!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`} +      </LinkSuccess> +      {showQR && <div> +        <QR text={privateUri} /> +        Scan the QR code or <a href={privateUri}>click here</a> +      </div>} +    </section> +  } + +  function ButtonsSection() { +    if (payErrMsg) { +      return <section> +        <div> +          <p>Payment failed: {payErrMsg}</p> +          <button class="pure-button button-success" onClick={onClick} > +            {i18n.str`Retry`} +          </button> +        </div> +      </section> +    } +    if (payStatus.status === PreparePayResultType.PaymentPossible) { +      return <Fragment> +        <section> +          <ButtonSuccess upperCased> +            {i18n.str`Pay`} {amountToString(payStatus.amountEffective)} +          </ButtonSuccess> +        </section> +        <Alternative /> +      </Fragment> +    } +    if (payStatus.status === PreparePayResultType.InsufficientBalance) { +      return <Fragment> +        <section> +          {balance ? <WarningBox> +            Your balance of {amountToString(balance)} is not enough to pay for this purchase +          </WarningBox> : <WarningBox> +            Your balance is not enough to pay for this purchase. +          </WarningBox>} +        </section> +        <section> +          <ButtonSuccess upperCased> +            {i18n.str`Withdraw digital cash`} +          </ButtonSuccess> +        </section> +        <Alternative /> +      </Fragment> +    } +    if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { +      return <Fragment> +        <section> +          {payStatus.paid && contractTerms.fulfillment_message && <Part title="Merchant message" text={contractTerms.fulfillment_message} kind='neutral' />} +        </section> +        {!payStatus.paid && <Alternative />} +      </Fragment> +    } +    return <span /> +  } +    return <WalletAction>      <LogoHeader /> +      <h2>        {i18n.str`Digital cash payment`}      </h2> +    {payStatus.status === PreparePayResultType.AlreadyConfirmed && +      (payStatus.paid ? <SuccessBox> Already paid </SuccessBox> : <WarningBox> Already confirmed </WarningBox>) +    }      <section> -      {payStatus.status === PreparePayResultType.InsufficientBalance ? -        <Part title="Insufficient balance" text="No enough coins to pay" kind='negative' /> : -        <Part big title="Total amount with fee" text={amountToString(payStatus.amountEffective)} kind='negative' /> +      {payStatus.status !== PreparePayResultType.InsufficientBalance && Amounts.isNonZero(totalFees) && +        <Part big title="Total to pay" text={amountToString(payStatus.amountEffective)} kind='negative' />        }        <Part big title="Purchase amount" text={amountToString(payStatus.amountRaw)} kind='neutral' /> -      {Amounts.isNonZero(totalFees) && <Part big title="Fee" text={amountToString(totalFees)} kind='negative' />} +      {Amounts.isNonZero(totalFees) && <Fragment> +        <Part big title="Fee" text={amountToString(totalFees)} kind='negative' /> +      </Fragment> +      }        <Part title="Merchant" text={contractTerms.merchant.name} kind='neutral' />        <Part title="Purchase" text={contractTerms.summary} kind='neutral' />        {contractTerms.order_id && <Part title="Receipt" text={`#${contractTerms.order_id}`} kind='neutral' />}      </section> -    {showQR && <section> -      <QR text={privateUri} /> -      Scan the QR code or <a href={privateUri}>click here</a> -    </section>} -    <section> -      {payErrMsg ? ( -        <div> -          <p>Payment failed: {payErrMsg}</p> -          <button class="pure-button button-success" onClick={onClick} > -            {i18n.str`Retry`} -          </button> -        </div> -      ) : ( -        payStatus.status === PreparePayResultType.PaymentPossible ? <Fragment> -          <LinkSuccess upperCased onClick={() => setShowQR(qr => !qr)}> -            {!showQR ? i18n.str`Complete with mobile wallet` : i18n.str`Hide QR`} -          </LinkSuccess> -          <ButtonSuccess upperCased> -            {i18n.str`Confirm payment`} -          </ButtonSuccess> -        </Fragment> : ( -          payStatus.status === PreparePayResultType.InsufficientBalance ? <Fragment> -            <LinkSuccess upperCased onClick={() => setShowQR(qr => !qr)}> -              {!showQR ? i18n.str`Pay with other device` : i18n.str`Hide QR`} -            </LinkSuccess> -            <ButtonDestructive upperCased disabled> -              {i18n.str`No enough coins`} -            </ButtonDestructive> -          </Fragment> : -            <Fragment> -              {payStatus.contractTerms.fulfillment_message && <div> -                {payStatus.contractTerms.fulfillment_message} -              </div>} -              <LinkWarning upperCased href={payStatus.contractTerms.fulfillment_url}> -                {i18n.str`Already paid`} -              </LinkWarning> -            </Fragment> - -        ) -      )} +    <ButtonsSection /> -    </section>    </WalletAction>  }  function amountToString(text: AmountLike) {    const aj = Amounts.jsonifyAmount(text) -  const amount = Amounts.stringifyValue(aj) +  const amount = Amounts.stringifyValue(aj, 2)    return `${amount} ${aj.currency}`  } diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx b/packages/taler-wallet-webextension/src/hooks/useBalances.tsx index f12fca21c..503b7a492 100644 --- a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx +++ b/packages/taler-wallet-webextension/src/hooks/useBalances.tsx @@ -32,7 +32,6 @@ export type BalancesHook = BalancesHookOk | BalancesHookError | undefined;  export function useBalances(): BalancesHook {    const [balance, setBalance] = useState<BalancesHook>(undefined); -  console.log('render balance')    useEffect(() => {      async function checkBalance() {        try { | 
