diff options
| author | Sebastian <sebasjm@gmail.com> | 2021-11-15 11:18:58 -0300 | 
|---|---|---|
| committer | Sebastian <sebasjm@gmail.com> | 2021-11-15 11:18:58 -0300 | 
| commit | 1d4815c66c395f4fcc86c30e20f3d005e3cb9ff5 (patch) | |
| tree | 99e8241a5eb5af4d752be93a460004bc0c6255aa /packages/taler-wallet-webextension | |
| parent | 9692f589c687a2ba39a705ca4238cf123f444c61 (diff) | |
prettier
Diffstat (limited to 'packages/taler-wallet-webextension')
86 files changed, 4970 insertions, 3580 deletions
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 3a43f1e76..b3d0b10af 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -13,6 +13,7 @@      "compile": "tsc && rollup -c",      "build-storybook": "build-storybook",      "storybook": "start-storybook -s . -p 6006", +    "pretty": "prettier --write src",      "watch": "tsc --watch & rollup -w -c"    },    "dependencies": { @@ -80,4 +81,4 @@        "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$": "<rootDir>/tests/__mocks__/fileTransformer.js"      }    } -} +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 9edd8ca67..f206fa2dd 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -28,29 +28,29 @@ import { i18n } from "@gnu-taler/taler-util";  import { ComponentChildren, JSX, h } from "preact";  import Match from "preact-router/match";  import { useDevContext } from "./context/devContext"; -import { PopupNavigation } from './components/styled' +import { PopupNavigation } from "./components/styled";  export enum Pages { -  welcome = '/welcome', -  balance = '/balance', -  manual_withdraw = '/manual-withdraw', -  settings = '/settings', -  dev = '/dev', -  cta = '/cta', -  backup = '/backup', -  history = '/history', -  transaction = '/transaction/:tid', -  provider_detail = '/provider/:pid', -  provider_add = '/provider/add', +  welcome = "/welcome", +  balance = "/balance", +  manual_withdraw = "/manual-withdraw", +  settings = "/settings", +  dev = "/dev", +  cta = "/cta", +  backup = "/backup", +  history = "/history", +  transaction = "/transaction/:tid", +  provider_detail = "/provider/:pid", +  provider_add = "/provider/add", -  reset_required = '/reset-required', -  payback = '/payback', -  return_coins = '/return-coins', +  reset_required = "/reset-required", +  payback = "/payback", +  return_coins = "/return-coins", -  pay = '/pay', -  refund = '/refund', -  tips = '/tip', -  withdraw = '/withdraw', +  pay = "/pay", +  refund = "/refund", +  tips = "/tip", +  withdraw = "/withdraw",  }  interface TabProps { @@ -71,23 +71,28 @@ function Tab(props: TabProps): JSX.Element {    );  } -export function NavBar({ devMode, path }: { path: string, devMode: boolean }) { -  return <PopupNavigation devMode={devMode}> -    <div> -      <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> -      <Tab target="/history" current={path}>{i18n.str`History`}</Tab> -      <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab> -      <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab> -      {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>} -    </div> -  </PopupNavigation> +export function NavBar({ devMode, path }: { path: string; devMode: boolean }) { +  return ( +    <PopupNavigation devMode={devMode}> +      <div> +        <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> +        <Tab target="/history" current={path}>{i18n.str`History`}</Tab> +        <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab> +        <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab> +        {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>} +      </div> +    </PopupNavigation> +  );  }  export function WalletNavBar() { -  const { devMode } = useDevContext() -  return <Match>{({ path }: any) => { -    console.log("path", path) -    return <NavBar devMode={devMode} path={path} /> -  }}</Match> +  const { devMode } = useDevContext(); +  return ( +    <Match> +      {({ path }: any) => { +        console.log("path", path); +        return <NavBar devMode={devMode} path={path} />; +      }} +    </Match> +  );  } - diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js index e9492a2fb..8d958d6bd 100644 --- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js +++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js @@ -21,24 +21,23 @@ exports.BrowserCryptoWorkerFactory = void 0;   * @author Florian Dold   */  class BrowserCryptoWorkerFactory { -    startWorker() { -        const workerCtor = Worker; -        const workerPath = "/browserWorkerEntry.js"; -        return new workerCtor(workerPath); -    } -    getConcurrency() { -        let concurrency = 2; -        try { -            // only works in the browser -            // tslint:disable-next-line:no-string-literal -            concurrency = navigator["hardwareConcurrency"]; -            concurrency = Math.max(1, Math.ceil(concurrency / 2)); -        } -        catch (e) { -            concurrency = 2; -        } -        return concurrency; +  startWorker() { +    const workerCtor = Worker; +    const workerPath = "/browserWorkerEntry.js"; +    return new workerCtor(workerPath); +  } +  getConcurrency() { +    let concurrency = 2; +    try { +      // only works in the browser +      // tslint:disable-next-line:no-string-literal +      concurrency = navigator["hardwareConcurrency"]; +      concurrency = Math.max(1, Math.ceil(concurrency / 2)); +    } catch (e) { +      concurrency = 2;      } +    return concurrency; +  }  }  exports.BrowserCryptoWorkerFactory = BrowserCryptoWorkerFactory; -//# sourceMappingURL=browserCryptoWorkerFactory.js.map
\ No newline at end of file +//# sourceMappingURL=browserCryptoWorkerFactory.js.map diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts index a8315dc6d..ab20228ef 100644 --- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts +++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts @@ -19,7 +19,10 @@   * @author Florian Dold   */ -import type { CryptoWorker, CryptoWorkerFactory } from "@gnu-taler/taler-wallet-core"; +import type { +  CryptoWorker, +  CryptoWorkerFactory, +} from "@gnu-taler/taler-wallet-core";  export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {    startWorker(): CryptoWorker { diff --git a/packages/taler-wallet-webextension/src/compat.js b/packages/taler-wallet-webextension/src/compat.js index fdfcbd4b9..48e49a0a7 100644 --- a/packages/taler-wallet-webextension/src/compat.js +++ b/packages/taler-wallet-webextension/src/compat.js @@ -21,41 +21,44 @@ exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;   * WebExtension APIs consistently.   */  function isFirefox() { -    const rt = chrome.runtime; -    if (typeof rt.getBrowserInfo === "function") { -        return true; -    } -    return false; +  const rt = chrome.runtime; +  if (typeof rt.getBrowserInfo === "function") { +    return true; +  } +  return false;  }  exports.isFirefox = isFirefox;  /**   * Check if we are running under nodejs.   */  function isNode() { -    return typeof process !== "undefined" && process.release.name === "node"; +  return typeof process !== "undefined" && process.release.name === "node";  }  exports.isNode = isNode;  function getPermissionsApi() { -    const myBrowser = globalThis.browser; -    if (typeof myBrowser === "object" && -        typeof myBrowser.permissions === "object") { -        return { -            addPermissionsListener: () => { -                // Not supported yet. -            }, -            contains: myBrowser.permissions.contains, -            request: myBrowser.permissions.request, -            remove: myBrowser.permissions.remove, -        }; -    } -    else { -        return { -            addPermissionsListener: chrome.permissions.onAdded.addListener.bind(chrome.permissions.onAdded), -            contains: chrome.permissions.contains, -            request: chrome.permissions.request, -            remove: chrome.permissions.remove, -        }; -    } +  const myBrowser = globalThis.browser; +  if ( +    typeof myBrowser === "object" && +    typeof myBrowser.permissions === "object" +  ) { +    return { +      addPermissionsListener: () => { +        // Not supported yet. +      }, +      contains: myBrowser.permissions.contains, +      request: myBrowser.permissions.request, +      remove: myBrowser.permissions.remove, +    }; +  } else { +    return { +      addPermissionsListener: chrome.permissions.onAdded.addListener.bind( +        chrome.permissions.onAdded, +      ), +      contains: chrome.permissions.contains, +      request: chrome.permissions.request, +      remove: chrome.permissions.remove, +    }; +  }  }  exports.getPermissionsApi = getPermissionsApi; -//# sourceMappingURL=compat.js.map
\ No newline at end of file +//# sourceMappingURL=compat.js.map diff --git a/packages/taler-wallet-webextension/src/components/Checkbox.tsx b/packages/taler-wallet-webextension/src/components/Checkbox.tsx index 2d7b98087..276ac9ff0 100644 --- a/packages/taler-wallet-webextension/src/components/Checkbox.tsx +++ b/packages/taler-wallet-webextension/src/components/Checkbox.tsx @@ -24,7 +24,13 @@ interface Props {    name: string;    description?: string;  } -export function Checkbox({ name, enabled, onToggle, label, description }: Props): JSX.Element { +export function Checkbox({ +  name, +  enabled, +  onToggle, +  label, +  description, +}: Props): JSX.Element {    return (      <div>        <input @@ -32,23 +38,26 @@ export function Checkbox({ name, enabled, onToggle, label, description }: Props)          onClick={onToggle}          type="checkbox"          id={`checkbox-${name}`} -        style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} /> +        style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} +      />        <label          htmlFor={`checkbox-${name}`}          style={{ marginLeft: "0.5em", fontWeight: "bold" }}        >          {label}        </label> -      {description && <span -        style={{ -          color: "#383838", -          fontSize: "smaller", -          display: "block", -          marginLeft: "2em", -        }} -      > -        {description} -      </span>} +      {description && ( +        <span +          style={{ +            color: "#383838", +            fontSize: "smaller", +            display: "block", +            marginLeft: "2em", +          }} +        > +          {description} +        </span> +      )}      </div>    );  } diff --git a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx index 5e30ee3d1..2fc8316f5 100644 --- a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx +++ b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx @@ -16,7 +16,7 @@  import { JSX } from "preact/jsx-runtime";  import { Outlined, StyledCheckboxLabel } from "./styled/index"; -import { h } from 'preact'; +import { h } from "preact";  interface Props {    enabled: boolean; @@ -25,28 +25,39 @@ interface Props {    name: string;  } +const Tick = () => ( +  <svg +    xmlns="http://www.w3.org/2000/svg" +    viewBox="0 0 24 24" +    aria-hidden="true" +    focusable="false" +    style={{ backgroundColor: "green" }} +  > +    <path +      fill="none" +      stroke="white" +      stroke-width="3" +      d="M1.73 12.91l6.37 6.37L22.79 4.59" +    /> +  </svg> +); -const Tick = () => <svg -  xmlns="http://www.w3.org/2000/svg" -  viewBox="0 0 24 24" -  aria-hidden="true" -  focusable="false" -  style={{ backgroundColor: 'green' }} -> -  <path -    fill="none" -    stroke="white" -    stroke-width="3" -    d="M1.73 12.91l6.37 6.37L22.79 4.59" -  /> -</svg> - -export function CheckboxOutlined({ name, enabled, onToggle, label }: Props): JSX.Element { +export function CheckboxOutlined({ +  name, +  enabled, +  onToggle, +  label, +}: Props): JSX.Element {    return (      <Outlined>        <StyledCheckboxLabel onClick={onToggle}>          <span> -          <input type="checkbox" name={name} checked={enabled} disabled={false} /> +          <input +            type="checkbox" +            name={name} +            checked={enabled} +            disabled={false} +          />            <div>              <Tick />            </div> diff --git a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx index f0c682ccb..952df15ae 100644 --- a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx +++ b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx @@ -14,9 +14,15 @@   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ - import { JSX, h } from "preact"; +import { JSX, h } from "preact"; -export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggle: () => void; }): JSX.Element { +export function DebugCheckbox({ +  enabled, +  onToggle, +}: { +  enabled: boolean; +  onToggle: () => void; +}): JSX.Element {    return (      <div>        <input @@ -24,7 +30,8 @@ export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggl          onClick={onToggle}          type="checkbox"          id="checkbox-perm" -        style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} /> +        style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} +      />        <label          htmlFor="checkbox-perm"          style={{ marginLeft: "0.5em", fontWeight: "bold" }} diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx index b48deb847..0f8afd525 100644 --- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx +++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx @@ -21,11 +21,13 @@ import { PageLink } from "../renderHtml";  interface Props {    timedOut: boolean; -  diagnostics: WalletDiagnostics | undefined +  diagnostics: WalletDiagnostics | undefined;  } -export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null { - +export function Diagnostics({ +  timedOut, +  diagnostics, +}: Props): JSX.Element | null {    if (timedOut) {      return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;    } @@ -60,8 +62,8 @@ export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null              <p>                Your wallet database is outdated. Currently automatic migration is                not supported. Please go{" "} -              <PageLink pageName="/reset-required">here</PageLink> to reset -              the wallet database. +              <PageLink pageName="/reset-required">here</PageLink> to reset the +              wallet database.              </p>            ) : null}          </div> diff --git a/packages/taler-wallet-webextension/src/components/EditableText.tsx b/packages/taler-wallet-webextension/src/components/EditableText.tsx index 6f3388bf9..8b3e6d375 100644 --- a/packages/taler-wallet-webextension/src/components/EditableText.tsx +++ b/packages/taler-wallet-webextension/src/components/EditableText.tsx @@ -25,25 +25,37 @@ interface Props {    name: string;    description?: string;  } -export function EditableText({ name, value, onChange, label, description }: Props): JSX.Element { -  const [editing, setEditing] = useState(false) -  const ref = useRef<HTMLInputElement>(null) +export function EditableText({ +  name, +  value, +  onChange, +  label, +  description, +}: Props): JSX.Element { +  const [editing, setEditing] = useState(false); +  const ref = useRef<HTMLInputElement>(null);    let InputText;    if (!editing) { -    InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}> -      <p>{value}</p> -      <button onClick={() => setEditing(true)}>edit</button> -    </div> +    InputText = () => ( +      <div style={{ display: "flex", justifyContent: "space-between" }}> +        <p>{value}</p> +        <button onClick={() => setEditing(true)}>edit</button> +      </div> +    );    } else { -    InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}> -      <input -        value={value} -        ref={ref} -        type="text" -        id={`text-${name}`} -      /> -      <button onClick={() => { if (ref.current) onChange(ref.current.value).then(r => setEditing(false)) }}>confirm</button> -    </div> +    InputText = () => ( +      <div style={{ display: "flex", justifyContent: "space-between" }}> +        <input value={value} ref={ref} type="text" id={`text-${name}`} /> +        <button +          onClick={() => { +            if (ref.current) +              onChange(ref.current.value).then((r) => setEditing(false)); +          }} +        > +          confirm +        </button> +      </div> +    );    }    return (      <div> @@ -54,16 +66,18 @@ export function EditableText({ name, value, onChange, label, description }: Prop          {label}        </label>        <InputText /> -      {description && <span -        style={{ -          color: "#383838", -          fontSize: "smaller", -          display: "block", -          marginLeft: "2em", -        }} -      > -        {description} -      </span>} +      {description && ( +        <span +          style={{ +            color: "#383838", +            fontSize: "smaller", +            display: "block", +            marginLeft: "2em", +          }} +        > +          {description} +        </span> +      )}      </div>    );  } diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx index cfcef16d5..c6b64fb6a 100644 --- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx +++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx @@ -13,22 +13,35 @@   You should have received a copy of the GNU General Public License along with   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ - import { VNode, h } from "preact"; +import { VNode, h } from "preact";  import { useState } from "preact/hooks"; -import arrowDown from '../../static/img/chevron-down.svg'; +import arrowDown from "../../static/img/chevron-down.svg";  import { ErrorBox } from "./styled"; -export function ErrorMessage({ title, description }: { title?: string|VNode; description?: string; }) { +export function ErrorMessage({ +  title, +  description, +}: { +  title?: string | VNode; +  description?: string; +}) {    const [showErrorDetail, setShowErrorDetail] = useState(false); -  if (!title) -    return null; -  return <ErrorBox style={{paddingTop: 0, paddingBottom: 0}}> -    <div> -      <p>{title}</p> -      { description && <button onClick={() => { setShowErrorDetail(v => !v); }}> -        <img style={{ height: '1.5em' }} src={arrowDown} /> -      </button> } -    </div> -    {showErrorDetail && <p>{description}</p>} -  </ErrorBox>; +  if (!title) return null; +  return ( +    <ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}> +      <div> +        <p>{title}</p> +        {description && ( +          <button +            onClick={() => { +              setShowErrorDetail((v) => !v); +            }} +          > +            <img style={{ height: "1.5em" }} src={arrowDown} /> +          </button> +        )} +      </div> +      {showErrorDetail && <p>{description}</p>} +    </ErrorBox> +  );  } diff --git a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx index cfa20280f..6d2731cd8 100644 --- a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx +++ b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx @@ -13,66 +13,80 @@   You should have received a copy of the GNU General Public License along with   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ - import { Fragment, VNode } from "preact" -import { useState } from "preact/hooks" -import { JSXInternal } from "preact/src/jsx" -import { h } from 'preact'; +import { Fragment, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { JSXInternal } from "preact/src/jsx"; +import { h } from "preact";  export function ExchangeXmlTos({ doc }: { doc: Document }) { -  const termsNode = doc.querySelector('[ids=terms-of-service]') +  const termsNode = doc.querySelector("[ids=terms-of-service]");    if (!termsNode) { -    return <div> -      <p>The exchange send us an xml but there is no node with 'ids=terms-of-service'. This is the content:</p> -      <pre>{new XMLSerializer().serializeToString(doc)}</pre> -    </div> +    return ( +      <div> +        <p> +          The exchange send us an xml but there is no node with +          'ids=terms-of-service'. This is the content: +        </p> +        <pre>{new XMLSerializer().serializeToString(doc)}</pre> +      </div> +    );    } -  return <Fragment> -    {Array.from(termsNode.children).map(renderChild)} -  </Fragment> +  return <Fragment>{Array.from(termsNode.children).map(renderChild)}</Fragment>;  }  /**   * Map XML elements into HTML - * @param child  - * @returns  + * @param child + * @returns   */  function renderChild(child: Element): VNode { -  const children = Array.from(child.children) +  const children = Array.from(child.children);    switch (child.nodeName) { -    case 'title': return <header>{child.textContent}</header> -    case '#text': return <Fragment /> -    case 'paragraph': return <p>{child.textContent}</p> -    case 'section': { -      return <AnchorWithOpenState href={`#terms-${child.getAttribute('ids')}`}> -        {children.map(renderChild)} -      </AnchorWithOpenState> +    case "title": +      return <header>{child.textContent}</header>; +    case "#text": +      return <Fragment />; +    case "paragraph": +      return <p>{child.textContent}</p>; +    case "section": { +      return ( +        <AnchorWithOpenState href={`#terms-${child.getAttribute("ids")}`}> +          {children.map(renderChild)} +        </AnchorWithOpenState> +      );      } -    case 'bullet_list': { -      return <ul>{children.map(renderChild)}</ul> +    case "bullet_list": { +      return <ul>{children.map(renderChild)}</ul>;      } -    case 'enumerated_list': { -      return <ol>{children.map(renderChild)}</ol> +    case "enumerated_list": { +      return <ol>{children.map(renderChild)}</ol>;      } -    case 'list_item': { -      return <li>{children.map(renderChild)}</li> +    case "list_item": { +      return <li>{children.map(renderChild)}</li>;      } -    case 'block_quote': { -      return <div>{children.map(renderChild)}</div> +    case "block_quote": { +      return <div>{children.map(renderChild)}</div>;      } -    default: return <div style={{ color: 'red', display: 'hidden' }}>unknown tag {child.nodeName} <a></a></div> +    default: +      return ( +        <div style={{ color: "red", display: "hidden" }}> +          unknown tag {child.nodeName} <a></a> +        </div> +      );    }  }  /**   * Simple anchor with a state persisted into 'data-open' prop - * @returns  + * @returns   */ -function AnchorWithOpenState(props: JSXInternal.HTMLAttributes<HTMLAnchorElement>) { -  const [open, setOpen] = useState<boolean>(false) +function AnchorWithOpenState( +  props: JSXInternal.HTMLAttributes<HTMLAnchorElement>, +) { +  const [open, setOpen] = useState<boolean>(false);    function doClick(e: JSXInternal.TargetedMouseEvent<HTMLAnchorElement>) {      setOpen(!open);      e.preventDefault();    } -  return <a data-open={open ? 'true' : 'false'} onClick={doClick} {...props} /> +  return <a data-open={open ? "true" : "false"} onClick={doClick} {...props} />;  } - diff --git a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx index 9b75c62a1..6c47dc92a 100644 --- a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx +++ b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx @@ -17,15 +17,22 @@  import { h } from "preact";  export function LogoHeader() { -  return <div style={{ -    display: 'flex', -    justifyContent: 'space-around', -    margin: '2em', -  }}> -    <img style={{ -      width: 150, -      height: 70, -    }} src="/static/img/logo-2021.svg" width="150" /> -  </div> - -}
\ No newline at end of file +  return ( +    <div +      style={{ +        display: "flex", +        justifyContent: "space-around", +        margin: "2em", +      }} +    > +      <img +        style={{ +          width: 150, +          height: 70, +        }} +        src="/static/img/logo-2021.svg" +        width="150" +      /> +    </div> +  ); +} diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx index 75c9df16f..c8ecb46d2 100644 --- a/packages/taler-wallet-webextension/src/components/Part.tsx +++ b/packages/taler-wallet-webextension/src/components/Part.tsx @@ -15,18 +15,28 @@   */  import { AmountLike } from "@gnu-taler/taler-util";  import { ExtraLargeText, LargeText, SmallLightText } from "./styled"; -import { h } from 'preact'; +import { h } from "preact"; -export type Kind = 'positive' | 'negative' | 'neutral'; +export type Kind = "positive" | "negative" | "neutral";  interface Props { -  title: string, text: AmountLike, kind: Kind, big?: boolean +  title: string; +  text: AmountLike; +  kind: Kind; +  big?: boolean;  }  export function Part({ text, title, kind, big }: Props) {    const Text = big ? ExtraLargeText : LargeText; -  return <div style={{ margin: '1em' }}> -    <SmallLightText style={{ margin: '.5em' }}>{title}</SmallLightText> -    <Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}> -      {text} -    </Text> -  </div> +  return ( +    <div style={{ margin: "1em" }}> +      <SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText> +      <Text +        style={{ +          color: +            kind == "positive" ? "green" : kind == "negative" ? "red" : "black", +        }} +      > +        {text} +      </Text> +    </div> +  );  } diff --git a/packages/taler-wallet-webextension/src/components/QR.tsx b/packages/taler-wallet-webextension/src/components/QR.tsx index 8e3f69295..4ff1af961 100644 --- a/packages/taler-wallet-webextension/src/components/QR.tsx +++ b/packages/taler-wallet-webextension/src/components/QR.tsx @@ -14,24 +14,35 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ - import { h, VNode } from "preact"; - import { useEffect, useRef } from "preact/hooks"; - import qrcode from "qrcode-generator"; -  - export function QR({ text }: { text: string; }):VNode { -   const divRef = useRef<HTMLDivElement>(null); -   useEffect(() => { -     if (!divRef.current) return -     const qr = qrcode(0, 'L'); -     qr.addData(text); -     qr.make(); -     divRef.current.innerHTML = qr.createSvgTag({ -       scalable: true, -     }); -   }); -  -   return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> -     <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} /> -   </div>; - } - 
\ No newline at end of file +import { h, VNode } from "preact"; +import { useEffect, useRef } from "preact/hooks"; +import qrcode from "qrcode-generator"; + +export function QR({ text }: { text: string }): VNode { +  const divRef = useRef<HTMLDivElement>(null); +  useEffect(() => { +    if (!divRef.current) return; +    const qr = qrcode(0, "L"); +    qr.addData(text); +    qr.make(); +    divRef.current.innerHTML = qr.createSvgTag({ +      scalable: true, +    }); +  }); + +  return ( +    <div +      style={{ +        width: "100%", +        display: "flex", +        flexDirection: "column", +        alignItems: "center", +      }} +    > +      <div +        style={{ width: "50%", minWidth: 200, maxWidth: 300 }} +        ref={divRef} +      /> +    </div> +  ); +} diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx index 536e5b89a..f89ba19b2 100644 --- a/packages/taler-wallet-webextension/src/components/SelectList.tsx +++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx @@ -23,46 +23,67 @@ interface Props {    onChange: (s: string) => void;    label: string;    list: { -    [label: string]: string -  } +    [label: string]: string; +  };    name: string;    description?: string;    canBeNull?: boolean;  } -export function SelectList({ name, value, list, canBeNull, onChange, label, description }: Props): JSX.Element { -  return <div> -    <label -      htmlFor={`text-${name}`} -      style={{ marginLeft: "0.5em", fontWeight: "bold" }} -    > {label}</label> -    <NiceSelect> -      <select name={name} onChange={(e) => { -        console.log(e.currentTarget.value, value) -        onChange(e.currentTarget.value) -      }}> -        {value !== undefined ? <option selected> -          {list[value]} -        </option> : <option selected disabled> -            Select one option -        </option>} -        {Object.keys(list) -          .filter((l) => l !== value) -          .map(key => <option value={key} key={key}>{list[key]}</option>) -        } -      </select> -    </NiceSelect> -    {description && <span -      style={{ -        color: "#383838", -        fontSize: "smaller", -        display: "block", -        marginLeft: "2em", -      }} -    > -      {description} -    </span>} - -  </div> - +export function SelectList({ +  name, +  value, +  list, +  canBeNull, +  onChange, +  label, +  description, +}: Props): JSX.Element { +  return ( +    <div> +      <label +        htmlFor={`text-${name}`} +        style={{ marginLeft: "0.5em", fontWeight: "bold" }} +      > +        {" "} +        {label} +      </label> +      <NiceSelect> +        <select +          name={name} +          onChange={(e) => { +            console.log(e.currentTarget.value, value); +            onChange(e.currentTarget.value); +          }} +        > +          {value !== undefined ? ( +            <option selected>{list[value]}</option> +          ) : ( +            <option selected disabled> +              Select one option +            </option> +          )} +          {Object.keys(list) +            .filter((l) => l !== value) +            .map((key) => ( +              <option value={key} key={key}> +                {list[key]} +              </option> +            ))} +        </select> +      </NiceSelect> +      {description && ( +        <span +          style={{ +            color: "#383838", +            fontSize: "smaller", +            display: "block", +            marginLeft: "2em", +          }} +        > +          {description} +        </span> +      )} +    </div> +  );  } diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index 991e97c94..1917d5627 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -14,18 +14,33 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { AmountString, Timestamp, Transaction, TransactionType } from '@gnu-taler/taler-util'; -import { format, formatDistance } from 'date-fns'; -import { h } from 'preact'; -import imageBank from '../../static/img/ri-bank-line.svg'; -import imageHandHeart from '../../static/img/ri-hand-heart-line.svg'; -import imageRefresh from '../../static/img/ri-refresh-line.svg'; -import imageRefund from '../../static/img/ri-refund-2-line.svg'; -import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg'; +import { +  AmountString, +  Timestamp, +  Transaction, +  TransactionType, +} from "@gnu-taler/taler-util"; +import { format, formatDistance } from "date-fns"; +import { h } from "preact"; +import imageBank from "../../static/img/ri-bank-line.svg"; +import imageHandHeart from "../../static/img/ri-hand-heart-line.svg"; +import imageRefresh from "../../static/img/ri-refresh-line.svg"; +import imageRefund from "../../static/img/ri-refund-2-line.svg"; +import imageShoppingCart from "../../static/img/ri-shopping-cart-line.svg";  import { Pages } from "../NavigationBar"; -import { Column, ExtraLargeText, HistoryRow, SmallLightText, LargeText, LightText } from './styled/index'; +import { +  Column, +  ExtraLargeText, +  HistoryRow, +  SmallLightText, +  LargeText, +  LightText, +} from "./styled/index"; -export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean }): JSX.Element { +export function TransactionItem(props: { +  tx: Transaction; +  multiCurrency: boolean; +}): JSX.Element {    const tx = props.tx;    switch (tx.type) {      case TransactionType.Withdrawal: @@ -112,20 +127,26 @@ export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean  function TransactionLayout(props: TransactionLayoutProps): JSX.Element {    const date = new Date(props.timestamp.t_ms); -  const dateStr = format(date, 'dd MMM, hh:mm') +  const dateStr = format(date, "dd MMM, hh:mm");    return ( -    <HistoryRow href={Pages.transaction.replace(':tid', props.id)}> +    <HistoryRow href={Pages.transaction.replace(":tid", props.id)}>        <img src={props.iconPath} />        <Column>          <LargeText>            <div>{props.title}</div> -          {props.subtitle && <div style={{color:'gray', fontSize:'medium', marginTop: 5}}>{props.subtitle}</div>} +          {props.subtitle && ( +            <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}> +              {props.subtitle} +            </div> +          )}          </LargeText> -        {props.pending && -          <LightText style={{ marginTop: 5, marginBottom: 5 }}>Waiting for confirmation</LightText> -        } -        <SmallLightText style={{marginTop:5 }}>{dateStr}</SmallLightText> +        {props.pending && ( +          <LightText style={{ marginTop: 5, marginBottom: 5 }}> +            Waiting for confirmation +          </LightText> +        )} +        <SmallLightText style={{ marginTop: 5 }}>{dateStr}</SmallLightText>        </Column>        <TransactionAmount          pending={props.pending} @@ -170,14 +191,18 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {        sign = "";    }    return ( -    <Column style={{ -      textAlign: 'center', -      color: -        props.pending ? "gray" : -          (sign === '+' ? 'darkgreen' : -            (sign === '-' ? 'darkred' : -              undefined)) -    }}> +    <Column +      style={{ +        textAlign: "center", +        color: props.pending +          ? "gray" +          : sign === "+" +          ? "darkgreen" +          : sign === "-" +          ? "darkred" +          : undefined, +      }} +    >        <ExtraLargeText>          {sign}          {amount} @@ -187,4 +212,3 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {      </Column>    );  } - diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 65c1f49e9..8b36dbd31 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -14,18 +14,17 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -  // need to import linaria types, otherwise compiler will complain -import type * as Linaria from '@linaria/core'; +import type * as Linaria from "@linaria/core"; -import { styled } from '@linaria/react'; +import { styled } from "@linaria/react";  export const PaymentStatus = styled.div<{ color: string }>`    padding: 5px;    border-radius: 5px;    color: white; -  background-color: ${p => p.color}; -` +  background-color: ${(p) => p.color}; +`;  export const WalletAction = styled.div`    display: flex; @@ -36,9 +35,9 @@ export const WalletAction = styled.div`    margin: auto;    height: 100%; -   +    & h1:first-child { -    margin-top: 0;  +    margin-top: 0;    }    section {      margin-bottom: 2em; @@ -47,7 +46,7 @@ export const WalletAction = styled.div`        margin-left: 8px;      }    } -` +`;  export const WalletActionOld = styled.section`    border: solid 5px black;    border-radius: 10px; @@ -59,17 +58,17 @@ export const WalletActionOld = styled.section`    margin: auto;    height: 100%; -   +    & h1:first-child { -    margin-top: 0;  +    margin-top: 0;    } -` +`;  export const DateSeparator = styled.div`    color: gray; -  margin: .2em; +  margin: 0.2em;    margin-top: 1em; -` +`;  export const WalletBox = styled.div<{ noPadding?: boolean }>`    display: flex;    flex-direction: column; @@ -79,10 +78,10 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`      width: 400px;    }    & > section { -    padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'}; -    padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'}; +    padding-left: ${({ noPadding }) => (noPadding ? "0px" : "8px")}; +    padding-right: ${({ noPadding }) => (noPadding ? "0px" : "8px")};      // this margin will send the section up when used with a header -    margin-bottom: auto;  +    margin-bottom: auto;      overflow: auto;      table td { @@ -128,13 +127,13 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`        margin-left: 8px;      }    } -` +`;  export const Middle = styled.div` -    justify-content: space-around; -    display: flex; -    flex-direction: column; -    height: 100%; -` +  justify-content: space-around; +  display: flex; +  flex-direction: column; +  height: 100%; +`;  export const PopupBox = styled.div<{ noPadding?: boolean }>`    height: 290px; @@ -144,9 +143,9 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`    justify-content: space-between;    & > section { -    padding: ${({ noPadding }) => noPadding ? '0px' : '8px'}; +    padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};      // this margin will send the section up when used with a header -    margin-bottom: auto;  +    margin-bottom: auto;      overflow-y: auto;      table td { @@ -201,8 +200,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`        margin-left: 8px;      }    } - -` +`;  export const Button = styled.button<{ upperCased?: boolean }>`    display: inline-block; @@ -214,7 +212,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`    cursor: pointer;    user-select: none;    box-sizing: border-box; -  text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'}; +  text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};    font-family: inherit;    font-size: 100%; @@ -223,7 +221,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`    color: rgba(0, 0, 0, 0.8); /* rgba supported */    border: 1px solid #999; /*IE 6/7/8*/    border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ -  background-color: '#e6e6e6'; +  background-color: "#e6e6e6";    text-decoration: none;    border-radius: 2px; @@ -263,7 +261,7 @@ export const Link = styled.a<{ upperCased?: boolean }>`    cursor: pointer;    user-select: none;    box-sizing: border-box; -  text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'}; +  text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};    font-family: inherit;    font-size: 100%; @@ -304,9 +302,9 @@ export const FontIcon = styled.div`    text-align: center;    font-weight: bold;    /* vertical-align: text-top; */ -` +`;  export const ButtonBox = styled(Button)` -  padding: .5em; +  padding: 0.5em;    width: fit-content;    height: 2em; @@ -322,89 +320,87 @@ export const ButtonBox = styled(Button)`    border-radius: 4px;    border-color: black;    color: black; -` - +`;  const ButtonVariant = styled(Button)`    color: white;    border-radius: 4px;    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -` +`;  export const ButtonPrimary = styled(ButtonVariant)`    background-color: rgb(66, 184, 221); -` +`;  export const ButtonBoxPrimary = styled(ButtonBox)`    color: rgb(66, 184, 221);    border-color: rgb(66, 184, 221); -` +`;  export const ButtonSuccess = styled(ButtonVariant)`    background-color: #388e3c; -` +`;  export const LinkSuccess = styled(Link)`    color: #388e3c; -` +`;  export const ButtonBoxSuccess = styled(ButtonBox)`    color: #388e3c;    border-color: #388e3c; -` +`;  export const ButtonWarning = styled(ButtonVariant)`    background-color: rgb(223, 117, 20); -` +`;  export const LinkWarning = styled(Link)`    color: rgb(223, 117, 20); -` +`;  export const ButtonBoxWarning = styled(ButtonBox)`    color: rgb(223, 117, 20);    border-color: rgb(223, 117, 20); -` +`;  export const ButtonDestructive = styled(ButtonVariant)`    background-color: rgb(202, 60, 60); -` +`;  export const ButtonBoxDestructive = styled(ButtonBox)`    color: rgb(202, 60, 60);    border-color: rgb(202, 60, 60); -` - +`;  export const BoldLight = styled.div` -color: gray; -font-weight: bold; -` +  color: gray; +  font-weight: bold; +`;  export const Centered = styled.div`    text-align: center;    & > :not(:first-child) {      margin-top: 15px;    } -` +`;  export const Row = styled.div`    display: flex;    margin: 0.5em 0;    justify-content: space-between;    padding: 0.5em; -` +`;  export const Row2 = styled.div`    display: flex;    /* margin: 0.5em 0; */    justify-content: space-between;    padding: 0.5em; -` +`;  export const Column = styled.div`    display: flex;    flex-direction: column;    margin: 0em 1em;    justify-content: space-between; -` +`;  export const RowBorderGray = styled(Row)`    border: 1px solid gray;    /* border-radius: 0.5em; */ -` +`;  export const RowLightBorderGray = styled(Row2)`    border: 1px solid lightgray; @@ -414,7 +410,7 @@ export const RowLightBorderGray = styled(Row2)`      border: 1px solid lightgray;      background-color: red;    } -` +`;  export const HistoryRow = styled.a`    text-decoration: none; @@ -423,7 +419,7 @@ export const HistoryRow = styled.a`    display: flex;    justify-content: space-between;    padding: 0.5em; -   +    border: 1px solid lightgray;    border-top: 0px; @@ -439,7 +435,7 @@ export const HistoryRow = styled.a`      margin-left: auto;      align-self: center;    } -` +`;  export const ListOfProducts = styled.div`    & > div > a > img { @@ -453,62 +449,62 @@ export const ListOfProducts = styled.div`      margin-right: auto;      margin-left: 1em;    } -` +`;  export const LightText = styled.div`    color: gray; -` +`;  export const WarningText = styled.div`    color: rgb(223, 117, 20); -` +`;  export const SmallText = styled.div` -  font-size: small;  -` +  font-size: small; +`;  export const LargeText = styled.div` -  font-size: large;  -` +  font-size: large; +`;  export const ExtraLargeText = styled.div` -  font-size: x-large;  -` +  font-size: x-large; +`;  export const SmallLightText = styled(SmallText)`    color: gray; -` +`;  export const CenteredText = styled.div`    white-space: nowrap;    text-align: center; -` +`;  export const CenteredBoldText = styled(CenteredText)`    white-space: nowrap;    text-align: center;    font-weight: bold;    color: ${((props: any): any => String(props.color) as any) as any}; -` +`;  export const Input = styled.div<{ invalid?: boolean }>`    & label {      display: block;      padding: 5px; -    color: ${({ invalid }) => !invalid ? 'inherit' : 'red'} +    color: ${({ invalid }) => (!invalid ? "inherit" : "red")};    }    & input {      display: block;      padding: 5px;      width: calc(100% - 4px - 10px); -    border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'} +    border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};    } -` +`;  export const InputWithLabel = styled.div<{ invalid?: boolean }>`    & label {      display: block;      padding: 5px; -    color: ${({ invalid }) => !invalid ? 'inherit' : 'red'} +    color: ${({ invalid }) => (!invalid ? "inherit" : "red")};    }    & > div {      position: relative; @@ -516,20 +512,20 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`      top: 0px;      bottom: 0px; -    &  > div { +    & > div {        position: absolute;        background-color: lightgray;        padding: 5px;        margin: 2px;      } -    &  > input { +    & > input {        flex: 1; -      padding: 5px;  -      border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'} +      padding: 5px; +      border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};      }    } -` +`;  export const ErrorBox = styled.div`    border: 2px solid #f5c6cb; @@ -555,22 +551,22 @@ export const ErrorBox = styled.div`        width: 28px;      }    } -` +`;  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; +  background-color: #0042b2;    height: 35px;    justify-content: space-around;    display: flex; @@ -582,7 +578,7 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`    & > div > a {      color: #f8faf7;      display: inline-block; -    width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5}); +    width: calc(400px / ${({ devMode }) => (!devMode ? 4 : 5)});      text-align: center;      text-decoration: none;      vertical-align: middle; @@ -597,7 +593,6 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`  `;  export const NiceSelect = styled.div` -    & > select {      -webkit-appearance: none;      -moz-appearance: none; @@ -617,19 +612,19 @@ export const NiceSelect = styled.div`    display: flex;    /* width: 10em; */    overflow: hidden; -  border-radius: .25em; +  border-radius: 0.25em;    &::after { -    content: '\u25BC'; +    content: "\u25BC";      position: absolute;      top: 0;      right: 0;      padding: 0.5em 1em;      cursor: pointer;      pointer-events: none; -    -webkit-transition: .25s all ease; -    -o-transition: .25s all ease; -    transition: .25s all ease; +    -webkit-transition: 0.25s all ease; +    -o-transition: 0.25s all ease; +    transition: 0.25s all ease;    }    &:hover::after { @@ -639,7 +634,7 @@ export const NiceSelect = styled.div`    &::-ms-expand {      display: none;    } -` +`;  export const Outlined = styled.div`    border: 2px solid #388e3c; @@ -647,13 +642,12 @@ export const Outlined = styled.div`    width: fit-content;    border-radius: 2px;    color: #388e3c; -` +`;  /* { width: "1.5em", height: "1.5em", verticalAlign: "middle" } */  export const CheckboxSuccess = styled.input`    vertical-align: center; - -` +`;  export const TermsSection = styled.a`    border: 1px solid black; @@ -664,13 +658,13 @@ export const TermsSection = styled.a`    text-decoration: none;    color: inherit;    flex-direction: column; -   +    display: flex;    &[data-open="true"] { -    display: flex;  +    display: flex;    }    &[data-open="false"] > *:not(:first-child) { -    display: none;  +    display: none;    }    header { @@ -681,11 +675,11 @@ export const TermsSection = styled.a`      height: auto;    } -  &[data-open="true"] header:after  { -    content: '\\2227'; +  &[data-open="true"] header:after { +    content: "\\2227";    } -  &[data-open="false"] header:after  { -    content: '\\2228'; +  &[data-open="false"] header:after { +    content: "\\2228";    }  `; @@ -712,13 +706,13 @@ export const TermsOfService = styled.div`      padding: 1em;      margin-top: 2px;      margin-bottom: 2px; -     +      display: flex;      &[data-open="true"] { -      display: flex;  +      display: flex;      }      &[data-open="false"] > *:not(:first-child) { -      display: none;  +      display: none;      }      header { @@ -729,22 +723,20 @@ export const TermsOfService = styled.div`        height: auto;      } -    &[data-open="true"] > header:after  { -      content: '\\2227'; +    &[data-open="true"] > header:after { +      content: "\\2227";      } -    &[data-open="false"] > header:after  { -      content: '\\2228'; +    &[data-open="false"] > header:after { +      content: "\\2228";      }    } - -` +`;  export const StyledCheckboxLabel = styled.div`    color: green;    text-transform: uppercase;    /* font-weight: bold; */    text-align: center;    span { -      input {        display: none;        opacity: 0; @@ -758,7 +750,7 @@ export const StyledCheckboxLabel = styled.div`        margin-right: 1em;        border-radius: 2px;        border: 2px solid currentColor; -   +        svg {          transition: transform 0.1s ease-in 25ms;          transform: scale(0); @@ -776,12 +768,11 @@ export const StyledCheckboxLabel = styled.div`    }    input:disabled + div {      color: #959495; -  }; +  }    input:disabled + div + label {      color: #959495; -  }; +  }    input:focus + div + label {      box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;    } - -`
\ No newline at end of file +`; diff --git a/packages/taler-wallet-webextension/src/context/devContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts index ea2ba4ceb..0344df057 100644 --- a/packages/taler-wallet-webextension/src/context/devContext.ts +++ b/packages/taler-wallet-webextension/src/context/devContext.ts @@ -15,13 +15,13 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createContext, h, VNode } from 'preact' -import { useContext, useState } from 'preact/hooks' -import { useLocalStorage } from '../hooks/useLocalStorage'; +import { createContext, h, VNode } from "preact"; +import { useContext, useState } from "preact/hooks"; +import { useLocalStorage } from "../hooks/useLocalStorage";  interface Type {    devMode: boolean; @@ -29,14 +29,14 @@ interface Type {  }  const Context = createContext<Type>({    devMode: false, -  toggleDevMode: () => null -}) +  toggleDevMode: () => null, +});  export const useDevContext = (): Type => useContext(Context);  export const DevContextProvider = ({ children }: { children: any }): VNode => { -  const [value, setter] = useLocalStorage('devMode') -  const devMode = value === "true" -  const toggleDevMode = () => setter(v => !v ? "true" : undefined) +  const [value, setter] = useLocalStorage("devMode"); +  const devMode = value === "true"; +  const toggleDevMode = () => setter((v) => (!v ? "true" : undefined));    return h(Context.Provider, { value: { devMode, toggleDevMode }, children }); -} +}; diff --git a/packages/taler-wallet-webextension/src/context/translation.ts b/packages/taler-wallet-webextension/src/context/translation.ts index 5f57958de..105da9dcf 100644 --- a/packages/taler-wallet-webextension/src/context/translation.ts +++ b/packages/taler-wallet-webextension/src/context/translation.ts @@ -15,54 +15,58 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createContext, h, VNode } from 'preact' -import { useContext, useEffect } from 'preact/hooks' -import { useLang } from '../hooks/useLang' +import { createContext, h, VNode } from "preact"; +import { useContext, useEffect } from "preact/hooks"; +import { useLang } from "../hooks/useLang";  //@ts-ignore: type declaration  import * as jedLib from "jed";  import { strings } from "../i18n/strings"; -import { setupI18n } from '@gnu-taler/taler-util'; +import { setupI18n } from "@gnu-taler/taler-util";  interface Type {    lang: string;    changeLanguage: (l: string) => void;  }  const initial = { -  lang: 'en', +  lang: "en",    changeLanguage: () => {      // do not change anything -  } -} -const Context = createContext<Type>(initial) +  }, +}; +const Context = createContext<Type>(initial);  interface Props { -  initial?: string, -  children: any, -  forceLang?: string +  initial?: string; +  children: any; +  forceLang?: string;  } -//we use forceLang when we don't want to use the saved state, but sone forced  -//runtime lang predefined lang  -export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => { -  const [lang, changeLanguage] = useLang(initial) +//we use forceLang when we don't want to use the saved state, but sone forced +//runtime lang predefined lang +export const TranslationProvider = ({ +  initial, +  children, +  forceLang, +}: Props): VNode => { +  const [lang, changeLanguage] = useLang(initial);    useEffect(() => {      if (forceLang) { -      changeLanguage(forceLang) +      changeLanguage(forceLang);      } -  }) -  useEffect(()=> { -    setupI18n(lang, strings) -  },[lang]) +  }); +  useEffect(() => { +    setupI18n(lang, strings); +  }, [lang]);    if (forceLang) { -    setupI18n(forceLang, strings) +    setupI18n(forceLang, strings);    } else { -    setupI18n(lang, strings)     +    setupI18n(lang, strings);    }    return h(Context.Provider, { value: { lang, changeLanguage }, children }); -} +};  export const useTranslationContext = (): Type => useContext(Context); diff --git a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx index 622e7950f..c2d360d3b 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx @@ -15,150 +15,156 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util'; -import { createExample } from '../test-utils'; -import { PaymentRequestView as TestedComponent } from './Pay'; +import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils"; +import { PaymentRequestView as TestedComponent } from "./Pay";  export default { -  title: 'cta/pay', +  title: "cta/pay",    component: TestedComponent, -  argTypes: { -  }, +  argTypes: {},  };  export const NoBalance = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.InsufficientBalance, -    noncePriv: '', +    noncePriv: "",      proposalId: "proposal1234", -    contractTerms: { +    contractTerms: ({        merchant: { -        name: 'someone' +        name: "someone",        }, -      summary: 'some beers', -      amount: 'USD:10', -    } as Partial<ContractTerms> as any, -    amountRaw: 'USD:10', -  } +      summary: "some beers", +      amount: "USD:10", +    } as Partial<ContractTerms>) as any, +    amountRaw: "USD:10", +  },  });  export const NoEnoughBalance = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.InsufficientBalance, -    noncePriv: '', +    noncePriv: "",      proposalId: "proposal1234", -    contractTerms: { +    contractTerms: ({        merchant: { -        name: 'someone' +        name: "someone",        }, -      summary: 'some beers', -      amount: 'USD:10', -    } as Partial<ContractTerms> as any, -    amountRaw: 'USD:10', +      summary: "some beers", +      amount: "USD:10", +    } as Partial<ContractTerms>) as any, +    amountRaw: "USD:10",    },    balance: { -    currency: 'USD', +    currency: "USD",      fraction: 40000000, -    value: 9 -  } +    value: 9, +  },  });  export const PaymentPossible = createExample(TestedComponent, { -  uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0', +  uri: +    "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",    payStatus: {      status: PreparePayResultType.PaymentPossible, -    amountEffective: 'USD:10', -    amountRaw: 'USD:10', -    noncePriv: '', -    contractTerms: { -      nonce: '123213123', +    amountEffective: "USD:10", +    amountRaw: "USD:10", +    noncePriv: "", +    contractTerms: ({ +      nonce: "123213123",        merchant: { -        name: 'someone' +        name: "someone",        }, -      amount: 'USD:10', -      summary: 'some beers', -    } as Partial<ContractTerms> as any, -    contractTermsHash: '123456', -    proposalId: 'proposal1234' -  } +      amount: "USD:10", +      summary: "some beers", +    } as Partial<ContractTerms>) as any, +    contractTermsHash: "123456", +    proposalId: "proposal1234", +  },  });  export const PaymentPossibleWithFee = createExample(TestedComponent, { -  uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0', +  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', +    amountEffective: "USD:10.20", +    amountRaw: "USD:10", +    noncePriv: "", +    contractTerms: ({ +      nonce: "123213123",        merchant: { -        name: 'someone' +        name: "someone",        }, -      amount: 'USD:10', -      summary: 'some beers', -    } as Partial<ContractTerms> as any, -    contractTermsHash: '123456', -    proposalId: 'proposal1234' -  } +      amount: "USD:10", +      summary: "some beers", +    } as Partial<ContractTerms>) as any, +    contractTermsHash: "123456", +    proposalId: "proposal1234", +  },  });  export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.AlreadyConfirmed, -    amountEffective: 'USD:10', -    amountRaw: 'USD:10', -    contractTerms: { +    amountEffective: "USD:10", +    amountRaw: "USD:10", +    contractTerms: ({        merchant: { -        name: 'someone' +        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', +      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: false, -  } +  },  }); -export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent, { -  payStatus: { -    status: PreparePayResultType.AlreadyConfirmed, -    amountEffective: 'USD:10', -    amountRaw: 'USD:10', -    contractTerms: { -      merchant: { -        name: 'someone' -      }, -      summary: 'some beers', -      amount: 'USD:10', -    } as Partial<ContractTerms> as any, -    contractTermsHash: '123456', -    proposalId: 'proposal1234', -    paid: false, -  } -}); +export const AlreadyConfirmedWithoutFullfilment = createExample( +  TestedComponent, +  { +    payStatus: { +      status: PreparePayResultType.AlreadyConfirmed, +      amountEffective: "USD:10", +      amountRaw: "USD:10", +      contractTerms: ({ +        merchant: { +          name: "someone", +        }, +        summary: "some beers", +        amount: "USD:10", +      } as Partial<ContractTerms>) as any, +      contractTermsHash: "123456", +      proposalId: "proposal1234", +      paid: false, +    }, +  }, +);  export const AlreadyPaid = createExample(TestedComponent, {    payStatus: {      status: PreparePayResultType.AlreadyConfirmed, -    amountEffective: 'USD:10', -    amountRaw: 'USD:10', -    contractTerms: { +    amountEffective: "USD:10", +    amountRaw: "USD:10", +    contractTerms: ({        merchant: { -        name: 'someone' +        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', +      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 675b14ff9..1023013d2 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -24,18 +24,36 @@   */  // import * as i18n from "../i18n"; -import { AmountJson, AmountLike, Amounts, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, ContractTerms, getJsonI18n, i18n, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util"; -import { Fragment, JSX, VNode } from "preact"; +import { +  AmountJson, +  AmountLike, +  Amounts, +  ConfirmPayResult, +  ConfirmPayResultDone, +  ConfirmPayResultType, +  ContractTerms, +  i18n, +  PreparePayResult, +  PreparePayResultType, +} from "@gnu-taler/taler-util"; +import { h, 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, ErrorBox, LinkSuccess, SuccessBox, WalletAction, WarningBox } from "../components/styled"; +import { +  ButtonSuccess, +  ErrorBox, +  LinkSuccess, +  SuccessBox, +  WalletAction, +  WarningBox, +} from "../components/styled";  import { useBalances } from "../hooks/useBalances";  import * as wxApi from "../wxApi";  interface Props { -  talerPayUri?: string +  talerPayUri?: string;  }  // export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) { @@ -64,7 +82,9 @@ interface Props {  //   </section>  // } -const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultDone> => { +const doPayment = async ( +  payStatus: PreparePayResult, +): Promise<ConfirmPayResultDone> => {    if (payStatus.status !== "payment-possible") {      throw Error(`invalid state: ${payStatus.status}`);    } @@ -80,18 +100,29 @@ const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultD    return res;  }; - -  export function PayPage({ talerPayUri }: Props): JSX.Element { -  const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined); -  const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined); +  const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>( +    undefined, +  ); +  const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>( +    undefined, +  );    const [payErrMsg, setPayErrMsg] = useState<string | undefined>(undefined); -  const balance = useBalances() -  const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || []) +  const balance = useBalances(); +  const balanceWithoutError = balance?.hasError +    ? [] +    : 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 +  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; @@ -101,7 +132,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {          setPayStatus(p);        } catch (e) {          if (e instanceof Error) { -          setPayErrMsg(e.message) +          setPayErrMsg(e.message);          }        }      }; @@ -109,30 +140,28 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {    }, [talerPayUri]);    if (!talerPayUri) { -    return <span>missing pay uri</span> +    return <span>missing pay uri</span>;    }    if (!payStatus) {      if (payErrMsg) { -      return <WalletAction> -        <LogoHeader /> -        <h2> -          {i18n.str`Digital cash payment`} -        </h2> -        <section> -          <p>Could not get the payment information for this order</p> -          <ErrorBox> -            {payErrMsg} -          </ErrorBox> -        </section> -      </WalletAction> +      return ( +        <WalletAction> +          <LogoHeader /> +          <h2>{i18n.str`Digital cash payment`}</h2> +          <section> +            <p>Could not get the payment information for this order</p> +            <ErrorBox>{payErrMsg}</ErrorBox> +          </section> +        </WalletAction> +      );      }      return <span>Loading payment information ...</span>;    }    const onClick = async () => {      try { -      const res = await doPayment(payStatus) +      const res = await doPayment(payStatus);        setPayResult(res);      } catch (e) {        console.error(e); @@ -140,13 +169,18 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {          setPayErrMsg(e.message);        }      } +  }; -  } - -  return <PaymentRequestView uri={talerPayUri} -    payStatus={payStatus} payResult={payResult} -    onClick={onClick} payErrMsg={payErrMsg} -    balance={foundAmount} />; +  return ( +    <PaymentRequestView +      uri={talerPayUri} +      payStatus={payStatus} +      payResult={payResult} +      onClick={onClick} +      payErrMsg={payErrMsg} +      balance={foundAmount} +    /> +  );  }  export interface PaymentRequestViewProps { @@ -157,7 +191,14 @@ export interface PaymentRequestViewProps {    uri: string;    balance: AmountJson | undefined;  } -export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrMsg, balance }: PaymentRequestViewProps) { +export function PaymentRequestView({ +  uri, +  payStatus, +  payResult, +  onClick, +  payErrMsg, +  balance, +}: PaymentRequestViewProps) {    let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);    const contractTerms: ContractTerms = payStatus.contractTerms; @@ -185,116 +226,174 @@ export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrM    }    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> +    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 (payResult) {        if (payResult.type === ConfirmPayResultType.Pending) { -        return <section> -          <div> -            <p>Processing...</p> -          </div> -        </section> +        return ( +          <section> +            <div> +              <p>Processing...</p> +            </div> +          </section> +        );        } -      return null +      return null;      }      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> +      return (          <section> -          <ButtonSuccess upperCased onClick={onClick}> -            {i18n.str`Pay`} {amountToString(payStatus.amountEffective)} -          </ButtonSuccess> +          <div> +            <p>Payment failed: {payErrMsg}</p> +            <button class="pure-button button-success" onClick={onClick}> +              {i18n.str`Retry`} +            </button> +          </div>          </section> -        <Alternative /> -      </Fragment> +      ); +    } +    if (payStatus.status === PreparePayResultType.PaymentPossible) { +      return ( +        <Fragment> +          <section> +            <ButtonSuccess upperCased onClick={onClick}> +              {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> +      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 ( +        <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 <span />;    } -  return <WalletAction> -    <LogoHeader /> - -    <h2> -      {i18n.str`Digital cash payment`} -    </h2> -    {payStatus.status === PreparePayResultType.AlreadyConfirmed && -      (payStatus.paid ? <SuccessBox> Already paid </SuccessBox> : <WarningBox> Already claimed </WarningBox>) -    } -    {payResult && payResult.type === ConfirmPayResultType.Done && ( -      <SuccessBox> -        <h3>Payment complete</h3> -        <p>{!payResult.contractTerms.fulfillment_message ? -          "You will now be sent back to the merchant you came from." : -          payResult.contractTerms.fulfillment_message -        }</p> -      </SuccessBox> -    )} -    <section> -      {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) && <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> -    <ButtonsSection /> +  return ( +    <WalletAction> +      <LogoHeader /> -  </WalletAction> +      <h2>{i18n.str`Digital cash payment`}</h2> +      {payStatus.status === PreparePayResultType.AlreadyConfirmed && +        (payStatus.paid ? ( +          <SuccessBox> Already paid </SuccessBox> +        ) : ( +          <WarningBox> Already claimed </WarningBox> +        ))} +      {payResult && payResult.type === ConfirmPayResultType.Done && ( +        <SuccessBox> +          <h3>Payment complete</h3> +          <p> +            {!payResult.contractTerms.fulfillment_message +              ? "You will now be sent back to the merchant you came from." +              : payResult.contractTerms.fulfillment_message} +          </p> +        </SuccessBox> +      )} +      <section> +        {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) && ( +          <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> +      <ButtonsSection /> +    </WalletAction> +  );  }  function amountToString(text: AmountLike) { -  const aj = Amounts.jsonifyAmount(text) -  const amount = Amounts.stringifyValue(aj, 2) -  return `${amount} ${aj.currency}` +  const aj = Amounts.jsonifyAmount(text); +  const amount = Amounts.stringifyValue(aj, 2); +  return `${amount} ${aj.currency}`;  } diff --git a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx index 88e714cb7..a0abcea58 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx @@ -15,63 +15,61 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { OrderShortInfo } from '@gnu-taler/taler-util'; -import { createExample } from '../test-utils'; -import { View as TestedComponent } from './Refund'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { OrderShortInfo } from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils"; +import { View as TestedComponent } from "./Refund";  export default { -  title: 'cta/refund', +  title: "cta/refund",    component: TestedComponent, -  argTypes: { -  }, +  argTypes: {},  };  export const Complete = createExample(TestedComponent, {    applyResult: { -    amountEffectivePaid: 'USD:10', -    amountRefundGone: 'USD:0', -    amountRefundGranted: 'USD:2', -    contractTermsHash: 'QWEASDZXC', -    info: { -      summary: 'tasty cold beer', -      contractTermsHash: 'QWEASDZXC', -    } as Partial<OrderShortInfo> as any, +    amountEffectivePaid: "USD:10", +    amountRefundGone: "USD:0", +    amountRefundGranted: "USD:2", +    contractTermsHash: "QWEASDZXC", +    info: ({ +      summary: "tasty cold beer", +      contractTermsHash: "QWEASDZXC", +    } as Partial<OrderShortInfo>) as any,      pendingAtExchange: false,      proposalId: "proposal123", -  } +  },  });  export const Partial = createExample(TestedComponent, {    applyResult: { -    amountEffectivePaid: 'USD:10', -    amountRefundGone: 'USD:1', -    amountRefundGranted: 'USD:2', -    contractTermsHash: 'QWEASDZXC', -    info: { -      summary: 'tasty cold beer', -      contractTermsHash: 'QWEASDZXC', -    } as Partial<OrderShortInfo> as any, +    amountEffectivePaid: "USD:10", +    amountRefundGone: "USD:1", +    amountRefundGranted: "USD:2", +    contractTermsHash: "QWEASDZXC", +    info: ({ +      summary: "tasty cold beer", +      contractTermsHash: "QWEASDZXC", +    } as Partial<OrderShortInfo>) as any,      pendingAtExchange: false,      proposalId: "proposal123", -  } +  },  });  export const InProgress = createExample(TestedComponent, {    applyResult: { -    amountEffectivePaid: 'USD:10', -    amountRefundGone: 'USD:1', -    amountRefundGranted: 'USD:2', -    contractTermsHash: 'QWEASDZXC', -    info: { -      summary: 'tasty cold beer', -      contractTermsHash: 'QWEASDZXC', -    } as Partial<OrderShortInfo> as any, +    amountEffectivePaid: "USD:10", +    amountRefundGone: "USD:1", +    amountRefundGranted: "USD:2", +    contractTermsHash: "QWEASDZXC", +    info: ({ +      summary: "tasty cold beer", +      contractTermsHash: "QWEASDZXC", +    } as Partial<OrderShortInfo>) as any,      pendingAtExchange: true,      proposalId: "proposal123", -  } +  },  }); diff --git a/packages/taler-wallet-webextension/src/cta/Refund.tsx b/packages/taler-wallet-webextension/src/cta/Refund.tsx index 943095360..aa11dca6a 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx @@ -22,45 +22,46 @@  import * as wxApi from "../wxApi";  import { AmountView } from "../renderHtml"; -import { -  ApplyRefundResponse, -  Amounts, -} from "@gnu-taler/taler-util"; +import { ApplyRefundResponse, Amounts } from "@gnu-taler/taler-util";  import { useEffect, useState } from "preact/hooks";  import { JSX } from "preact/jsx-runtime"; -import { h } from 'preact'; +import { h } from "preact";  interface Props { -  talerRefundUri?: string +  talerRefundUri?: string;  }  export interface ViewProps {    applyResult: ApplyRefundResponse;  }  export function View({ applyResult }: ViewProps) { -  return <section class="main"> -    <h1>GNU Taler Wallet</h1> -    <article class="fade"> -      <h2>Refund Status</h2> -      <p> -        The product <em>{applyResult.info.summary}</em> has received a total -        effective refund of{" "} -        <AmountView amount={applyResult.amountRefundGranted} />. -      </p> -      {applyResult.pendingAtExchange ? ( -        <p>Refund processing is still in progress.</p> -      ) : null} -      {!Amounts.isZero(applyResult.amountRefundGone) ? ( +  return ( +    <section class="main"> +      <h1>GNU Taler Wallet</h1> +      <article class="fade"> +        <h2>Refund Status</h2>          <p> -          The refund amount of{" "} -          <AmountView amount={applyResult.amountRefundGone} />{" "} -          could not be applied. +          The product <em>{applyResult.info.summary}</em> has received a total +          effective refund of{" "} +          <AmountView amount={applyResult.amountRefundGranted} />.          </p> -      ) : null} -    </article> -  </section> +        {applyResult.pendingAtExchange ? ( +          <p>Refund processing is still in progress.</p> +        ) : null} +        {!Amounts.isZero(applyResult.amountRefundGone) ? ( +          <p> +            The refund amount of{" "} +            <AmountView amount={applyResult.amountRefundGone} /> could not be +            applied. +          </p> +        ) : null} +      </article> +    </section> +  );  }  export function RefundPage({ talerRefundUri }: Props): JSX.Element { -  const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined); +  const [applyResult, setApplyResult] = useState< +    ApplyRefundResponse | undefined +  >(undefined);    const [errMsg, setErrMsg] = useState<string | undefined>(undefined);    useEffect(() => { diff --git a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx index 389b183f0..8da599513 100644 --- a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx @@ -15,45 +15,43 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { createExample } from '../test-utils'; -import { View as TestedComponent } from './Tip'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { createExample } from "../test-utils"; +import { View as TestedComponent } from "./Tip";  export default { -  title: 'cta/tip', +  title: "cta/tip",    component: TestedComponent, -  argTypes: { -  }, +  argTypes: {},  };  export const Accepted = createExample(TestedComponent, {    prepareTipResult: {      accepted: true, -    merchantBaseUrl: '', -    exchangeBaseUrl: '', -    expirationTimestamp : { -      t_ms: 0 +    merchantBaseUrl: "", +    exchangeBaseUrl: "", +    expirationTimestamp: { +      t_ms: 0,      }, -    tipAmountEffective: 'USD:10', -    tipAmountRaw: 'USD:5', -    walletTipId: 'id' -  } +    tipAmountEffective: "USD:10", +    tipAmountRaw: "USD:5", +    walletTipId: "id", +  },  });  export const NotYetAccepted = createExample(TestedComponent, {    prepareTipResult: {      accepted: false, -    merchantBaseUrl: 'http://merchant.url/', -    exchangeBaseUrl: 'http://exchange.url/', -    expirationTimestamp : { -      t_ms: 0 +    merchantBaseUrl: "http://merchant.url/", +    exchangeBaseUrl: "http://exchange.url/", +    expirationTimestamp: { +      t_ms: 0,      }, -    tipAmountEffective: 'USD:10', -    tipAmountRaw: 'USD:5', -    walletTipId: 'id' -  } +    tipAmountEffective: "USD:10", +    tipAmountRaw: "USD:5", +    walletTipId: "id", +  },  }); diff --git a/packages/taler-wallet-webextension/src/cta/Tip.tsx b/packages/taler-wallet-webextension/src/cta/Tip.tsx index dc1feaed3..0a1c1238c 100644 --- a/packages/taler-wallet-webextension/src/cta/Tip.tsx +++ b/packages/taler-wallet-webextension/src/cta/Tip.tsx @@ -25,43 +25,43 @@ import { PrepareTipResult } from "@gnu-taler/taler-util";  import { AmountView } from "../renderHtml";  import * as wxApi from "../wxApi";  import { JSX } from "preact/jsx-runtime"; -import { h } from 'preact'; +import { h } from "preact";  interface Props { -  talerTipUri?: string +  talerTipUri?: string;  }  export interface ViewProps {    prepareTipResult: PrepareTipResult;    onAccept: () => void;    onIgnore: () => void; -  }  export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) { -  return <section class="main"> -    <h1>GNU Taler Wallet</h1> -    <article class="fade"> -      {prepareTipResult.accepted ? ( -        <span> -          Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. Check -        your transactions list for more details. -        </span> -      ) : ( +  return ( +    <section class="main"> +      <h1>GNU Taler Wallet</h1> +      <article class="fade"> +        {prepareTipResult.accepted ? ( +          <span> +            Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. +            Check your transactions list for more details. +          </span> +        ) : (            <div>              <p>                The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is -            offering you a tip of{" "} +              offering you a tip of{" "}                <strong>                  <AmountView amount={prepareTipResult.tipAmountEffective} />                </strong>{" "} -            via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code> +              via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>              </p>              <button onClick={onAccept}>Accept tip</button>              <button onClick={onIgnore}>Ignore</button>            </div>          )} -    </article> -  </section> - +      </article> +    </section> +  );  }  export function TipPage({ talerTipUri }: Props): JSX.Element { @@ -105,7 +105,11 @@ export function TipPage({ talerTipUri }: Props): JSX.Element {      return <span>Loading ...</span>;    } -  return <View prepareTipResult={prepareTipResult} -    onAccept={doAccept} onIgnore={doIgnore} -  /> +  return ( +    <View +      prepareTipResult={prepareTipResult} +      onAccept={doAccept} +      onIgnore={doIgnore} +    /> +  );  } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx index 5e29a3e39..90df2a27e 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx @@ -15,23 +15,22 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { amountFractionalBase, Amounts } from '@gnu-taler/taler-util'; -import { ExchangeRecord } from '@gnu-taler/taler-wallet-core'; -import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw'; -import { getMaxListeners } from 'process'; -import { createExample } from '../test-utils'; -import { View as TestedComponent } from './Withdraw'; + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { amountFractionalBase, Amounts } from "@gnu-taler/taler-util"; +import { ExchangeRecord } from "@gnu-taler/taler-wallet-core"; +import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw"; +import { getMaxListeners } from "process"; +import { createExample } from "../test-utils"; +import { View as TestedComponent } from "./Withdraw";  export default { -  title: 'cta/withdraw', +  title: "cta/withdraw",    component: TestedComponent,    argTypes: { -    onSwitchExchange: { action: 'onRetry' }, +    onSwitchExchange: { action: "onRetry" },    },  }; @@ -48,7 +47,7 @@ const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">    </div>  </body>  </html> -` +`;  const termsPlain = `  Terms Of Service  **************** @@ -432,7 +431,7 @@ Questions or comments  We welcome comments, questions, concerns, or suggestions. Please send  us a message on our contact page at legal@taler-systems.com. -` +`;  const termsXml = `<?xml version="1.0" encoding="utf-8"?>  <!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd"> @@ -781,120 +780,131 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>  `;  export const NewTerms = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'new' +    status: "new",    }, -}) +});  export const TermsReviewingPLAIN = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'plain', -      content: termsPlain +      type: "plain", +      content: termsPlain,      }, -    status: 'new' +    status: "new",    }, -  reviewing: true -}) +  reviewing: true, +});  export const TermsReviewingHTML = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'html', -      href: new URL(`data:text/html;base64,${Buffer.from(termsHtml).toString('base64')}`), +      type: "html", +      href: new URL( +        `data:text/html;base64,${Buffer.from(termsHtml).toString("base64")}`, +      ),      }, -    status: 'new' +    status: "new",    }, -  reviewing: true -}) +  reviewing: true, +});  const termsPdf = `  %PDF-1.2  @@ -909,306 +919,330 @@ endobj  trailer  << /Root 3 0 R >>  %%EOF -` +`;  export const TermsReviewingPDF = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'pdf', -      location: new URL(`data:text/html;base64,${Buffer.from(termsPdf).toString('base64')}`), +      type: "pdf", +      location: new URL( +        `data:text/html;base64,${Buffer.from(termsPdf).toString("base64")}`, +      ),      }, -    status: 'new' +    status: "new",    }, -  reviewing: true -}) - +  reviewing: true, +});  export const TermsReviewingXML = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'new' +    status: "new",    }, -  reviewing: true -}) +  reviewing: true, +});  export const NewTermsAccepted = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'new' +    status: "new",    }, -  reviewed: true -}) +  reviewed: true, +});  export const TermsShowAgainXML = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'new' +    status: "new",    },    reviewed: true,    reviewing: true, -}) +});  export const TermsChanged = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'changed' +    status: "changed",    }, -}) +});  export const TermsNotFound = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: { -    status: 'notfound' +    status: "notfound",    }, -}) +});  export const TermsAlreadyAccepted = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: amountFractionalBase * 0.5, -    value: 0 +    value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: { -    status: 'accepted' +    status: "accepted",    }, -}) - +});  export const WithoutFee = createExample(TestedComponent, { -  knownExchanges: [{ -    currency: 'USD', -    exchangeBaseUrl: 'exchange.demo.taler.net', -    paytoUris: ['asd'], -  }, { -    currency: 'USD', -    exchangeBaseUrl: 'exchange.test.taler.net', -    paytoUris: ['asd'], -  }], -  exchangeBaseUrl: 'exchange.demo.taler.net', +  knownExchanges: [ +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.demo.taler.net", +      paytoUris: ["asd"], +    }, +    { +      currency: "USD", +      exchangeBaseUrl: "exchange.test.taler.net", +      paytoUris: ["asd"], +    }, +  ], +  exchangeBaseUrl: "exchange.demo.taler.net",    details: { -    content: '', -    contentType: '', -    currentEtag: '', +    content: "", +    contentType: "", +    currentEtag: "",      acceptedEtag: undefined,    },    withdrawalFee: { -    currency: 'USD', +    currency: "USD",      fraction: 0,      value: 0,    },    amount: { -    currency: 'USD', +    currency: "USD",      value: 2, -    fraction: 10000000 +    fraction: 10000000,    }, -  onSwitchExchange: async () => { }, +  onSwitchExchange: async () => {},    terms: {      value: { -      type: 'xml', +      type: "xml",        document: new DOMParser().parseFromString(termsXml, "text/xml"),      }, -    status: 'accepted', -  } -})
\ No newline at end of file +    status: "accepted", +  }, +}); diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx index 6ef72cbe6..603dafcde 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx @@ -21,21 +21,39 @@   * @author Florian Dold   */ -import { AmountJson, Amounts, ExchangeListItem, GetExchangeTosResult, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util'; -import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw'; +import { +  AmountJson, +  Amounts, +  ExchangeListItem, +  GetExchangeTosResult, +  i18n, +  WithdrawUriInfoResponse, +} from "@gnu-taler/taler-util"; +import { VNode, h } from "preact";  import { useState } from "preact/hooks"; -import { Fragment } from 'preact/jsx-runtime'; -import { CheckboxOutlined } from '../components/CheckboxOutlined'; -import { ExchangeXmlTos } from '../components/ExchangeToS'; -import { LogoHeader } from '../components/LogoHeader'; -import { Part } from '../components/Part'; -import { SelectList } from '../components/SelectList'; -import { ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, TermsOfService, WalletAction, WarningText } from '../components/styled'; -import { useAsyncAsHook } from '../hooks/useAsyncAsHook'; +import { Fragment } from "preact/jsx-runtime"; +import { CheckboxOutlined } from "../components/CheckboxOutlined"; +import { ExchangeXmlTos } from "../components/ExchangeToS"; +import { LogoHeader } from "../components/LogoHeader"; +import { Part } from "../components/Part"; +import { SelectList } from "../components/SelectList"; +import { +  ButtonSuccess, +  ButtonWarning, +  LinkSuccess, +  TermsOfService, +  WalletAction, +  WarningText, +} from "../components/styled"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook";  import { -  acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges, getExchangeTos +  acceptWithdrawal, +  getExchangeTos, +  getExchangeWithdrawalInfo, +  getWithdrawalDetailsForUri, +  listExchanges, +  setExchangeTosAccepted,  } from "../wxApi"; -import { wxMain } from '../wxBackend.js';  interface Props {    talerWithdrawUri?: string; @@ -58,145 +76,193 @@ export interface ViewProps {      status: TermsStatus;    };    knownExchanges: ExchangeListItem[]; +} -}; - -type TermsStatus = 'new' | 'accepted' | 'changed' | 'notfound'; +type TermsStatus = "new" | "accepted" | "changed" | "notfound"; -type TermsDocument = TermsDocumentXml | TermsDocumentHtml | TermsDocumentPlain | TermsDocumentJson | TermsDocumentPdf; +type TermsDocument = +  | TermsDocumentXml +  | TermsDocumentHtml +  | TermsDocumentPlain +  | TermsDocumentJson +  | TermsDocumentPdf;  interface TermsDocumentXml { -  type: 'xml', -  document: Document, +  type: "xml"; +  document: Document;  }  interface TermsDocumentHtml { -  type: 'html', -  href: URL, +  type: "html"; +  href: URL;  }  interface TermsDocumentPlain { -  type: 'plain', -  content: string, +  type: "plain"; +  content: string;  }  interface TermsDocumentJson { -  type: 'json', -  data: any, +  type: "json"; +  data: any;  }  interface TermsDocumentPdf { -  type: 'pdf', -  location: URL, +  type: "pdf"; +  location: URL;  }  function amountToString(text: AmountJson) { -  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}`;  } -export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, amount, onWithdraw, onSwitchExchange, terms, reviewing, onReview, onAccept, reviewed, confirmed }: ViewProps) { -  const needsReview = terms.status === 'changed' || terms.status === 'new' - -  const [switchingExchange, setSwitchingExchange] = useState<string | undefined>(undefined) -  const exchanges = knownExchanges.reduce((prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {}) +export function View({ +  details, +  withdrawalFee, +  exchangeBaseUrl, +  knownExchanges, +  amount, +  onWithdraw, +  onSwitchExchange, +  terms, +  reviewing, +  onReview, +  onAccept, +  reviewed, +  confirmed, +}: ViewProps) { +  const needsReview = terms.status === "changed" || terms.status === "new"; + +  const [switchingExchange, setSwitchingExchange] = useState< +    string | undefined +  >(undefined); +  const exchanges = knownExchanges.reduce( +    (prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), +    {}, +  );    return (      <WalletAction>        <LogoHeader /> -      <h2> -        {i18n.str`Digital cash withdrawal`} -      </h2> +      <h2>{i18n.str`Digital cash withdrawal`}</h2>        <section> -        <Part title="Total to withdraw" text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} kind='positive' /> -        <Part title="Chosen amount" text={amountToString(amount)} kind='neutral' /> -        {Amounts.isNonZero(withdrawalFee) && -          <Part title="Exchange fee" text={amountToString(withdrawalFee)} kind='negative' /> -        } -        <Part title="Exchange" text={exchangeBaseUrl} kind='neutral' big /> +        <Part +          title="Total to withdraw" +          text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} +          kind="positive" +        /> +        <Part +          title="Chosen amount" +          text={amountToString(amount)} +          kind="neutral" +        /> +        {Amounts.isNonZero(withdrawalFee) && ( +          <Part +            title="Exchange fee" +            text={amountToString(withdrawalFee)} +            kind="negative" +          /> +        )} +        <Part title="Exchange" text={exchangeBaseUrl} kind="neutral" big />        </section> -      {!reviewing && +      {!reviewing && (          <section> -          {switchingExchange !== undefined ? <Fragment> -            <div> -              <SelectList label="Known exchanges" list={exchanges} name="" onChange={onSwitchExchange} /> -            </div> -            <LinkSuccess upperCased onClick={() => onSwitchExchange(switchingExchange)}> -              {i18n.str`Confirm exchange selection`} -            </LinkSuccess> -          </Fragment> -            : <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}> +          {switchingExchange !== undefined ? ( +            <Fragment> +              <div> +                <SelectList +                  label="Known exchanges" +                  list={exchanges} +                  name="" +                  onChange={onSwitchExchange} +                /> +              </div> +              <LinkSuccess +                upperCased +                onClick={() => onSwitchExchange(switchingExchange)} +              > +                {i18n.str`Confirm exchange selection`} +              </LinkSuccess> +            </Fragment> +          ) : ( +            <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}>                {i18n.str`Switch exchange`} -            </LinkSuccess>} - +            </LinkSuccess> +          )}          </section> -      } -      {!reviewing && reviewed && +      )} +      {!reviewing && reviewed && (          <section> -          <LinkSuccess -            upperCased -            onClick={() => onReview(true)} -          > +          <LinkSuccess upperCased onClick={() => onReview(true)}>              {i18n.str`Show terms of service`}            </LinkSuccess>          </section> -      } -      {terms.status === 'notfound' && +      )} +      {terms.status === "notfound" && (          <section>            <WarningText>              {i18n.str`Exchange doesn't have terms of service`}            </WarningText>          </section> -      } -      {reviewing && +      )} +      {reviewing && (          <section> -          {terms.status !== 'accepted' && terms.value && terms.value.type === 'xml' && -            <TermsOfService> -              <ExchangeXmlTos doc={terms.value.document} /> -            </TermsOfService> -          } -          {terms.status !== 'accepted' && terms.value && terms.value.type === 'plain' && -            <div style={{ textAlign: 'left' }}> -              <pre>{terms.value.content}</pre> -            </div> -          } -          {terms.status !== 'accepted' && terms.value && terms.value.type === 'html' && -            <iframe src={terms.value.href.toString()} /> -          } -          {terms.status !== 'accepted' && terms.value && terms.value.type === 'pdf' && -            <a href={terms.value.location.toString()} download="tos.pdf" >Download Terms of Service</a> -          } -        </section>} -      {reviewing && reviewed && +          {terms.status !== "accepted" && +            terms.value && +            terms.value.type === "xml" && ( +              <TermsOfService> +                <ExchangeXmlTos doc={terms.value.document} /> +              </TermsOfService> +            )} +          {terms.status !== "accepted" && +            terms.value && +            terms.value.type === "plain" && ( +              <div style={{ textAlign: "left" }}> +                <pre>{terms.value.content}</pre> +              </div> +            )} +          {terms.status !== "accepted" && +            terms.value && +            terms.value.type === "html" && ( +              <iframe src={terms.value.href.toString()} /> +            )} +          {terms.status !== "accepted" && +            terms.value && +            terms.value.type === "pdf" && ( +              <a href={terms.value.location.toString()} download="tos.pdf"> +                Download Terms of Service +              </a> +            )} +        </section> +      )} +      {reviewing && reviewed && (          <section> -          <LinkSuccess -            upperCased -            onClick={() => onReview(false)} -          > +          <LinkSuccess upperCased onClick={() => onReview(false)}>              {i18n.str`Hide terms of service`}            </LinkSuccess>          </section> -      } -      {(reviewing || reviewed) && +      )} +      {(reviewing || reviewed) && (          <section>            <CheckboxOutlined              name="terms"              enabled={reviewed}              label={i18n.str`I accept the exchange terms of service`}              onToggle={() => { -              onAccept(!reviewed) -              onReview(false) +              onAccept(!reviewed); +              onReview(false);              }}            />          </section> -      } +      )}        {/**         * Main action section         */}        <section> -        {terms.status === 'new' && !reviewed && !reviewing && +        {terms.status === "new" && !reviewed && !reviewing && (            <ButtonSuccess              upperCased              disabled={!exchangeBaseUrl} @@ -204,8 +270,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,            >              {i18n.str`Review exchange terms of service`}            </ButtonSuccess> -        } -        {terms.status === 'changed' && !reviewed && !reviewing && +        )} +        {terms.status === "changed" && !reviewed && !reviewing && (            <ButtonWarning              upperCased              disabled={!exchangeBaseUrl} @@ -213,8 +279,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,            >              {i18n.str`Review new version of terms of service`}            </ButtonWarning> -        } -        {(terms.status === 'accepted' || (needsReview && reviewed)) && +        )} +        {(terms.status === "accepted" || (needsReview && reviewed)) && (            <ButtonSuccess              upperCased              disabled={!exchangeBaseUrl || confirmed} @@ -222,8 +288,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,            >              {i18n.str`Confirm withdrawal`}            </ButtonSuccess> -        } -        {terms.status === 'notfound' && +        )} +        {terms.status === "notfound" && (            <ButtonWarning              upperCased              disabled={!exchangeBaseUrl} @@ -231,60 +297,88 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,            >              {i18n.str`Withdraw anyway`}            </ButtonWarning> -        } +        )}        </section>      </WalletAction> -  ) +  );  } -export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) { -  const [customExchange, setCustomExchange] = useState<string | undefined>(undefined) -  const [errorAccepting, setErrorAccepting] = useState<string | undefined>(undefined) - -  const [reviewing, setReviewing] = useState<boolean>(false) -  const [reviewed, setReviewed] = useState<boolean>(false) -  const [confirmed, setConfirmed] = useState<boolean>(false) - -  const knownExchangesHook = useAsyncAsHook(() => listExchanges()) - -  const knownExchanges = !knownExchangesHook || knownExchangesHook.hasError ? [] : knownExchangesHook.response.exchanges -  const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount) -  const thisCurrencyExchanges = knownExchanges.filter(ex => ex.currency === withdrawAmount.currency) - -  const exchange = customExchange || uriInfo.defaultExchangeBaseUrl || thisCurrencyExchanges[0]?.exchangeBaseUrl +export function WithdrawPageWithParsedURI({ +  uri, +  uriInfo, +}: { +  uri: string; +  uriInfo: WithdrawUriInfoResponse; +}) { +  const [customExchange, setCustomExchange] = useState<string | undefined>( +    undefined, +  ); +  const [errorAccepting, setErrorAccepting] = useState<string | undefined>( +    undefined, +  ); + +  const [reviewing, setReviewing] = useState<boolean>(false); +  const [reviewed, setReviewed] = useState<boolean>(false); +  const [confirmed, setConfirmed] = useState<boolean>(false); + +  const knownExchangesHook = useAsyncAsHook(() => listExchanges()); + +  const knownExchanges = +    !knownExchangesHook || knownExchangesHook.hasError +      ? [] +      : knownExchangesHook.response.exchanges; +  const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount); +  const thisCurrencyExchanges = knownExchanges.filter( +    (ex) => ex.currency === withdrawAmount.currency, +  ); + +  const exchange = +    customExchange || +    uriInfo.defaultExchangeBaseUrl || +    thisCurrencyExchanges[0]?.exchangeBaseUrl;    const detailsHook = useAsyncAsHook(async () => { -    if (!exchange) throw Error('no default exchange') -    const tos = await getExchangeTos(exchange, ['text/xml']) +    if (!exchange) throw Error("no default exchange"); +    const tos = await getExchangeTos(exchange, ["text/xml"]);      const info = await getExchangeWithdrawalInfo({        exchangeBaseUrl: exchange,        amount: withdrawAmount, -      tosAcceptedFormat: ['text/xml'] -    }) -    return { tos, info } -  }) +      tosAcceptedFormat: ["text/xml"], +    }); +    return { tos, info }; +  });    if (!detailsHook) { -    return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>; +    return ( +      <span> +        <i18n.Translate>Getting withdrawal details.</i18n.Translate> +      </span> +    );    }    if (detailsHook.hasError) { -    return <span><i18n.Translate>Problems getting details: {detailsHook.message}</i18n.Translate></span>; +    return ( +      <span> +        <i18n.Translate> +          Problems getting details: {detailsHook.message} +        </i18n.Translate> +      </span> +    );    } -  const details = detailsHook.response +  const details = detailsHook.response;    const onAccept = async (): Promise<void> => {      try { -      await setExchangeTosAccepted(exchange, details.tos.currentEtag) -      setReviewed(true) +      await setExchangeTosAccepted(exchange, details.tos.currentEtag); +      setReviewed(true);      } catch (e) {        if (e instanceof Error) { -        setErrorAccepting(e.message) +        setErrorAccepting(e.message);        }      } -  } +  };    const onWithdraw = async (): Promise<void> => { -    setConfirmed(true) +    setConfirmed(true);      console.log("accepting exchange", exchange);      try {        const res = await acceptWithdrawal(uri, exchange); @@ -293,91 +387,121 @@ export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriIn          document.location.href = res.confirmTransferUrl;        }      } catch (e) { -      setConfirmed(false) +      setConfirmed(false);      }    }; -  const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(details.tos.contentType, details.tos.content); - -  const status: TermsStatus = !termsContent ? 'notfound' : ( -    !details.tos.acceptedEtag ? 'new' : ( -      details.tos.acceptedEtag !== details.tos.currentEtag ? 'changed' : 'accepted' -    )) - - -  return <View onWithdraw={onWithdraw} -    details={details.tos} amount={withdrawAmount} -    exchangeBaseUrl={exchange} -    withdrawalFee={details.info.withdrawFee} //FIXME -    terms={{ -      status, value: termsContent -    }} -    onSwitchExchange={setCustomExchange} -    knownExchanges={knownExchanges} -    confirmed={confirmed} -    reviewed={reviewed} onAccept={onAccept} -    reviewing={reviewing} onReview={setReviewing} -  /> +  const termsContent: TermsDocument | undefined = parseTermsOfServiceContent( +    details.tos.contentType, +    details.tos.content, +  ); + +  const status: TermsStatus = !termsContent +    ? "notfound" +    : !details.tos.acceptedEtag +    ? "new" +    : details.tos.acceptedEtag !== details.tos.currentEtag +    ? "changed" +    : "accepted"; + +  return ( +    <View +      onWithdraw={onWithdraw} +      details={details.tos} +      amount={withdrawAmount} +      exchangeBaseUrl={exchange} +      withdrawalFee={details.info.withdrawFee} //FIXME +      terms={{ +        status, +        value: termsContent, +      }} +      onSwitchExchange={setCustomExchange} +      knownExchanges={knownExchanges} +      confirmed={confirmed} +      reviewed={reviewed} +      onAccept={onAccept} +      reviewing={reviewing} +      onReview={setReviewing} +    /> +  );  } -export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { -  const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) : -    getWithdrawalDetailsForUri({ talerWithdrawUri }) -  ) +export function WithdrawPage({ talerWithdrawUri }: Props): VNode { +  const uriInfoHook = useAsyncAsHook(() => +    !talerWithdrawUri +      ? Promise.reject(undefined) +      : getWithdrawalDetailsForUri({ talerWithdrawUri }), +  );    if (!talerWithdrawUri) { -    return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; +    return ( +      <span> +        <i18n.Translate>missing withdraw uri</i18n.Translate> +      </span> +    );    }    if (!uriInfoHook) { -    return <span><i18n.Translate>Loading...</i18n.Translate></span>; +    return ( +      <span> +        <i18n.Translate>Loading...</i18n.Translate> +      </span> +    );    }    if (uriInfoHook.hasError) { -    return <span><i18n.Translate>This URI is not valid anymore: {uriInfoHook.message}</i18n.Translate></span>; +    return ( +      <span> +        <i18n.Translate> +          This URI is not valid anymore: {uriInfoHook.message} +        </i18n.Translate> +      </span> +    );    } -  return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} /> +  return ( +    <WithdrawPageWithParsedURI +      uri={talerWithdrawUri} +      uriInfo={uriInfoHook.response} +    /> +  );  } -function parseTermsOfServiceContent(type: string, text: string): TermsDocument | undefined { -  if (type === 'text/xml') { +function parseTermsOfServiceContent( +  type: string, +  text: string, +): TermsDocument | undefined { +  if (type === "text/xml") {      try { -      const document = new DOMParser().parseFromString(text, "text/xml") -      return { type: 'xml', document } +      const document = new DOMParser().parseFromString(text, "text/xml"); +      return { type: "xml", document };      } catch (e) { -      console.log(e) -      debugger; +      console.log(e);      } -  } else if (type === 'text/html') { +  } else if (type === "text/html") {      try { -      const href = new URL(text) -      return { type: 'html', href } +      const href = new URL(text); +      return { type: "html", href };      } catch (e) { -      console.log(e) -      debugger; +      console.log(e);      } -  } else if (type === 'text/json') { +  } else if (type === "text/json") {      try { -      const data = JSON.parse(text) -      return { type: 'json', data } +      const data = JSON.parse(text); +      return { type: "json", data };      } catch (e) { -      console.log(e) -      debugger; +      console.log(e);      } -  } else if (type === 'text/pdf') { +  } else if (type === "text/pdf") {      try { -      const location = new URL(text) -      return { type: 'pdf', location } +      const location = new URL(text); +      return { type: "pdf", location };      } catch (e) { -      console.log(e) -      debugger; +      console.log(e);      } -  } else if (type === 'text/plain') { +  } else if (type === "text/plain") {      try { -      const content = text -      return { type: 'plain', content } +      const content = text; +      return { type: "plain", content };      } catch (e) { -      console.log(e) -      debugger; +      console.log(e);      }    } -  return undefined +  return undefined;  } - diff --git a/packages/taler-wallet-webextension/src/cta/payback.tsx b/packages/taler-wallet-webextension/src/cta/payback.tsx index 1e27fd912..60cb8c513 100644 --- a/packages/taler-wallet-webextension/src/cta/payback.tsx +++ b/packages/taler-wallet-webextension/src/cta/payback.tsx @@ -15,7 +15,7 @@   */  import { JSX } from "preact/jsx-runtime"; -import { h } from 'preact'; +import { h } from "preact";  /**   * View and edit auditors. diff --git a/packages/taler-wallet-webextension/src/cta/reset-required.tsx b/packages/taler-wallet-webextension/src/cta/reset-required.tsx index e66c0db57..3949318c4 100644 --- a/packages/taler-wallet-webextension/src/cta/reset-required.tsx +++ b/packages/taler-wallet-webextension/src/cta/reset-required.tsx @@ -63,7 +63,7 @@ class ResetNotification extends Component<any, State> {              type="checkbox"              checked={this.state.checked}              onChange={() => { -              this.setState(prev => ({ checked: prev.checked })) +              this.setState((prev) => ({ checked: prev.checked }));              }}            />{" "}            <label htmlFor="check"> diff --git a/packages/taler-wallet-webextension/src/cta/return-coins.tsx b/packages/taler-wallet-webextension/src/cta/return-coins.tsx index 43d73b5fe..548202cab 100644 --- a/packages/taler-wallet-webextension/src/cta/return-coins.tsx +++ b/packages/taler-wallet-webextension/src/cta/return-coins.tsx @@ -15,7 +15,7 @@   */  import { JSX } from "preact/jsx-runtime"; -import { h } from 'preact'; +import { h } from "preact";  /**   * Return coins to own bank account.   * diff --git a/packages/taler-wallet-webextension/src/custom.d.ts b/packages/taler-wallet-webextension/src/custom.d.ts index 1981067d4..521b824c7 100644 --- a/packages/taler-wallet-webextension/src/custom.d.ts +++ b/packages/taler-wallet-webextension/src/custom.d.ts @@ -21,7 +21,7 @@ declare module "*.png" {    const content: any;    export default content;  } -declare module '*.svg' { +declare module "*.svg" {    const content: any;    export default content;  } diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts index 2131d45cb..aa6695c3e 100644 --- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts +++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts @@ -29,7 +29,7 @@ interface HookError {  export type HookResponse<T> = HookOk<T> | HookError | undefined; -export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> { +export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> {    const [result, setHookResponse] = useState<HookResponse<T>>(undefined);    useEffect(() => {      async function doAsync() { @@ -42,7 +42,7 @@ export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {          }        }      } -    doAsync() +    doAsync();    }, []);    return result;  } diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts index f3b1b3b5f..1aa711a90 100644 --- a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts +++ b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts @@ -17,34 +17,31 @@  import { useEffect, useState } from "preact/hooks";  import * as wxApi from "../wxApi"; -  export interface BackupDeviceName {    name: string; -  update: (s:string) => Promise<void> +  update: (s: string) => Promise<void>;  } -  export function useBackupDeviceName(): BackupDeviceName {    const [status, setStatus] = useState<BackupDeviceName>({ -    name: '', -    update: () => Promise.resolve() -  }) +    name: "", +    update: () => Promise.resolve(), +  });    useEffect(() => {      async function run() {        //create a first list of backup info by currency -      const status = await wxApi.getBackupInfo() +      const status = await wxApi.getBackupInfo();        async function update(newName: string) { -        await wxApi.setWalletDeviceId(newName) -        setStatus(old => ({ ...old, name: newName }))   +        await wxApi.setWalletDeviceId(newName); +        setStatus((old) => ({ ...old, name: newName }));        } -      setStatus({ name: status.deviceId, update }) +      setStatus({ name: status.deviceId, update });      } -    run() -  }, []) +    run(); +  }, []); -  return status +  return status;  } - diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts b/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts index c46ab6a5f..8a8fd6f2f 100644 --- a/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts +++ b/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts @@ -14,11 +14,15 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { ProviderInfo, ProviderPaymentPaid, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; +import { +  ProviderInfo, +  ProviderPaymentPaid, +  ProviderPaymentStatus, +  ProviderPaymentType, +} from "@gnu-taler/taler-wallet-core";  import { useEffect, useState } from "preact/hooks";  import * as wxApi from "../wxApi"; -  export interface BackupStatus {    deviceName: string;    providers: ProviderInfo[]; @@ -32,40 +36,46 @@ function getStatusTypeOrder(t: ProviderPaymentStatus) {      ProviderPaymentType.Unpaid,      ProviderPaymentType.Paid,      ProviderPaymentType.Pending, -  ].indexOf(t.type) +  ].indexOf(t.type);  }  function getStatusPaidOrder(a: ProviderPaymentPaid, b: ProviderPaymentPaid) { -  return a.paidUntil.t_ms === 'never' ? -1 : -    b.paidUntil.t_ms === 'never' ? 1 : -      a.paidUntil.t_ms - b.paidUntil.t_ms +  return a.paidUntil.t_ms === "never" +    ? -1 +    : b.paidUntil.t_ms === "never" +    ? 1 +    : a.paidUntil.t_ms - b.paidUntil.t_ms;  }  export function useBackupStatus(): BackupStatus | undefined { -  const [status, setStatus] = useState<BackupStatus | undefined>(undefined) +  const [status, setStatus] = useState<BackupStatus | undefined>(undefined);    useEffect(() => {      async function run() {        //create a first list of backup info by currency -      const status = await wxApi.getBackupInfo() +      const status = await wxApi.getBackupInfo();        const providers = status.providers.sort((a, b) => { -        if (a.paymentStatus.type === ProviderPaymentType.Paid && b.paymentStatus.type === ProviderPaymentType.Paid) { -          return getStatusPaidOrder(a.paymentStatus, b.paymentStatus) +        if ( +          a.paymentStatus.type === ProviderPaymentType.Paid && +          b.paymentStatus.type === ProviderPaymentType.Paid +        ) { +          return getStatusPaidOrder(a.paymentStatus, b.paymentStatus);          } -        return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus) -      }) +        return ( +          getStatusTypeOrder(a.paymentStatus) - +          getStatusTypeOrder(b.paymentStatus) +        ); +      });        async function sync() { -        await wxApi.syncAllProviders() +        await wxApi.syncAllProviders();        } -       -      setStatus({ deviceName: status.deviceId, providers, sync }) + +      setStatus({ deviceName: status.deviceId, providers, sync });      } -    run() -  }, []) +    run(); +  }, []); -  return status +  return status;  } - - diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.ts b/packages/taler-wallet-webextension/src/hooks/useBalances.ts index 37424fb05..403ce7b87 100644 --- a/packages/taler-wallet-webextension/src/hooks/useBalances.ts +++ b/packages/taler-wallet-webextension/src/hooks/useBalances.ts @@ -18,7 +18,6 @@ import { BalancesResponse } from "@gnu-taler/taler-util";  import { useEffect, useState } from "preact/hooks";  import * as wxApi from "../wxApi"; -  interface BalancesHookOk {    hasError: false;    response: BalancesResponse; @@ -46,7 +45,7 @@ export function useBalances(): BalancesHook {          }        }      } -    checkBalance() +    checkBalance();      return wxApi.onUpdateNotification(checkBalance);    }, []); diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts index 888d4d5f1..48aff2602 100644 --- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts +++ b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts @@ -21,7 +21,7 @@ import * as wxApi from "../wxApi";  export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {    const [timedOut, setTimedOut] = useState(false);    const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>( -    undefined +    undefined,    );    useEffect(() => { @@ -41,5 +41,5 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {      console.log("fetching diagnostics");      doFetch();    }, []); -  return [diagnostics, timedOut] -}
\ No newline at end of file +  return [diagnostics, timedOut]; +} diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts index a92425760..aaab0aa43 100644 --- a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts +++ b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts @@ -19,13 +19,12 @@ import * as wxApi from "../wxApi";  import { getPermissionsApi } from "../compat";  import { extendedPermissions } from "../permissions"; -  export function useExtendedPermissions(): [boolean, () => void] {    const [enabled, setEnabled] = useState(false);    const toggle = () => { -    setEnabled(v => !v); -    handleExtendedPerm(enabled).then(result => { +    setEnabled((v) => !v); +    handleExtendedPerm(enabled).then((result) => {        setEnabled(result);      });    }; @@ -65,5 +64,5 @@ async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {      nextVal = res.newValue;    }    console.log("new permissions applied:", nextVal ?? false); -  return nextVal ?? false -}
\ No newline at end of file +  return nextVal ?? false; +} diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts b/packages/taler-wallet-webextension/src/hooks/useLang.ts index 70b9614f6..cc4ff3fc8 100644 --- a/packages/taler-wallet-webextension/src/hooks/useLang.ts +++ b/packages/taler-wallet-webextension/src/hooks/useLang.ts @@ -14,10 +14,13 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { useNotNullLocalStorage } from './useLocalStorage'; +import { useNotNullLocalStorage } from "./useLocalStorage"; -export function useLang(initial?: string): [string, (s:string) => void] { -  const browserLang: string | undefined = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined; -  const defaultLang = (browserLang || initial || 'en').substring(0, 2) -  return useNotNullLocalStorage('lang-preference', defaultLang) +export function useLang(initial?: string): [string, (s: string) => void] { +  const browserLang: string | undefined = +    typeof window !== "undefined" +      ? navigator.language || (navigator as any).userLanguage +      : undefined; +  const defaultLang = (browserLang || initial || "en").substring(0, 2); +  return useNotNullLocalStorage("lang-preference", defaultLang);  } diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts index 78a8b65d5..3883aff04 100644 --- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts +++ b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts @@ -15,38 +15,52 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */  import { StateUpdater, useState } from "preact/hooks"; -export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] { -  const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => { -    return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue; +export function useLocalStorage( +  key: string, +  initialValue?: string, +): [string | undefined, StateUpdater<string | undefined>] { +  const [storedValue, setStoredValue] = useState<string | undefined>((): +    | string +    | undefined => { +    return typeof window !== "undefined" +      ? window.localStorage.getItem(key) || initialValue +      : initialValue;    }); -  const setValue = (value?: string | ((val?: string) => string | undefined)) => { -    setStoredValue(p => { -      const toStore = value instanceof Function ? value(p) : value +  const setValue = ( +    value?: string | ((val?: string) => string | undefined), +  ) => { +    setStoredValue((p) => { +      const toStore = value instanceof Function ? value(p) : value;        if (typeof window !== "undefined") {          if (!toStore) { -          window.localStorage.removeItem(key) +          window.localStorage.removeItem(key);          } else {            window.localStorage.setItem(key, toStore);          }        } -      return toStore -    }) +      return toStore; +    });    };    return [storedValue, setValue];  }  //TODO: merge with the above function -export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] { +export function useNotNullLocalStorage( +  key: string, +  initialValue: string, +): [string, StateUpdater<string>] {    const [storedValue, setStoredValue] = useState<string>((): string => { -    return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue; +    return typeof window !== "undefined" +      ? window.localStorage.getItem(key) || initialValue +      : initialValue;    });    const setValue = (value: string | ((val: string) => string)) => { @@ -54,7 +68,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri      setStoredValue(valueToStore);      if (typeof window !== "undefined") {        if (!valueToStore) { -        window.localStorage.removeItem(key) +        window.localStorage.removeItem(key);        } else {          window.localStorage.setItem(key, valueToStore);        } diff --git a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts index 6520848a5..ea167463e 100644 --- a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts +++ b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts @@ -32,7 +32,9 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {        //create a first list of backup info by currency        const status = await wxApi.getBackupInfo(); -      const providers = status.providers.filter(p => p.syncProviderBaseUrl === url); +      const providers = status.providers.filter( +        (p) => p.syncProviderBaseUrl === url, +      );        const info = providers.length ? providers[0] : undefined;        async function sync() { diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts index ff9cc029a..96a278401 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts @@ -17,15 +17,18 @@  import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";  import { useEffect, useState } from "preact/hooks"; -export function useTalerActionURL(): [string | undefined, (s: boolean) => void] { +export function useTalerActionURL(): [ +  string | undefined, +  (s: boolean) => void, +] {    const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>( -    undefined +    undefined,    );    const [dismissed, setDismissed] = useState(false);    useEffect(() => {      async function check(): Promise<void> {        const talerUri = await findTalerUriInActiveTab(); -      setTalerActionUrl(talerUri) +      setTalerActionUrl(talerUri);      }      check();    }, []); diff --git a/packages/taler-wallet-webextension/src/i18n/strings.ts b/packages/taler-wallet-webextension/src/i18n/strings.ts index 5b1257830..0fefb0f70 100644 --- a/packages/taler-wallet-webextension/src/i18n/strings.ts +++ b/packages/taler-wallet-webextension/src/i18n/strings.ts @@ -193,7 +193,7 @@ strings["es"] = {        "Order redirected": [""],        "Payment aborted": [""],        "Payment Sent": [""], -      "Backup": ["Resguardo"], +      Backup: ["Resguardo"],        "Order accepted": [""],        "Reserve balance updated": [""],        "Payment refund": [""], diff --git a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx index d256f6d98..232b0da73 100644 --- a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/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: 'popup/backup/list', +  title: "popup/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/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx index dcc5e5313..894c8a791 100644 --- a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx @@ -14,15 +14,28 @@   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 +  BoldLight, +  ButtonPrimary, +  ButtonSuccess, +  Centered, +  CenteredText, +  CenteredBoldText, +  PopupBox, +  RowBorderGray, +  SmallText, +  SmallLightText,  } from "../components/styled";  import { useBackupStatus } from "../hooks/useBackupStatus";  import { Pages } from "../NavigationBar"; @@ -32,49 +45,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 (      <PopupBox>        <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> +      )}      </PopupBox> -  ) +  );  }  interface TransactionLayoutProps { @@ -92,55 +124,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/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx index 382f9b549..80203f6d3 100644 --- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/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: 'popup/balance', +  title: "popup/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,13 +88,15 @@ export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, {    balance: {      hasError: false,      response: { -      balances: [{ -        available: 'USD:2.23', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:5.11', -        requiresUserInput: false -      }] +      balances: [ +        { +          available: "USD:2.23", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:5.11", +          requiresUserInput: false, +        }, +      ],      },    },    Linker: NullLink, @@ -103,13 +106,15 @@ export const SomeCoinsAndMovingMoney = createExample(TestedComponent, {    balance: {      hasError: false,      response: { -      balances: [{ -        available: 'USD:2.23', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:2', -        pendingOutgoing: 'USD:5.11', -        requiresUserInput: false -      }] +      balances: [ +        { +          available: "USD:2.23", +          hasPendingTransactions: false, +          pendingIncoming: "USD:2", +          pendingOutgoing: "USD:5.11", +          requiresUserInput: false, +        }, +      ],      },    },    Linker: NullLink, @@ -119,19 +124,22 @@ export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {    balance: {      hasError: false,      response: { -      balances: [{ -        available: 'USD:2', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:5.1', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'EUR:4', -        hasPendingTransactions: false, -        pendingIncoming: 'EUR:0', -        pendingOutgoing: 'EUR:3.01', -        requiresUserInput: false -      }] +      balances: [ +        { +          available: "USD:2", +          hasPendingTransactions: false, +          pendingIncoming: "USD:5.1", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "EUR:4", +          hasPendingTransactions: false, +          pendingIncoming: "EUR:0", +          pendingOutgoing: "EUR:3.01", +          requiresUserInput: false, +        }, +      ],      },    },    Linker: NullLink, @@ -141,78 +149,89 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {    balance: {      hasError: false,      response: { -      balances: [{ -        available: 'USD:1', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'COL:2000', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'EUR:4', -        hasPendingTransactions: false, -        pendingIncoming: 'EUR:15', -        pendingOutgoing: 'EUR:0', -        requiresUserInput: false -      }] +      balances: [ +        { +          available: "USD:1", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "COL:2000", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "EUR:4", +          hasPendingTransactions: false, +          pendingIncoming: "EUR:15", +          pendingOutgoing: "EUR:0", +          requiresUserInput: false, +        }, +      ],      },    },    Linker: NullLink,  }); -  export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {    balance: {      hasError: false,      response: { -      balances: [{ -        available: 'USD:13451', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'EUR:202.02', -        hasPendingTransactions: false, -        pendingIncoming: 'EUR:0', -        pendingOutgoing: 'EUR:0', -        requiresUserInput: false -      },{ -        available: 'ARS:30', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'JPY:51223233', -        hasPendingTransactions: false, -        pendingIncoming: 'EUR:0', -        pendingOutgoing: 'EUR:0', -        requiresUserInput: false -      },{ -        available: 'JPY:51223233', -        hasPendingTransactions: false, -        pendingIncoming: 'EUR:0', -        pendingOutgoing: 'EUR:0', -        requiresUserInput: false -      },{ -        available: 'DEMOKUDOS:6', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:0', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      },{ -        available: 'TESTKUDOS:6', -        hasPendingTransactions: false, -        pendingIncoming: 'USD:5', -        pendingOutgoing: 'USD:0', -        requiresUserInput: false -      }] +      balances: [ +        { +          available: "USD:13451", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "EUR:202.02", +          hasPendingTransactions: false, +          pendingIncoming: "EUR:0", +          pendingOutgoing: "EUR:0", +          requiresUserInput: false, +        }, +        { +          available: "ARS:30", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "JPY:51223233", +          hasPendingTransactions: false, +          pendingIncoming: "EUR:0", +          pendingOutgoing: "EUR:0", +          requiresUserInput: false, +        }, +        { +          available: "JPY:51223233", +          hasPendingTransactions: false, +          pendingIncoming: "EUR:0", +          pendingOutgoing: "EUR:0", +          requiresUserInput: false, +        }, +        { +          available: "DEMOKUDOS:6", +          hasPendingTransactions: false, +          pendingIncoming: "USD:0", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +        { +          available: "TESTKUDOS:6", +          hasPendingTransactions: false, +          pendingIncoming: "USD:5", +          pendingOutgoing: "USD:0", +          requiresUserInput: false, +        }, +      ],      },    },    Linker: NullLink, diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index 8e5c5c42e..2913f60e0 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -15,20 +15,37 @@   */  import { -  amountFractionalBase, Amounts, -  Balance, BalancesResponse, -  i18n +  amountFractionalBase, +  Amounts, +  Balance, +  BalancesResponse, +  i18n,  } from "@gnu-taler/taler-util";  import { JSX, h, Fragment } from "preact";  import { ErrorMessage } from "../components/ErrorMessage"; -import { PopupBox, Centered, ButtonPrimary, ErrorBox, Middle } from "../components/styled/index"; +import { +  PopupBox, +  Centered, +  ButtonPrimary, +  ErrorBox, +  Middle, +} 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 {    balance: BalancesHook; @@ -46,22 +63,26 @@ function formatPending(entry: Balance): JSX.Element {    if (!Amounts.isZero(pendingIncoming)) {      incoming = ( -      <span><i18n.Translate> -        <span style={{ color: "darkgreen" }} title="incoming amount"> -          {"+"} -          {renderAmount(entry.pendingIncoming)} -        </span>{" "} -      </i18n.Translate></span> +      <span> +        <i18n.Translate> +          <span style={{ color: "darkgreen" }} title="incoming amount"> +            {"+"} +            {renderAmount(entry.pendingIncoming)} +          </span>{" "} +        </i18n.Translate> +      </span>      );    }    if (!Amounts.isZero(pendingOutgoing)) {      payment = ( -      <span><i18n.Translate> -        <span style={{ color: "darkred" }} title="outgoing amount"> -          {"-"} -          {renderAmount(entry.pendingOutgoing)} -        </span>{" "} -      </i18n.Translate></span> +      <span> +        <i18n.Translate> +          <span style={{ color: "darkred" }} title="outgoing amount"> +            {"-"} +            {renderAmount(entry.pendingOutgoing)} +          </span>{" "} +        </i18n.Translate> +      </span>      );    } @@ -80,76 +101,110 @@ function formatPending(entry: Balance): JSX.Element {    );  } - -export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) { - +export function BalanceView({ +  balance, +  Linker, +  goToWalletManualWithdraw, +}: BalanceViewProps) {    function Content() {      if (!balance) { -      return <span /> +      return <span />;      }      if (balance.hasError) { -      return (<section> -        <ErrorBox>{balance.message}</ErrorBox> -        <p> -          Click <Linker pageName="welcome">here</Linker> for help and -          diagnostics. -        </p> -      </section>) +      return ( +        <section> +          <ErrorBox>{balance.message}</ErrorBox> +          <p> +            Click <Linker pageName="welcome">here</Linker> for help and +            diagnostics. +          </p> +        </section> +      );      }      if (balance.response.balances.length === 0) { -      return (<section data-expanded> -        <Middle> -          <p><i18n.Translate> -            You have no balance to show. Need some{" "} -            <Linker pageName="/welcome">help</Linker> getting started? -          </i18n.Translate></p> -        </Middle> -      </section>) +      return ( +        <section data-expanded> +          <Middle> +            <p> +              <i18n.Translate> +                You have no balance to show. Need some{" "} +                <Linker pageName="/welcome">help</Linker> getting started? +              </i18n.Translate> +            </p> +          </Middle> +        </section> +      );      } -    return <section data-expanded data-centered> -      <table style={{width:'100%'}}>{balance.response.balances.map((entry) => { -        const av = Amounts.parseOrThrow(entry.available); -        // Create our number formatter. -        let formatter; -        try { -          formatter = new Intl.NumberFormat('en-US', { -            style: 'currency', -            currency: av.currency, -            currencyDisplay: 'symbol' -            // These options are needed to round to whole numbers if that's what you want. -            //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) -            //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) -          }); -        } catch { -          formatter = new Intl.NumberFormat('en-US', { -            // style: 'currency', -            // currency: av.currency, -            // These options are needed to round to whole numbers if that's what you want. -            //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) -            //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) -          }); -        } - -        const v = formatter.format(av.value + av.fraction / amountFractionalBase); -        const fontSize = v.length < 8 ? '3em' : (v.length < 13 ? '2em' : '1em') -        return (<tr> -          <td style={{ height: 50, fontSize, width: '60%', textAlign: 'right', padding: 0 }}>{v}</td> -          <td style={{ maxWidth: '2em', overflowX: 'hidden' }}>{av.currency}</td> -          <td style={{ fontSize: 'small', color: 'gray' }}>{formatPending(entry)}</td> -        </tr> -        ); -      })}</table> -    </section> +    return ( +      <section data-expanded data-centered> +        <table style={{ width: "100%" }}> +          {balance.response.balances.map((entry) => { +            const av = Amounts.parseOrThrow(entry.available); +            // Create our number formatter. +            let formatter; +            try { +              formatter = new Intl.NumberFormat("en-US", { +                style: "currency", +                currency: av.currency, +                currencyDisplay: "symbol", +                // These options are needed to round to whole numbers if that's what you want. +                //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) +                //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) +              }); +            } catch { +              formatter = new Intl.NumberFormat("en-US", { +                // style: 'currency', +                // currency: av.currency, +                // These options are needed to round to whole numbers if that's what you want. +                //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) +                //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) +              }); +            } + +            const v = formatter.format( +              av.value + av.fraction / amountFractionalBase, +            ); +            const fontSize = +              v.length < 8 ? "3em" : v.length < 13 ? "2em" : "1em"; +            return ( +              <tr> +                <td +                  style={{ +                    height: 50, +                    fontSize, +                    width: "60%", +                    textAlign: "right", +                    padding: 0, +                  }} +                > +                  {v} +                </td> +                <td style={{ maxWidth: "2em", overflowX: "hidden" }}> +                  {av.currency} +                </td> +                <td style={{ fontSize: "small", color: "gray" }}> +                  {formatPending(entry)} +                </td> +              </tr> +            ); +          })} +        </table> +      </section> +    );    } -  return <PopupBox> -    {/* <section> */} -    <Content /> -    {/* </section> */} -    <footer> -      <div /> -      <ButtonPrimary onClick={goToWalletManualWithdraw}>Withdraw</ButtonPrimary> -    </footer> -  </PopupBox> +  return ( +    <PopupBox> +      {/* <section> */} +      <Content /> +      {/* </section> */} +      <footer> +        <div /> +        <ButtonPrimary onClick={goToWalletManualWithdraw}> +          Withdraw +        </ButtonPrimary> +      </footer> +    </PopupBox> +  );  } diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx index ccc747466..8722c1cf8 100644 --- a/packages/taler-wallet-webextension/src/popup/Debug.tsx +++ b/packages/taler-wallet-webextension/src/popup/Debug.tsx @@ -19,13 +19,14 @@ import { Diagnostics } from "../components/Diagnostics";  import { useDiagnostics } from "../hooks/useDiagnostics.js";  import * as wxApi from "../wxApi"; -  export function DeveloperPage(props: any): JSX.Element {    const [status, timedOut] = useDiagnostics();    return (      <div>        <p>Debug tools:</p> -      <button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button> +      <button onClick={openExtensionPage("/static/popup.html")}> +        wallet tab +      </button>        <br />        <button onClick={confirmReset}>reset</button>        <Diagnostics diagnostics={status} timedOut={timedOut} /> @@ -46,7 +47,7 @@ export async function confirmReset(): Promise<void> {    if (      confirm(        "Do you want to IRREVOCABLY DESTROY everything inside your" + -      " wallet and LOSE ALL YOUR COINS?", +        " wallet and LOSE ALL YOUR COINS?",      )    ) {      await wxApi.resetDb(); @@ -61,4 +62,3 @@ export function openExtensionPage(page: string) {      });    };  } - diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx index daa263a81..95f4a547a 100644 --- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.stories.tsx @@ -15,135 +15,149 @@   */  /** -* -* @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 { HistoryView as TestedComponent } from './History'; +  WithdrawalType, +} from "@gnu-taler/taler-util"; +import { createExample } from "../test-utils"; +import { HistoryView as TestedComponent } from "./History";  export default { -  title: 'popup/history/list', +  title: "popup/history/list",    component: TestedComponent,  };  const commonTransaction = { -  amountRaw: 'USD:10', -  amountEffective: 'USD:9', +  amountRaw: "USD:10", +  amountEffective: "USD:9",    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.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: 'the merchant', +        name: "the merchant",        }, -      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://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, -} +};  export const EmptyWithBalance = 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 EmptyWithNoBalance = createExample(TestedComponent, {    list: [], -  balances: [] +  balances: [],  });  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, { @@ -157,13 +171,15 @@ export const Several = createExample(TestedComponent, {      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, { @@ -177,18 +193,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/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx index 1447da9b0..8fe6de16c 100644 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -14,7 +14,13 @@   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { AmountString, Balance, i18n, Transaction, TransactionsResponse } from "@gnu-taler/taler-util"; +import { +  AmountString, +  Balance, +  i18n, +  Transaction, +  TransactionsResponse, +} from "@gnu-taler/taler-util";  import { h, JSX } from "preact";  import { useEffect, useState } from "preact/hooks";  import { PopupBox } from "../components/styled"; @@ -22,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> => { @@ -42,46 +49,79 @@ 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[] }) { -  const multiCurrency = balances.length > 1 -  return <PopupBox noPadding> -    {balances.length > 0 && <header> -      {multiCurrency ? <div class="title"> -        Balance: <ul style={{ margin: 0 }}> -          {balances.map(b => <li>{b.available}</li>)} -        </ul> -      </div> : <div class="title"> -        Balance: <span>{amountToString(balances[0].available)}</span> -      </div>} -    </header>} -    {list.length === 0 ? <section data-expanded data-centered> -      <p><i18n.Translate> -        You have no history yet, here you will be able to check your last transactions. -      </i18n.Translate></p> -    </section> : -      <section> -        {list.slice(0, 3).map((tx, i) => ( -          <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} /> -        ))} -      </section> -    } -    <footer style={{ justifyContent: 'space-around' }}> -      {list.length > 0 && -        <a target="_blank" -          rel="noopener noreferrer" -          style={{ color: 'darkgreen', textDecoration: 'none' }} -          href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a> -      } -    </footer> -  </PopupBox> +export function HistoryView({ +  list, +  balances, +}: { +  list: Transaction[]; +  balances: Balance[]; +}) { +  const multiCurrency = balances.length > 1; +  return ( +    <PopupBox noPadding> +      {balances.length > 0 && ( +        <header> +          {multiCurrency ? ( +            <div class="title"> +              Balance:{" "} +              <ul style={{ margin: 0 }}> +                {balances.map((b) => ( +                  <li>{b.available}</li> +                ))} +              </ul> +            </div> +          ) : ( +            <div class="title"> +              Balance: <span>{amountToString(balances[0].available)}</span> +            </div> +          )} +        </header> +      )} +      {list.length === 0 ? ( +        <section data-expanded data-centered> +          <p> +            <i18n.Translate> +              You have no history yet, here you will be able to check your last +              transactions. +            </i18n.Translate> +          </p> +        </section> +      ) : ( +        <section> +          {list.slice(0, 3).map((tx, i) => ( +            <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} /> +          ))} +        </section> +      )} +      <footer style={{ justifyContent: "space-around" }}> +        {list.length > 0 && ( +          <a +            target="_blank" +            rel="noopener noreferrer" +            style={{ color: "darkgreen", textDecoration: "none" }} +            href={ +              chrome.extension +                ? chrome.extension.getURL(`/static/wallet.html#/history`) +                : "#" +            } +          > +            VIEW MORE TRANSACTIONS +          </a> +        )} +      </footer> +    </PopupBox> +  );  } diff --git a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx index cd443e9d4..5009684c5 100644 --- a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx @@ -15,30 +15,29 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { NavBar as TestedComponent } from '../NavigationBar'; +import { createExample } from "../test-utils"; +import { NavBar as TestedComponent } from "../NavigationBar";  export default { -  title: 'popup/header', +  title: "popup/header",    // component: TestedComponent,    argTypes: { -    onRetry: { action: 'onRetry' }, -    onDelete: { action: 'onDelete' }, -    onBack: { action: 'onBack' }, -  } +    onRetry: { action: "onRetry" }, +    onDelete: { action: "onDelete" }, +    onBack: { action: "onBack" }, +  },  }; -  export const OnBalance = createExample(TestedComponent, { -  devMode:false, -  path:'/balance' +  devMode: false, +  path: "/balance",  });  export const OnHistoryWithDevMode = createExample(TestedComponent, { -  devMode:true, -  path:'/history' +  devMode: true, +  path: "/history",  }); diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx index de1f67b96..0cff7f75f 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/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: 'popup/backup/confirm', +  title: "popup/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/popup/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx index 2daf49e0c..9a2f97051 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/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: 'popup/backup/add', +  title: "popup/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/popup/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx index 4416608f8..fab21398a 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/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: 'popup/backup/details', +  title: "popup/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/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx index 04adbb21c..9617c9a41 100644 --- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/popup/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, PopupBox, SmallLightText } from "../components/styled"; +import { +  Button, +  ButtonDestructive, +  ButtonPrimary, +  PaymentStatus, +  PopupBox, +  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 (      <PopupBox>        <Error info={info} />        <header> -        <h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3> -        <PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus> +        <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>      </PopupBox> -  ) +  );  }  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/popup/Settings.stories.tsx b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx index 06e33c9d3..ae8e54ba1 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx @@ -15,29 +15,28 @@   */  /** -* -* @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: 'popup/settings', +  title: "popup/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(),  }); - diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx index 8595c87ff..3b83f0762 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -14,7 +14,6 @@   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>  */ -  import { i18n } from "@gnu-taler/taler-util";  import { VNode, h } from "preact";  import { Checkbox } from "../components/Checkbox"; @@ -28,15 +27,21 @@ import { useLang } from "../hooks/useLang";  export function SettingsPage(): VNode {    const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); -  const { devMode, toggleDevMode } = useDevContext() -  const { name, update } = useBackupDeviceName() -  const [lang, changeLang] = useLang() -  return <SettingsView -    lang={lang} changeLang={changeLang} -    deviceName={name} setDeviceName={update} -    permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions} -    developerMode={devMode} toggleDeveloperMode={toggleDevMode} -  />; +  const { devMode, toggleDevMode } = useDevContext(); +  const { name, update } = useBackupDeviceName(); +  const [lang, changeLang] = useLang(); +  return ( +    <SettingsView +      lang={lang} +      changeLang={changeLang} +      deviceName={name} +      setDeviceName={update} +      permissionsEnabled={permissionsEnabled} +      togglePermissions={togglePermissions} +      developerMode={devMode} +      toggleDeveloperMode={toggleDevMode} +    /> +  );  }  export interface ViewProps { @@ -50,23 +55,31 @@ export interface ViewProps {    toggleDeveloperMode: () => void;  } -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({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { +export function SettingsView({ +  lang, +  changeLang, +  deviceName, +  setDeviceName, +  permissionsEnabled, +  togglePermissions, +  developerMode, +  toggleDeveloperMode, +}: ViewProps): VNode {    return (      <PopupBox>        <section> @@ -86,25 +99,39 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm            label={i18n.str`Device name`}            description="(This is how you will recognize the wallet in the backup provider)"          /> */} -        <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> -      <footer style={{ justifyContent: 'space-around' }}> -        <a target="_blank" +      <footer style={{ justifyContent: "space-around" }}> +        <a +          target="_blank"            rel="noopener noreferrer" -          style={{ color: 'darkgreen', textDecoration: 'none' }} -          href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/settings`) : '#'}>VIEW MORE SETTINGS</a> +          style={{ color: "darkgreen", textDecoration: "none" }} +          href={ +            chrome.extension +              ? chrome.extension.getURL(`/static/wallet.html#/settings`) +              : "#" +          } +        > +          VIEW MORE SETTINGS +        </a>        </footer>      </PopupBox> -  ) -}
\ No newline at end of file +  ); +} diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx index 88c7c725e..f20403d6a 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx @@ -15,38 +15,38 @@   */  /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { createExample } from '../test-utils'; -import { TalerActionFound as TestedComponent } from './TalerActionFound'; +import { createExample } from "../test-utils"; +import { TalerActionFound as TestedComponent } from "./TalerActionFound";  export default { -  title: 'popup/TalerActionFound', +  title: "popup/TalerActionFound",    component: TestedComponent,  };  export const PayAction = createExample(TestedComponent, { -  url: 'taler://pay/something' +  url: "taler://pay/something",  });  export const WithdrawalAction = createExample(TestedComponent, { -  url: 'taler://withdraw/something' +  url: "taler://withdraw/something",  });  export const TipAction = createExample(TestedComponent, { -  url: 'taler://tip/something' +  url: "taler://tip/something",  });  export const NotifyAction = createExample(TestedComponent, { -  url: 'taler://notify-reserve/something' +  url: "taler://notify-reserve/something",  });  export const RefundAction = createExample(TestedComponent, { -  url: 'taler://refund/something' +  url: "taler://refund/something",  });  export const InvalidAction = createExample(TestedComponent, { -  url: 'taler://something/asd' +  url: "taler://something/asd",  }); diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx index ef0ec341c..cbdcbeb15 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx @@ -1,5 +1,31 @@ +/* + 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 { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util"; -import { ButtonPrimary, ButtonSuccess, PopupBox } from "../components/styled/index"; +import { +  ButtonPrimary, +  ButtonSuccess, +  PopupBox, +} from "../components/styled/index"; +import { h } from "preact";  export interface Props {    url: string; @@ -8,54 +34,89 @@ export interface Props {  export function TalerActionFound({ url, onDismiss }: Props) {    const uriType = classifyTalerUri(url); -  return <PopupBox> -    <section> -      <h1>Taler Action </h1> -      {uriType === TalerUriType.TalerPay && <div> -        <p>This page has pay action.</p> -        <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}> -          Open pay page -        </ButtonSuccess> -      </div>} -      {uriType === TalerUriType.TalerWithdraw && <div> -        <p>This page has a withdrawal action.</p> -        <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}> -          Open withdraw page -        </ButtonSuccess> -      </div>} -      {uriType === TalerUriType.TalerTip && <div> -        <p>This page has a tip action.</p> -        <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}> -          Open tip page -        </ButtonSuccess> -      </div>} -      {uriType === TalerUriType.TalerNotifyReserve && <div> -        <p>This page has a notify reserve action.</p> -        <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}> -          Notify -        </ButtonSuccess> -      </div>} -      {uriType === TalerUriType.TalerRefund && <div> -        <p>This page has a refund action.</p> -        <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}> -          Open refund page -        </ButtonSuccess> -      </div>} -      {uriType === TalerUriType.Unknown && <div> -        <p>This page has a malformed taler uri.</p> -        <p>{url}</p> -      </div>} - -    </section> -    <footer> -      <div /> -      <ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary> -    </footer> -  </PopupBox>; - +  return ( +    <PopupBox> +      <section> +        <h1>Taler Action </h1> +        {uriType === TalerUriType.TalerPay && ( +          <div> +            <p>This page has pay action.</p> +            <ButtonSuccess +              onClick={() => { +                chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); +              }} +            > +              Open pay page +            </ButtonSuccess> +          </div> +        )} +        {uriType === TalerUriType.TalerWithdraw && ( +          <div> +            <p>This page has a withdrawal action.</p> +            <ButtonSuccess +              onClick={() => { +                chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); +              }} +            > +              Open withdraw page +            </ButtonSuccess> +          </div> +        )} +        {uriType === TalerUriType.TalerTip && ( +          <div> +            <p>This page has a tip action.</p> +            <ButtonSuccess +              onClick={() => { +                chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); +              }} +            > +              Open tip page +            </ButtonSuccess> +          </div> +        )} +        {uriType === TalerUriType.TalerNotifyReserve && ( +          <div> +            <p>This page has a notify reserve action.</p> +            <ButtonSuccess +              onClick={() => { +                chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); +              }} +            > +              Notify +            </ButtonSuccess> +          </div> +        )} +        {uriType === TalerUriType.TalerRefund && ( +          <div> +            <p>This page has a refund action.</p> +            <ButtonSuccess +              onClick={() => { +                chrome.tabs.create({ url: actionForTalerUri(uriType, url) }); +              }} +            > +              Open refund page +            </ButtonSuccess> +          </div> +        )} +        {uriType === TalerUriType.Unknown && ( +          <div> +            <p>This page has a malformed taler uri.</p> +            <p>{url}</p> +          </div> +        )} +      </section> +      <footer> +        <div /> +        <ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary> +      </footer> +    </PopupBox> +  );  } -function actionForTalerUri(uriType: TalerUriType, talerUri: string): string | undefined { +function actionForTalerUri( +  uriType: TalerUriType, +  talerUri: string, +): string | undefined {    switch (uriType) {      case TalerUriType.TalerWithdraw:        return makeExtensionUrlWithParams("static/wallet.html#/withdraw", { @@ -91,8 +152,10 @@ function makeExtensionUrlWithParams(  ): string {    const innerUrl = new URL(chrome.extension.getURL("/" + url));    if (params) { -    const hParams = Object.keys(params).map(k => `${k}=${params[k]}`).join('&') -    innerUrl.hash = innerUrl.hash + '?' + hParams +    const hParams = Object.keys(params) +      .map((k) => `${k}=${params[k]}`) +      .join("&"); +    innerUrl.hash = innerUrl.hash + "?" + hParams;    }    return innerUrl.href;  } diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index 070df554c..a5723ccb5 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -22,19 +22,17 @@  import { setupI18n } from "@gnu-taler/taler-util";  import { createHashHistory } from "history"; -import { render, h, VNode } from "preact"; -import Router, { route, Route, getCurrentUrl } from "preact-router"; -import { useEffect, useState } from "preact/hooks"; +import { render, h } from "preact"; +import Router, { route, Route } from "preact-router"; +import { useEffect } from "preact/hooks";  import { DevContextProvider } from "./context/devContext";  import { useTalerActionURL } from "./hooks/useTalerActionURL";  import { strings } from "./i18n/strings"; +import { Pages, WalletNavBar } from "./NavigationBar";  import { BackupPage } from "./popup/BackupPage";  import { BalancePage } from "./popup/BalancePage"; -import { DeveloperPage as DeveloperPage } from "./popup/Debug"; +import { DeveloperPage } from "./popup/Debug";  import { HistoryPage } from "./popup/History"; -import { -  Pages, WalletNavBar -} from "./NavigationBar";  import { ProviderAddPage } from "./popup/ProviderAddPage";  import { ProviderDetailPage } from "./popup/ProviderDetailPage";  import { SettingsPage } from "./popup/Settings"; @@ -64,11 +62,11 @@ if (document.readyState === "loading") {  }  function Application() { -  const [talerActionUrl, setDismissed] = useTalerActionURL() +  const [talerActionUrl, setDismissed] = useTalerActionURL();    useEffect(() => { -    if (talerActionUrl) route(Pages.cta) -  },[talerActionUrl]) +    if (talerActionUrl) route(Pages.cta); +  }, [talerActionUrl]);    return (      <div> @@ -78,33 +76,54 @@ function Application() {            <Router history={createHashHistory()}>              <Route path={Pages.dev} component={DeveloperPage} /> -            <Route path={Pages.balance} component={BalancePage} -              goToWalletManualWithdraw={() => goToWalletPage(Pages.manual_withdraw)} +            <Route +              path={Pages.balance} +              component={BalancePage} +              goToWalletManualWithdraw={() => +                goToWalletPage(Pages.manual_withdraw) +              }              />              <Route path={Pages.settings} component={SettingsPage} /> -            <Route path={Pages.cta} component={() => <TalerActionFound url={talerActionUrl!} onDismiss={() => { -              setDismissed(true) -              route(Pages.balance) -            }} />} /> +            <Route +              path={Pages.cta} +              component={() => ( +                <TalerActionFound +                  url={talerActionUrl!} +                  onDismiss={() => { +                    setDismissed(true); +                    route(Pages.balance); +                  }} +                /> +              )} +            /> -            <Route path={Pages.transaction} -              component={({ tid }: { tid: string }) => goToWalletPage(Pages.transaction.replace(':tid', tid))} +            <Route +              path={Pages.transaction} +              component={({ tid }: { tid: string }) => +                goToWalletPage(Pages.transaction.replace(":tid", tid)) +              }              />              <Route path={Pages.history} component={HistoryPage} /> -            <Route path={Pages.backup} component={BackupPage} +            <Route +              path={Pages.backup} +              component={BackupPage}                onAddProvider={() => { -                route(Pages.provider_add) +                route(Pages.provider_add);                }}              /> -            <Route path={Pages.provider_detail} component={ProviderDetailPage} +            <Route +              path={Pages.provider_detail} +              component={ProviderDetailPage}                onBack={() => { -                route(Pages.backup) +                route(Pages.backup);                }}              /> -            <Route path={Pages.provider_add} component={ProviderAddPage} +            <Route +              path={Pages.provider_add} +              component={ProviderAddPage}                onBack={() => { -                route(Pages.backup) +                route(Pages.backup);                }}              />              <Route default component={Redirect} to={Pages.balance} /> @@ -119,13 +138,13 @@ function goToWalletPage(page: Pages | string): null {    chrome.tabs.create({      active: true,      url: chrome.extension.getURL(`/static/wallet.html#${page}`), -  }) -  return null +  }); +  return null;  }  function Redirect({ to }: { to: string }): null {    useEffect(() => { -    route(to, true) -  }) -  return null +    route(to, true); +  }); +  return null;  } diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx index bbe8e465c..9c2a794dd 100644 --- a/packages/taler-wallet-webextension/src/renderHtml.tsx +++ b/packages/taler-wallet-webextension/src/renderHtml.tsx @@ -87,10 +87,7 @@ interface CollapsibleProps {   * Component that shows/hides its children when clicking   * a heading.   */ -export class Collapsible extends Component< -  CollapsibleProps, -  CollapsibleState -> { +export class Collapsible extends Component<CollapsibleProps, CollapsibleState> {    constructor(props: CollapsibleProps) {      super(props);      this.state = { collapsed: props.initiallyCollapsed }; @@ -139,23 +136,20 @@ export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {    return <span>{text}</span>;  } -export interface LoadingButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> { +export interface LoadingButtonProps +  extends JSX.HTMLAttributes<HTMLButtonElement> {    isLoading: boolean;  } -export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.Element { +export function ProgressButton({ +  isLoading, +  ...rest +}: LoadingButtonProps): JSX.Element {    return ( -    <button -      class="pure-button pure-button-primary" -      type="button" -      {...rest} -    > +    <button class="pure-button pure-button-primary" type="button" {...rest}>        {isLoading ? (          <span> -          <object -            class="svg-icon svg-baseline" -            data="/img/spinner-bars.svg" -          /> +          <object class="svg-icon svg-baseline" data="/img/spinner-bars.svg" />          </span>        ) : null}{" "}        {rest.children} @@ -163,17 +157,13 @@ export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.El    );  } -export function PageLink( -  props: { pageName: string, children?: ComponentChildren }, -): JSX.Element { +export function PageLink(props: { +  pageName: string; +  children?: ComponentChildren; +}): JSX.Element {    const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);    return ( -    <a -      class="actionLink" -      href={url} -      target="_blank" -      rel="noopener noreferrer" -    > +    <a class="actionLink" href={url} target="_blank" rel="noopener noreferrer">        {props.children}      </a>    ); diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts index 6bf1be3ff..28622bb85 100644 --- a/packages/taler-wallet-webextension/src/test-utils.ts +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -14,15 +14,17 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ -import { ComponentChildren, FunctionalComponent, h as render } from 'preact'; +import { ComponentChildren, FunctionalComponent, h as render } from "preact"; -export function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { -  const r = (args: any) => render(Component, args) -  r.args = props -  return r +export function createExample<Props>( +  Component: FunctionalComponent<Props>, +  props: Partial<Props>, +) { +  const r = (args: any) => render(Component, args); +  r.args = props; +  return r;  } -  export function NullLink({ children }: { children?: ComponentChildren }) { -  return render('a', { children, href: 'javascript:void(0);' }) +  return render("a", { children, href: "javascript:void(0);" });  } 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>    );  } diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index 023ee94c5..f097d58b5 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -21,7 +21,7 @@   */  import { setupI18n } from "@gnu-taler/taler-util"; -import { createHashHistory } from 'history'; +import { createHashHistory } from "history";  import { Fragment, h, render } from "preact";  import Router, { route, Route } from "preact-router";  import { useEffect } from "preact/hooks"; @@ -29,22 +29,19 @@ import { LogoHeader } from "./components/LogoHeader";  import { DevContextProvider } from "./context/devContext";  import { PayPage } from "./cta/Pay";  import { RefundPage } from "./cta/Refund"; -import { TipPage } from './cta/Tip'; +import { TipPage } from "./cta/Tip";  import { WithdrawPage } from "./cta/Withdraw";  import { strings } from "./i18n/strings"; -import { -  Pages, WalletNavBar -} from "./NavigationBar"; +import { Pages, WalletNavBar } from "./NavigationBar";  import { BalancePage } from "./wallet/BalancePage";  import { HistoryPage } from "./wallet/History";  import { SettingsPage } from "./wallet/Settings"; -import { TransactionPage } from './wallet/Transaction'; +import { TransactionPage } from "./wallet/Transaction";  import { WelcomePage } from "./wallet/Welcome"; -import { BackupPage } from './wallet/BackupPage'; +import { BackupPage } from "./wallet/BackupPage";  import { DeveloperPage } from "./popup/Debug.js";  import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js"; -  function main(): void {    try {      const container = document.getElementById("container"); @@ -69,51 +66,86 @@ if (document.readyState === "loading") {  }  function withLogoAndNavBar(Component: any) { -  return (props: any) => <Fragment> -    <LogoHeader /> -    <WalletNavBar /> -    <Component {...props} /> -  </Fragment> +  return (props: any) => ( +    <Fragment> +      <LogoHeader /> +      <WalletNavBar /> +      <Component {...props} /> +    </Fragment> +  );  }  function Application() { -  return <div> -    <DevContextProvider> -      <Router history={createHashHistory()} > - -        <Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} /> - -        <Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} /> -        <Route path={Pages.transaction} component={withLogoAndNavBar(TransactionPage)} /> -        <Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)}  -              goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}  -        /> -        <Route path={Pages.settings} component={withLogoAndNavBar(SettingsPage)} /> -        <Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} /> - -        <Route path={Pages.manual_withdraw} component={withLogoAndNavBar(ManualWithdrawPage)} /> - -        <Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} /> -        <Route path={Pages.payback} component={() => <div>no yet implemented</div>} /> -        <Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} /> - -        <Route path={Pages.dev} component={withLogoAndNavBar(DeveloperPage)} /> - -        {/** call to action */} -        <Route path={Pages.pay} component={PayPage} /> -        <Route path={Pages.refund} component={RefundPage} /> -        <Route path={Pages.tips} component={TipPage} /> -        <Route path={Pages.withdraw} component={WithdrawPage} /> - -        <Route default component={Redirect} to={Pages.history} /> -      </Router> -    </DevContextProvider> -  </div> +  return ( +    <div> +      <DevContextProvider> +        <Router history={createHashHistory()}> +          <Route +            path={Pages.welcome} +            component={withLogoAndNavBar(WelcomePage)} +          /> + +          <Route +            path={Pages.history} +            component={withLogoAndNavBar(HistoryPage)} +          /> +          <Route +            path={Pages.transaction} +            component={withLogoAndNavBar(TransactionPage)} +          /> +          <Route +            path={Pages.balance} +            component={withLogoAndNavBar(BalancePage)} +            goToWalletManualWithdraw={() => route(Pages.manual_withdraw)} +          /> +          <Route +            path={Pages.settings} +            component={withLogoAndNavBar(SettingsPage)} +          /> +          <Route +            path={Pages.backup} +            component={withLogoAndNavBar(BackupPage)} +          /> + +          <Route +            path={Pages.manual_withdraw} +            component={withLogoAndNavBar(ManualWithdrawPage)} +          /> + +          <Route +            path={Pages.reset_required} +            component={() => <div>no yet implemented</div>} +          /> +          <Route +            path={Pages.payback} +            component={() => <div>no yet implemented</div>} +          /> +          <Route +            path={Pages.return_coins} +            component={() => <div>no yet implemented</div>} +          /> + +          <Route +            path={Pages.dev} +            component={withLogoAndNavBar(DeveloperPage)} +          /> + +          {/** call to action */} +          <Route path={Pages.pay} component={PayPage} /> +          <Route path={Pages.refund} component={RefundPage} /> +          <Route path={Pages.tips} component={TipPage} /> +          <Route path={Pages.withdraw} component={WithdrawPage} /> + +          <Route default component={Redirect} to={Pages.history} /> +        </Router> +      </DevContextProvider> +    </div> +  );  }  function Redirect({ to }: { to: string }): null {    useEffect(() => { -    route(to, true) -  }) -  return null +    route(to, true); +  }); +  return null;  } diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 92597cbd2..90cfd3ed6 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -47,7 +47,12 @@ import {    AddExchangeRequest,    GetExchangeTosResult,  } from "@gnu-taler/taler-util"; -import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core"; +import { +  AddBackupProviderRequest, +  BackupProviderState, +  OperationFailedError, +  RemoveBackupProviderRequest, +} from "@gnu-taler/taler-wallet-core";  import { BackupInfo } from "@gnu-taler/taler-wallet-core";  import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw"; @@ -149,78 +154,89 @@ interface CurrencyInfo {    pub: string;  }  interface ListOfKnownCurrencies { -  auditors: CurrencyInfo[], -  exchanges: CurrencyInfo[], +  auditors: CurrencyInfo[]; +  exchanges: CurrencyInfo[];  }  /**   * Get a list of currencies from known auditors and exchanges   */  export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> { -  return callBackend("listCurrencies", {}).then(result => { -    console.log("result list", result) -    const auditors = result.trustedAuditors.map((a: Record<string, string>) => ({ -      name: a.currency, -      baseUrl: a.auditorBaseUrl, -      pub: a.auditorPub, -    })) -    const exchanges = result.trustedExchanges.map((a: Record<string, string>) => ({ -      name: a.currency, -      baseUrl: a.exchangeBaseUrl, -      pub: a.exchangeMasterPub, -    })) -    return { auditors, exchanges } +  return callBackend("listCurrencies", {}).then((result) => { +    console.log("result list", result); +    const auditors = result.trustedAuditors.map( +      (a: Record<string, string>) => ({ +        name: a.currency, +        baseUrl: a.auditorBaseUrl, +        pub: a.auditorPub, +      }), +    ); +    const exchanges = result.trustedExchanges.map( +      (a: Record<string, string>) => ({ +        name: a.currency, +        baseUrl: a.exchangeBaseUrl, +        pub: a.exchangeMasterPub, +      }), +    ); +    return { auditors, exchanges };    });  }  export function listExchanges(): Promise<ExchangesListRespose> { -  return callBackend("listExchanges", {}) +  return callBackend("listExchanges", {});  }  /**   * Get information about the current state of wallet backups.   */  export function getBackupInfo(): Promise<BackupInfo> { -  return callBackend("getBackupInfo", {}) +  return callBackend("getBackupInfo", {});  }  /**   * Add a backup provider and activate it   */ -export function addBackupProvider(backupProviderBaseUrl: string, name: string): Promise<void> { +export function addBackupProvider( +  backupProviderBaseUrl: string, +  name: string, +): Promise<void> {    return callBackend("addBackupProvider", { -    backupProviderBaseUrl, activate: true, name -  } as AddBackupProviderRequest) +    backupProviderBaseUrl, +    activate: true, +    name, +  } as AddBackupProviderRequest);  }  export function setWalletDeviceId(walletDeviceId: string): Promise<void> {    return callBackend("setWalletDeviceId", { -    walletDeviceId -  } as SetWalletDeviceIdRequest) +    walletDeviceId, +  } as SetWalletDeviceIdRequest);  }  export function syncAllProviders(): Promise<void> { -  return callBackend("runBackupCycle", {}) +  return callBackend("runBackupCycle", {});  }  export function syncOneProvider(url: string): Promise<void> { -  return callBackend("runBackupCycle", { providers: [url] }) +  return callBackend("runBackupCycle", { providers: [url] });  }  export function removeProvider(url: string): Promise<void> { -  return callBackend("removeBackupProvider", { provider: url } as RemoveBackupProviderRequest) +  return callBackend("removeBackupProvider", { +    provider: url, +  } as RemoveBackupProviderRequest);  }  export function extendedProvider(url: string): Promise<void> { -  return callBackend("extendBackupProvider", { provider: url }) +  return callBackend("extendBackupProvider", { provider: url });  }  /**   * Retry a transaction - * @param transactionId  - * @returns  + * @param transactionId + * @returns   */  export function retryTransaction(transactionId: string): Promise<void> {    return callBackend("retryTransaction", { -    transactionId +    transactionId,    } as RetryTransactionRequest);  } @@ -229,7 +245,7 @@ export function retryTransaction(transactionId: string): Promise<void> {   */  export function deleteTransaction(transactionId: string): Promise<void> {    return callBackend("deleteTransaction", { -    transactionId +    transactionId,    } as DeleteTransactionRequest);  } @@ -264,29 +280,30 @@ export function acceptWithdrawal(  /**   * Create a reserve into the exchange that expect the amount indicated - * @param exchangeBaseUrl  - * @param amount  - * @returns  + * @param exchangeBaseUrl + * @param amount + * @returns   */  export function acceptManualWithdrawal(    exchangeBaseUrl: string,    amount: string,  ): Promise<AcceptManualWithdrawalResult> {    return callBackend("acceptManualWithdrawal", { -    amount, exchangeBaseUrl +    amount, +    exchangeBaseUrl,    });  }  export function setExchangeTosAccepted(    exchangeBaseUrl: string, -  etag: string | undefined +  etag: string | undefined,  ): Promise<void> {    return callBackend("setExchangeTosAccepted", { -    exchangeBaseUrl, etag -  } as AcceptExchangeTosRequest) +    exchangeBaseUrl, +    etag, +  } as AcceptExchangeTosRequest);  } -  /**   * Get diagnostics information   */ @@ -319,7 +336,6 @@ export function getWithdrawalDetailsForUri(    return callBackend("getWithdrawalDetailsForUri", req);  } -  /**   * Get diagnostics information   */ @@ -333,17 +349,15 @@ export function getExchangeTos(    acceptedFormat: string[],  ): Promise<GetExchangeTosResult> {    return callBackend("getExchangeTos", { -    exchangeBaseUrl, acceptedFormat +    exchangeBaseUrl, +    acceptedFormat,    });  } -export function addExchange( -  req: AddExchangeRequest, -): Promise<void> { +export function addExchange(req: AddExchangeRequest): Promise<void> {    return callBackend("addExchange", req);  } -  export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {    return callBackend("prepareTip", req);  } diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json index cff3d8857..25920a120 100644 --- a/packages/taler-wallet-webextension/tsconfig.json +++ b/packages/taler-wallet-webextension/tsconfig.json @@ -1,9 +1,13 @@  {    "compilerOptions": {      "composite": true, -    "lib": ["es6", "DOM"], -    "jsx": "react-jsx", -    "jsxImportSource": "preact", +    "lib": [ +      "es6", +      "DOM" +    ], +    "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ +    "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */ +    "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories      "moduleResolution": "Node",      "module": "ESNext",      "target": "ES6", @@ -16,7 +20,9 @@      "esModuleInterop": true,      "importHelpers": true,      "rootDir": "./src", -    "typeRoots": ["./node_modules/@types"] +    "typeRoots": [ +      "./node_modules/@types" +    ]    },    "references": [      { @@ -26,5 +32,7 @@        "path": "../taler-util/"      }    ], -  "include": ["src/**/*"] -} +  "include": [ +    "src/**/*" +  ] +}
\ No newline at end of file  | 
