diff options
Diffstat (limited to 'packages/merchant-backend-ui/src/pages')
11 files changed, 1545 insertions, 0 deletions
diff --git a/packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx b/packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx new file mode 100644 index 000000000..c20f6dc18 --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/DepletedTip.stories.tsx @@ -0,0 +1,40 @@ +/* + 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 { h, VNode, FunctionalComponent } from 'preact'; +import { DepletedTip as TestedComponent } from './DepletedTip'; + + +export default { + title: 'DepletedTip', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const Example = createExample(TestedComponent, { +}); diff --git a/packages/merchant-backend-ui/src/pages/DepletedTip.tsx b/packages/merchant-backend-ui/src/pages/DepletedTip.tsx new file mode 100644 index 000000000..756b08d6a --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/DepletedTip.tsx @@ -0,0 +1,60 @@ +/* + 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 { Fragment, h, render, VNode } from 'preact'; +import { render as renderToString } from 'preact-render-to-string'; +import { Footer } from '../components/Footer'; +import "../css/pure-min.css"; +import "../css/style.css"; +import { Page } from '../styled'; + +function Head(): VNode { + return <title>Status of your tip</title> +} + +export function DepletedTip(): VNode { + return <Page> + <section> + <h1>Tip already collected</h1> + <div> + You have already collected this tip. + </div> + </section> + <Footer /> + </Page> +} + +export function mount(): void { + try { + render(<DepletedTip />, document.body); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +export function buildTimeRendering(): { head: string, body: string } { + return { + head: renderToString(<Head />), + body: renderToString(<DepletedTip />) + } +} diff --git a/packages/merchant-backend-ui/src/pages/OfferRefund.stories.tsx b/packages/merchant-backend-ui/src/pages/OfferRefund.stories.tsx new file mode 100644 index 000000000..92694f867 --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/OfferRefund.stories.tsx @@ -0,0 +1,45 @@ +/* + 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 { h, VNode, FunctionalComponent } from 'preact'; +import { createSVG } from '../components/QR'; +import { OfferRefund as TestedComponent } from './OfferRefund'; + + +export default { + title: 'OfferRefund', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +const REFUND_URI_EXAMPLE = 'taler://pay/backend.demo.taler.net/instances/blog/2021.249-022NW2KG88QGA/def537eb-00c2-4a8b-8a17-0be034d118d3?c=2Y4N4PMST7KYAPS83428GTPCD4' + +export const Example = createExample(TestedComponent, { + refundURI: REFUND_URI_EXAMPLE, + qr_code: createSVG(REFUND_URI_EXAMPLE) +}); diff --git a/packages/merchant-backend-ui/src/pages/OfferRefund.tsx b/packages/merchant-backend-ui/src/pages/OfferRefund.tsx new file mode 100644 index 000000000..14c9372c2 --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/OfferRefund.tsx @@ -0,0 +1,154 @@ +/* + 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 { Fragment, h, render, VNode } from 'preact'; +import { render as renderToString } from 'preact-render-to-string'; +import { useEffect } from 'preact/hooks'; +import { Footer } from '../components/Footer'; +import { QR } from '../components/QR'; +import "../css/pure-min.css"; +import "../css/style.css"; +import { Page, QRPlaceholder, WalletLink } from '../styled'; + +/** + * This page creates a refund offer QR code + * + * It will build into a mustache html template for server side rendering + * + * server side rendering params: + * - order_status_url + * - taler_refund_qrcode_svg + * - taler_refund_uri + * + * request params: + * - refund_uri + * - order_status_url + */ + +interface Props { + refundURI?: string; + order_status_url?: string; + qr_code?: string; +} + +function Head({ order_summary }: { order_summary?: string }): VNode { + return <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title>Refund available for {order_summary ? order_summary : `{{ order_summary }}`}</title> + </Fragment> +} + +export function OfferRefund({ refundURI, qr_code, order_status_url }: Props): VNode { + useEffect(() => { + let checkUrl: URL; + try { + checkUrl = new URL(order_status_url ? order_status_url : "{{& order_status_url }}"); + } catch (e) { + return; + } + checkUrl.searchParams.set("await_refund_obtained", "yes"); + const delayMs = 500; + function check() { + let retried = false; + function retryOnce() { + if (!retried) { + retried = true; + check(); + } + } + const req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + try { + const resp = JSON.parse(req.responseText); + if (!resp.refund_pending) { + window.location.reload(); + } + } catch (e) { + console.error("could not parse response:", e); + } + } + setTimeout(retryOnce, delayMs); + } + }; + req.onerror = function () { + setTimeout(retryOnce, delayMs); + } + req.open("GET", checkUrl.href); + req.send(); + } + + setTimeout(check, delayMs); + }) + return <Page> + <section> + <h1>Collect Taler refund</h1> + <p> + Scan this QR code with your Taler mobile wallet: + </p> + <QRPlaceholder dangerouslySetInnerHTML={{ __html: qr_code ? qr_code : `{{{ taler_refund_qrcode_svg }}}` }} /> + <p> + <WalletLink href={refundURI ? refundURI : `{{ taler_refund_uri }}`}> + Or open your Taller wallet + </WalletLink> + </p> + <p> + <a href="https://wallet.taler.net/">Don't have a Taler wallet yet? Install it!</a> + </p> + </section> + <Footer /> + </Page> +} + +export function mount(): void { + try { + const fromLocation = new URL(window.location.href).searchParams + const os = fromLocation.get('order_summary') || undefined; + if (os) { + render(<Head order_summary={os} />, document.head); + } + + const uri = fromLocation.get('refund_uri') || undefined; + const osu = fromLocation.get('order_status_url') || undefined; + const qr_code = uri ? renderToString(<QR text={uri} />) : undefined; + + render(<OfferRefund + refundURI={uri} order_status_url={osu} + qr_code={qr_code} + />, document.body); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +export function buildTimeRendering(): { head: string, body: string } { + return { + head: renderToString(<Head />), + body: renderToString(<OfferRefund />) + } +} diff --git a/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx b/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx new file mode 100644 index 000000000..dfbf71fff --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/OfferTip.stories.tsx @@ -0,0 +1,45 @@ +/* + 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 { h, VNode, FunctionalComponent } from 'preact'; +import { createSVG } from '../components/QR'; +import { OfferTip as TestedComponent } from './OfferTip'; + + +export default { + title: 'OfferTip', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +const TIP_URI_EXAMPLE = 'taler+http://tip/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0' + +export const Example = createExample(TestedComponent, { + tipURI: TIP_URI_EXAMPLE, + qr_code: createSVG(TIP_URI_EXAMPLE) +}); diff --git a/packages/merchant-backend-ui/src/pages/OfferTip.tsx b/packages/merchant-backend-ui/src/pages/OfferTip.tsx new file mode 100644 index 000000000..ace1059ca --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/OfferTip.tsx @@ -0,0 +1,141 @@ +/* + 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 { Fragment, h, render, VNode } from 'preact'; +import { render as renderToString } from 'preact-render-to-string'; +import { useEffect } from 'preact/hooks'; +import { Footer } from '../components/Footer'; +import { QR } from '../components/QR'; +import "../css/pure-min.css"; +import "../css/style.css"; +import { Page, QRPlaceholder, WalletLink } from '../styled'; +import { ShowOrderDetails } from './ShowOrderDetails'; + + +/** + * This page creates a tip offer QR code + * + * It will build into a mustache html template for server side rendering + * + * server side rendering params: + * - tip_status_url + * - taler_tip_qrcode_svg + * - taler_tip_uri + * + * request params: + * - tip_uri + * - tip_status_url + */ + +interface Props { + tipURI?: string, + tip_status_url?: string, + qr_code?: string, +} + +export function Head(): VNode { + return <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title>Tip available</title> + </Fragment> +} + +export function OfferTip({ tipURI, qr_code, tip_status_url }: Props): VNode { + useEffect(() => { + let checkUrl: URL; + try { + checkUrl = new URL(tip_status_url ? tip_status_url : "{{& tip_status_url }}"); + } catch (e) { + return; + } + + const delayMs = 500; + function check() { + let retried = false; + function retryOnce() { + if (!retried) { + retried = true; + check(); + } + } + const req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 410) { + window.location.reload(); + } + setTimeout(retryOnce, delayMs); + } + }; + req.onerror = function () { + setTimeout(retryOnce, delayMs); + } + req.open("GET", checkUrl.href); + req.send(); + } + + setTimeout(check, delayMs); + }) + return <Page> + <section> + <h1 >Collect Taler tip</h1> + <p> + Scan this QR code with your Taler mobile wallet: + </p> + <QRPlaceholder dangerouslySetInnerHTML={{ __html: qr_code ? qr_code : `{{{ taler_tip_qrcode_svg }}}` }} /> + <p> + <WalletLink href={tipURI ? tipURI : `{{ taler_tip_uri }}`}> + Or open your Taller wallet + </WalletLink> + </p> + <p> + <a href="https://wallet.taler.net/">Don't have a Taler wallet yet? Install it!</a> + </p> + </section> + <Footer /> + </Page> +} + +export function mount(): void { + try { + const fromLocation = new URL(window.location.href).searchParams + + const uri = fromLocation.get('tip_uri') || undefined + const tsu = fromLocation.get('tip_status_url') || undefined + + render(<OfferTip tipURI={uri} tip_status_url={tsu} />, document.body); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +export function buildTimeRendering(): { head: string, body: string } { + return { + head: renderToString(<Head />), + body: renderToString(<ShowOrderDetails />) + } +} diff --git a/packages/merchant-backend-ui/src/pages/RequestPayment.stories.tsx b/packages/merchant-backend-ui/src/pages/RequestPayment.stories.tsx new file mode 100644 index 000000000..5d6d79adf --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/RequestPayment.stories.tsx @@ -0,0 +1,45 @@ +/* + 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 { FunctionalComponent, h } from 'preact'; +import { createSVG } from '../components/QR'; +import { RequestPayment as TestedComponent } from './RequestPayment'; + + +export default { + title: 'RequestPayment', + component: TestedComponent, + argTypes: { + }, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +const PAYTO_URI_EXAMPLE = 'taler+http://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0' + +export const Example = createExample(TestedComponent, { + payURI: 'taler+http://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0', + qr_code: createSVG(PAYTO_URI_EXAMPLE) +}); diff --git a/packages/merchant-backend-ui/src/pages/RequestPayment.tsx b/packages/merchant-backend-ui/src/pages/RequestPayment.tsx new file mode 100644 index 000000000..050755dfb --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/RequestPayment.tsx @@ -0,0 +1,196 @@ +/* + 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 { Fragment, h, render, VNode } from "preact"; +import { render as renderToString } from "preact-render-to-string"; +import { useEffect } from "preact/hooks"; +import { Footer } from "../components/Footer"; +import "../css/pure-min.css"; +import "../css/style.css"; +import { QR } from "../components/QR"; +import { Page, QRPlaceholder, WalletLink } from "../styled"; + +/** + * This page creates a payment request QR code + * + * It will build into a mustache html template for server side rendering + * + * server side rendering params: + * - order_status_url + * - taler_pay_qrcode_svg + * - taler_pay_uri + * - order_summary + * + * request params: + * - pay_uri + * - order_summary + * - order_status_url + */ + +interface Props { + payURI?: string; + order_status_url?: string; + qr_code?: string; +} + +function Head({ order_summary }: { order_summary?: string }): VNode { + return ( + <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title> + Payment requested for{" "} + {order_summary ? order_summary : `{{ order_summary }}`} + </title> + </Fragment> + ); +} + +export function RequestPayment({ + payURI, + qr_code, + order_status_url, +}: Props): VNode { + useEffect(() => { + const longpollDelayMs = 60 * 1000; + let checkUrl: URL; + try { + checkUrl = new URL( + order_status_url ? order_status_url : "{{& order_status_url }}" + ); + } catch (e) { + return; + } + checkUrl.searchParams.set("timeout_s", longpollDelayMs.toString()); + function check() { + let retried = false; + function retryOnce() { + if (!retried) { + retried = true; + check(); + } + } + const req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + try { + const resp = JSON.parse(req.responseText); + if (resp.fulfillment_url) { + window.location.replace(resp.fulfillment_url); + } + } catch (e) { + console.error("could not parse response:", e); + } + } + if (req.status === 202) { + try { + const resp = JSON.parse(req.responseText); + if (resp.fulfillment_url) { + window.location.replace(resp.fulfillment_url); + } + } catch (e) { + console.error("could not parse response:", e); + } + } + if (req.status === 402) { + try { + const resp = JSON.parse(req.responseText); + if (resp.already_paid_order_id && resp.fulfillment_url) { + window.location.replace(resp.fulfillment_url); + } + } catch (e) { + console.error("could not parse response:", e); + } + } + setTimeout(retryOnce, 500); + } + }; + req.onerror = function () { + setTimeout(retryOnce, 500); + }; + req.ontimeout = function () { + setTimeout(retryOnce, 500); + }; + req.timeout = longpollDelayMs; + req.open("GET", checkUrl.href); + req.send(); + } + setTimeout(check, 500); + }); + return ( + <Page> + <section> + <h1>Pay with Taler</h1> + <p>Scan this QR code with your mobile wallet:</p> + <QRPlaceholder + dangerouslySetInnerHTML={{ + __html: qr_code ? qr_code : `{{{ taler_pay_qrcode_svg }}}`, + }} + /> + <p> + <WalletLink href={payURI ? payURI : `{{ taler_pay_uri }}`}> + Or open your Taller wallet + </WalletLink> + </p> + <p> + <a href="https://wallet.taler.net/"> + Don't have a Taler wallet yet? Install it! + </a> + </p> + </section> + <Footer /> + </Page> + ); +} + +export function mount(): void { + try { + const fromLocation = new URL(window.location.href).searchParams; + const os = fromLocation.get("order_summary") || undefined; + if (os) { + render(<Head order_summary={os} />, document.head); + } + + const uri = fromLocation.get("pay_uri") || undefined; + const osu = fromLocation.get("order_status_url") || undefined; + const qr_code = uri ? renderToString(<QR text={uri} />) : undefined; + + render( + <RequestPayment payURI={uri} order_status_url={osu} qr_code={qr_code} />, + document.body + ); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +export function buildTimeRendering(): { head: string; body: string } { + return { + head: renderToString(<Head />), + body: renderToString(<RequestPayment />), + }; +} diff --git a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts new file mode 100644 index 000000000..ba68397ee --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts @@ -0,0 +1,219 @@ +/* + 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 { MerchantBackend } from '../declaration'; +import { Props } from './ShowOrderDetails'; + + +const defaultContractTerms: MerchantBackend.ContractTerms = { + order_id: 'XRS8876388373', + amount: 'USD:10', + summary: 'this is a short summary', + pay_deadline: { + t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 + }, + merchant: { + name: 'the merchant (inc)', + address: { + country_subdivision: 'Buenos Aires', + town: 'CABA', + country: 'Argentina' + }, + jurisdiction: { + country_subdivision: 'Cordoba', + town: 'Capital', + country: 'Argentina' + }, + }, + max_fee: 'USD:0.1', + max_wire_fee: 'USD:0.2', + wire_fee_amortization: 1, + products: [], + timestamp: { + t_s: new Date().getTime() + }, + auditors: [], + exchanges: [], + h_wire: '', + merchant_base_url: 'http://merchant.base.url/', + merchant_pub: 'QWEASDQWEASD', + nonce: 'NONCE', + refund_deadline: { + t_s: new Date().getTime() + 6 * 24 * 60 * 60 * 1000 + }, + wire_method: 'x-taler-bank', + wire_transfer_deadline: { + t_s: new Date().getTime() + 3 * 24 * 60 * 60 * 1000 + }, +}; + +const inSixDays = new Date().getTime() + 6 * 24 * 60 * 60 * 1000 +const in10Minutes = new Date().getTime() + 10 * 60 * 1000 +const in15Minutes = new Date().getTime() + 15 * 60 * 1000 +const in20Minutes = new Date().getTime() + 20 * 60 * 1000 + +export const exampleData: { [name: string]: Props } = { + Simplest: { + order_summary: 'here goes the order summary', + contract_terms: defaultContractTerms, + }, + WithRefundAmount: { + order_summary: 'here goes the order summary', + refund_amount: 'USD:10', + contract_terms: defaultContractTerms, + }, + WithDeliveryDate: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + delivery_date: { + t_s: inSixDays + }, + }, + }, + WithDeliveryLocation: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + delivery_location: { + address_lines: ['addr line 1', 'addr line 2', 'addr line 3', 'addr line 4', 'addr line 5', 'addr line 6', 'addr line 7'], + building_name: 'building-name', + building_number: 'building-number', + country: 'country', + country_subdivision: 'country sub', + district: 'district', + post_code: 'post-code', + street: 'street', + town: 'town', + town_location: 'town loc', + }, + }, + }, + WithDeliveryLocationAndDate: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + delivery_location: { + address_lines: ['addr1', 'addr2', 'addr3', 'addr4', 'addr5', 'addr6', 'addr7'], + building_name: 'building-name', + building_number: 'building-number', + country: 'country', + country_subdivision: 'country sub', + district: 'district', + post_code: 'post-code', + street: 'street', + town: 'town', + town_location: 'town loc', + }, + delivery_date: { + t_s: inSixDays + }, + }, + }, + WithThreeProducts: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + products: [{ + description: 'description of the first product', + price: '5:USD', + quantity: 1, + delivery_date: { t_s: in10Minutes }, + product_id: '12333', + }, { + description: 'another description', + price: '10:USD', + quantity: 5, + unit: 't-shirt', + }, { + description: 'one last description', + price: '10:USD', + quantity: 5 + }] + } as MerchantBackend.ContractTerms + }, + WithProductWithTaxes: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + products: [{ + description: 'description of the first product', + price: '5:USD', + quantity: 1, + unit: 'beer', + delivery_date: { t_s: in10Minutes }, + product_id: '456', + taxes: [{ + name: 'VAT', tax: 'USD:1' + }], + }, { + description: 'one last description', + price: '10:USD', + quantity: 5, + product_id: '123', + unit: 'beer', + taxes: [{ + name: 'VAT', tax: 'USD:1' + }], + }] + } as MerchantBackend.ContractTerms + }, + WithExchangeList: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + exchanges: [{ + master_pub: 'ABCDEFGHIJKLMNO', + url: 'http://exchange0.taler.net' + }, { + master_pub: 'AAAAAAAAAAAAAAA', + url: 'http://exchange1.taler.net' + }, { + master_pub: 'BBBBBBBBBBBBBBB', + url: 'http://exchange2.taler.net' + }] + }, + }, + WithAuditorList: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + auditors: [{ + auditor_pub: 'ABCDEFGHIJKLMNO', + name: 'the USD auditor', + url: 'http://auditor-usd.taler.net' + }, { + auditor_pub: 'OPQRSTUVWXYZABCD', + name: 'the EUR auditor', + url: 'http://auditor-eur.taler.net' + }] + }, + }, + WithAutoRefund: { + order_summary: 'here goes the order summary', + contract_terms: { + ...defaultContractTerms, + auto_refund: { + d_us: 1000 * 60 * 60 * 26 + 1000 * 60 * 30 + } + }, + }, +} diff --git a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.stories.tsx b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.stories.tsx new file mode 100644 index 000000000..6a902cc9e --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.stories.tsx @@ -0,0 +1,49 @@ +/* + 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 { FunctionalComponent, h } from 'preact'; +import { ShowOrderDetails as TestedComponent } from './ShowOrderDetails'; +import { exampleData } from './ShowOrderDetails.examples'; + +export default { + title: 'ShowOrderDetails', + component: TestedComponent, + argTypes: { + }, + excludeStories: /.*Data$/, +}; + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const Simplest = createExample(TestedComponent, exampleData.Simplest); +export const WithRefundAmount = createExample(TestedComponent, exampleData.WithRefundAmount); +export const WithDeliveryDate = createExample(TestedComponent, exampleData.WithDeliveryDate); +export const WithDeliveryLocation = createExample(TestedComponent, exampleData.WithDeliveryLocation); +export const WithDeliveryLocationAndDate = createExample(TestedComponent, exampleData.WithDeliveryLocationAndDate); +export const WithThreeProducts = createExample(TestedComponent, exampleData.WithThreeProducts); +export const WithAuditorList = createExample(TestedComponent, exampleData.WithAuditorList); +export const WithExchangeList = createExample(TestedComponent, exampleData.WithExchangeList); +export const WithAutoRefund = createExample(TestedComponent, exampleData.WithAutoRefund); +export const WithProductWithTaxes = createExample(TestedComponent, exampleData.WithProductWithTaxes); diff --git a/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx new file mode 100644 index 000000000..aa62c2932 --- /dev/null +++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx @@ -0,0 +1,551 @@ +/* + 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 { format, formatDuration } from "date-fns"; +import { intervalToDuration } from "date-fns/esm"; +import { Fragment, h, render, VNode } from "preact"; +import { render as renderToString } from "preact-render-to-string"; +import { Footer } from "../components/Footer"; +import "../css/pure-min.css"; +import "../css/style.css"; +import { MerchantBackend } from "../declaration"; +import { Page, InfoBox, TableExpanded, TableSimple } from "../styled"; + +/** + * This page creates a payment request QR code + * + * It will build into a mustache html template for server side rendering + * + * server side rendering params: + * - order_summary + * - contract_terms + * - refund_amount + * + * request params: + * - refund_amount + * - contract_terms + * - order_summary + */ + +export interface Props { + btr?: boolean; // build time rendering flag + order_summary?: string; + refund_amount?: string; + contract_terms?: MerchantBackend.ContractTerms; +} + +function Head({ order_summary }: { order_summary?: string }): VNode { + return ( + <Fragment> + <meta charSet="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <noscript> + <meta http-equiv="refresh" content="1" /> + </noscript> + <title> + Status of your order for{" "} + {order_summary ? order_summary : `{{ order_summary }}`} + </title> + <script>{` + var contractTermsStr = '{{{contract_terms_json}}}'; + `}</script> + </Fragment> + ); +} + +function Location({ + templateName, + location, + btr, +}: { + templateName: string; + location: MerchantBackend.Location | undefined; + btr?: boolean; +}) { + //FIXME: mustache strings show be constructed in a way that ends in the final output of the html but is not present in the + // javascript code, otherwise when mustache render engine run over the html it will also replace string in the javascript code + // that is made to run when the browser has javascript enable leading into undefined behavior. + // that's why in the next fields we are using concatenations to build the mustache placeholder. + return ( + <Fragment> + {btr && `{{` + `#${templateName}.building_name}}`} + <dd> + {location?.building_name || + (btr && `{{ ${templateName}.building_name }}`)}{" "} + {location?.building_number || + (btr && `{{ ${templateName}.building_number }}`)} + </dd> + {btr && `{{` + `/${templateName}.building_name}}`} + + {btr && `{{` + `#${templateName}.country}}`} + <dd> + {location?.country || (btr && `{{ ${templateName}.country }}`)}{" "} + {location?.country_subdivision || + (btr && `{{ ${templateName}.country_subdivision }}`)} + </dd> + {btr && `{{` + `/${templateName}.country}}`} + + {btr && `{{` + `#${templateName}.district}}`} + <dd>{location?.district || (btr && `{{ ${templateName}.district }}`)}</dd> + {btr && `{{` + `/${templateName}.district}}`} + + {btr && `{{` + `#${templateName}.post_code}}`} + <dd> + {location?.post_code || (btr && `{{ ${templateName}.post_code }}`)} + </dd> + {btr && `{{` + `/${templateName}.post_code}}`} + + {btr && `{{` + `#${templateName}.street}}`} + <dd>{location?.street || (btr && `{{ ${templateName}.street }}`)}</dd> + {btr && `{{` + `/${templateName}.street}}`} + + {btr && `{{` + `#${templateName}.town}}`} + <dd>{location?.town || (btr && `{{ ${templateName}.town }}`)}</dd> + {btr && `{{` + `/${templateName}.town}}`} + + {btr && `{{` + `#${templateName}.town_location}}`} + <dd> + {location?.town_location || + (btr && `{{ ${templateName}.town_location }}`)} + </dd> + {btr && `{{` + `/${templateName}.town_location}}`} + </Fragment> + ); +} + +export function ShowOrderDetails({ + order_summary, + refund_amount, + contract_terms, + btr, +}: Props): VNode { + const productList = btr + ? [{} as MerchantBackend.Product] + : contract_terms?.products || []; + const auditorsList = btr + ? [{} as MerchantBackend.Auditor] + : contract_terms?.auditors || []; + const exchangesList = btr + ? [{} as MerchantBackend.Exchange] + : contract_terms?.exchanges || []; + const hasDeliveryInfo = + btr || + !!contract_terms?.delivery_date || + !!contract_terms?.delivery_location; + + return ( + <Page> + <header> + <h1> + Details of order{" "} + {contract_terms?.order_id || `{{ contract_terms.order_id }}`} + </h1> + </header> + + <section> + {btr && `{{#refund_amount}}`} + {(btr || refund_amount) && ( + <section> + <InfoBox> + <b>Refunded:</b> The merchant refunded you{" "} + <b>{refund_amount || `{{ refund_amount }}`}</b>. + </InfoBox> + </section> + )} + {btr && `{{/refund_amount}}`} + + <section> + <TableExpanded> + <dt>Order summary:</dt> + <dd>{contract_terms?.summary || `{{ contract_terms.summary }}`}</dd> + <dt>Amount paid:</dt> + <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> + <dt>Order date:</dt> + <dd> + {contract_terms?.timestamp + ? contract_terms?.timestamp.t_s != "never" + ? format( + contract_terms?.timestamp.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.timestamp_str }}`}{" "} + </dd> + <dt>Merchant name:</dt> + <dd> + {contract_terms?.merchant.name || + `{{ contract_terms.merchant.name }}`} + </dd> + </TableExpanded> + </section> + + {btr && `{{#contract_terms.hasProducts}}`} + {!productList.length ? null : ( + <section> + <h2>Products purchased</h2> + <TableSimple> + {btr && "{{" + "#contract_terms.products" + "}}"} + {productList.map((p, i) => { + const taxList = btr + ? [{} as MerchantBackend.Tax] + : p.taxes || []; + + return ( + <Fragment key={i}> + <p>{p.description || `{{description}}`}</p> + <dl> + <dt>Quantity:</dt> + <dd>{p.quantity || `{{quantity}}`}</dd> + + <dt>Price:</dt> + <dd>{p.price || `{{price}}`}</dd> + + {btr && `{{#hasTaxes}}`} + {!taxList.length ? null : ( + <Fragment> + {btr && "{{" + "#taxes" + "}}"} + {taxList.map((t, i) => { + return ( + <Fragment key={i}> + <dt>{t.name || `{{name}}`}</dt> + <dd>{t.tax || `{{tax}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/taxes" + "}}"} + </Fragment> + )} + {btr && `{{/hasTaxes}}`} + + {btr && `{{#delivery_date}}`} + {(btr || p.delivery_date) && ( + <Fragment> + <dt>Delivered on:</dt> + <dd> + {p.delivery_date + ? p.delivery_date.t_s != "never" + ? format( + p.delivery_date.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ delivery_date_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/delivery_date}}`} + + {btr && `{{#unit}}`} + {(btr || p.unit) && ( + <Fragment> + <dt>Product unit:</dt> + <dd>{p.unit || `{{.}}`}</dd> + </Fragment> + )} + {btr && `{{/unit}}`} + + {btr && `{{#product_id}}`} + {(btr || p.product_id) && ( + <Fragment> + <dt>Product ID:</dt> + <dd>{p.product_id || `{{.}}`}</dd> + </Fragment> + )} + {btr && `{{/product_id}}`} + </dl> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.products" + "}}"} + </TableSimple> + </section> + )} + {btr && `{{/contract_terms.hasProducts}}`} + + {btr && `{{#contract_terms.has_delivery_info}}`} + {!hasDeliveryInfo ? null : ( + <section> + <h2>Delivery information</h2> + <TableExpanded> + {btr && `{{#contract_terms.delivery_date}}`} + {(btr || contract_terms?.delivery_date) && ( + <Fragment> + <dt>Delivery date:</dt> + <dd> + {contract_terms?.delivery_date + ? contract_terms?.delivery_date.t_s != "never" + ? format( + contract_terms?.delivery_date.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.delivery_date_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.delivery_date}}`} + + {btr && `{{#contract_terms.delivery_location}}`} + {(btr || contract_terms?.delivery_location) && ( + <Fragment> + <dt>Delivery address:</dt> + <Location + btr={btr} + location={contract_terms?.delivery_location} + templateName="contract_terms.delivery_location" + /> + </Fragment> + )} + {btr && `{{/contract_terms.delivery_location}}`} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.has_delivery_info}}`} + + <section> + <h2>Full payment information</h2> + <TableExpanded> + <dt>Amount paid:</dt> + <dd>{contract_terms?.amount || `{{ contract_terms.amount }}`}</dd> + <dt>Wire transfer method:</dt> + <dd> + {contract_terms?.wire_method || + `{{ contract_terms.wire_method }}`} + </dd> + <dt>Payment deadline:</dt> + <dd> + {contract_terms?.pay_deadline + ? contract_terms?.pay_deadline.t_s != "never" + ? format( + contract_terms?.pay_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.pay_deadline_str }}`}{" "} + </dd> + <dt>Exchange transfer deadline:</dt> + <dd> + {contract_terms?.wire_transfer_deadline + ? contract_terms?.wire_transfer_deadline.t_s != "never" + ? format( + contract_terms?.wire_transfer_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.wire_transfer_deadline_str }}`}{" "} + </dd> + <dt>Maximum deposit fee:</dt> + <dd>{contract_terms?.max_fee || `{{ contract_terms.max_fee }}`}</dd> + <dt>Maximum wire fee:</dt> + <dd> + {contract_terms?.max_wire_fee || + `{{ contract_terms.max_wire_fee }}`} + </dd> + <dt>Wire fee amortization:</dt> + <dd> + {contract_terms?.wire_fee_amortization || + `{{ contract_terms.wire_fee_amortization }}`}{" "} + transactions + </dd> + </TableExpanded> + </section> + + <section> + <h2>Refund information</h2> + <TableExpanded> + <dt>Refund deadline:</dt> + <dd> + {contract_terms?.refund_deadline + ? contract_terms?.refund_deadline.t_s != "never" + ? format( + contract_terms?.refund_deadline.t_s, + "dd MMM yyyy HH:mm:ss" + ) + : "never" + : `{{ contract_terms.refund_deadline_str }}`}{" "} + </dd> + + {btr && `{{#contract_terms.auto_refund}}`} + {(btr || contract_terms?.auto_refund) && ( + <Fragment> + <dt>Attempt autorefund for:</dt> + <dd> + {contract_terms?.auto_refund + ? contract_terms?.auto_refund.d_us != "forever" + ? formatDuration( + intervalToDuration({ + start: 0, + end: contract_terms?.auto_refund.d_us, + }) + ) + : "forever" + : `{{ contract_terms.auto_refund_str }}`}{" "} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.auto_refund}}`} + </TableExpanded> + </section> + + <section> + <h2>Additional order details</h2> + <TableExpanded> + <dt>Public reorder URL:</dt> + <dd> -- not defined yet -- </dd> + {btr && `{{#contract_terms.fulfillment_url}}`} + {(btr || contract_terms?.fulfillment_url) && ( + <Fragment> + <dt>Fulfillment URL:</dt> + <dd> + {contract_terms?.fulfillment_url || + (btr && `{{ contract_terms.fulfillment_url }}`)} + </dd> + </Fragment> + )} + {btr && `{{/contract_terms.fulfillment_url}}`} + {/* <dt>Fulfillment message:</dt> + <dd> -- not defined yet -- </dd> */} + </TableExpanded> + </section> + + <section> + <h2>Full merchant information</h2> + <TableExpanded> + <dt>Merchant name:</dt> + <dd> + {contract_terms?.merchant.name || + `{{ contract_terms.merchant.name }}`} + </dd> + <dt>Merchant address:</dt> + <Location + btr={btr} + location={contract_terms?.merchant.address} + templateName="contract_terms.merchant.address" + /> + <dt>Merchant's jurisdiction:</dt> + <Location + btr={btr} + location={contract_terms?.merchant.jurisdiction} + templateName="contract_terms.merchant.jurisdiction" + /> + <dt>Merchant URI:</dt> + <dd> + {contract_terms?.merchant_base_url || + `{{ contract_terms.merchant_base_url }}`} + </dd> + <dt>Merchant's public key:</dt> + <dd> + {contract_terms?.merchant_pub || + `{{ contract_terms.merchant_pub }}`} + </dd> + {/* <dt>Merchant's hash:</dt> + <dd> -- not defined yet -- </dd> */} + </TableExpanded> + </section> + + {btr && `{{#contract_terms.hasAuditors}}`} + {!auditorsList.length ? null : ( + <section> + <h2>Auditors accepted by the merchant</h2> + <TableExpanded> + {btr && "{{" + "#contract_terms.auditors" + "}}"} + {auditorsList.map((p, i) => { + return ( + <Fragment key={i}> + <p>{p.name || `{{name}}`}</p> + <dt>Auditor's public key:</dt> + <dd>{p.auditor_pub || `{{auditor_pub}}`}</dd> + <dt>Auditor's URL:</dt> + <dd>{p.url || `{{url}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.auditors" + "}}"} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.hasAuditors}}`} + + {btr && `{{#contract_terms.hasExchanges}}`} + {!exchangesList.length ? null : ( + <section> + <h2>Exchanges accepted by the merchant</h2> + <TableExpanded> + {btr && "{{" + "#contract_terms.exchanges" + "}}"} + {exchangesList.map((p, i) => { + return ( + <Fragment key={i}> + <dt>Exchange's URL:</dt> + <dd>{p.url || `{{url}}`}</dd> + <dt>Public key:</dt> + <dd>{p.master_pub || `{{master_pub}}`}</dd> + </Fragment> + ); + })} + {btr && "{{" + "/contract_terms.exchanges" + "}}"} + </TableExpanded> + </section> + )} + {btr && `{{/contract_terms.hasExchanges}}`} + </section> + + <Footer /> + </Page> + ); +} + +export function mount(): void { + try { + const fromLocation = new URL(window.location.href).searchParams; + const os = fromLocation.get("order_summary") || undefined; + if (os) { + render(<Head order_summary={os} />, document.head); + } + + const ra = fromLocation.get("refund_amount") || undefined; + const ct = fromLocation.get("contract_terms") || undefined; + + let contractTerms: MerchantBackend.ContractTerms | undefined; + try { + contractTerms = JSON.parse((window as any).contractTermsStr); + } catch {} + + render( + <ShowOrderDetails + contract_terms={contractTerms} + order_summary={os} + refund_amount={ra} + />, + document.body + ); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +export function buildTimeRendering(): { head: string; body: string } { + return { + head: renderToString(<Head />), + body: renderToString(<ShowOrderDetails btr />), + }; +} |