summaryrefslogtreecommitdiff
path: root/packages/merchant-backend-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backend-ui/src/pages')
-rw-r--r--packages/merchant-backend-ui/src/pages/OfferRefund.stories.tsx45
-rw-r--r--packages/merchant-backend-ui/src/pages/OfferRefund.tsx158
-rw-r--r--packages/merchant-backend-ui/src/pages/RequestPayment.stories.tsx45
-rw-r--r--packages/merchant-backend-ui/src/pages/RequestPayment.tsx203
-rw-r--r--packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts253
-rw-r--r--packages/merchant-backend-ui/src/pages/ShowOrderDetails.stories.tsx49
-rw-r--r--packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx566
7 files changed, 1319 insertions, 0 deletions
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..b1cf63572
--- /dev/null
+++ b/packages/merchant-backend-ui/src/pages/OfferRefund.tsx
@@ -0,0 +1,158 @@
+/*
+ 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" />
+ <meta name="taler-support" content="uri" />
+ <meta name="taler-uri" content="{{ taler_refund_uri }}"></meta>
+ <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(() => {
+ const longpollDelayMs = 60 * 1000;
+ const delayMs = 500;
+ 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");
+ checkUrl.searchParams.set("timeout_ms", 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.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 Taler 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/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..513438ba2
--- /dev/null
+++ b/packages/merchant-backend-ui/src/pages/RequestPayment.tsx
@@ -0,0 +1,203 @@
+/*
+ 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" />
+ <meta name="taler-support" content="uri" />
+ <meta name="taler-uri" content="{{ taler_pay_uri }}"></meta>
+ <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_ms", longpollDelayMs.toString());
+ 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.fulfillment_url) {
+ window.location.replace(resp.fulfillment_url);
+ } else {
+ window.location.reload()
+ }
+ } 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);
+ } else {
+ window.location.reload()
+ }
+ } 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, delayMs);
+ }
+ };
+ req.onerror = function () {
+ setTimeout(retryOnce, delayMs);
+ };
+ req.ontimeout = function () {
+ setTimeout(retryOnce, delayMs);
+ };
+ req.timeout = longpollDelayMs;
+ req.open("GET", checkUrl.href);
+ req.send();
+ }
+ setTimeout(check, delayMs);
+ });
+ 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 Taler 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..86992c9e1
--- /dev/null
+++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.examples.ts
@@ -0,0 +1,253 @@
+/*
+ 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: Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
+ },
+ 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: Math.round(new Date().getTime() / 1000)
+ },
+ auditors: [],
+ exchanges: [],
+ h_wire: '',
+ merchant_base_url: 'http://merchant.base.url/',
+ merchant_pub: 'QWEASDQWEASD',
+ nonce: 'NONCE',
+ refund_deadline: {
+ t_s: Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
+ },
+ wire_method: 'x-taler-bank',
+ wire_transfer_deadline: {
+ t_s: Math.round(new Date().getTime() / 1000) + 3 * 24 * 60 * 60
+ },
+};
+
+const inSixDays = Math.round(new Date().getTime() / 1000) + 6 * 24 * 60 * 60
+const in10Minutes = Math.round(new Date().getTime() / 1000) + 10 * 60
+const in15Minutes = Math.round(new Date().getTime() / 1000) + 15 * 60
+const in20Minutes = Math.round(new Date().getTime() / 1000) + 20 * 60
+
+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
+ }
+ },
+ },
+ WithFulfillmentURL: {
+ order_summary: 'this is the order with fulfillmentURL',
+ contract_terms: {
+ ...defaultContractTerms,
+ fulfillment_url: "https://demo.taler.net",
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
+ WithFulfillmentMessage: {
+ order_summary: 'this is the order with fulfillment message',
+ contract_terms: {
+ ...defaultContractTerms,
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
+ WithoutWireTransferDeadline: {
+ order_summary: 'this is the order without transfer deadline',
+ contract_terms: {
+ ...defaultContractTerms,
+ // @ts-ignore
+ wire_transfer_deadline: undefined,
+ },
+ },
+ ZeroFee: {
+ order_summary: 'example with zero fee',
+ contract_terms: {
+ ...defaultContractTerms,
+ // @ts-ignore
+ max_fee: undefined,
+ // @ts-ignore
+ max_wire_fee: undefined,
+ fulfillment_message: "Congratulations! You just purchased an valuable item!"
+ },
+ },
+}
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..7d11eb21d
--- /dev/null
+++ b/packages/merchant-backend-ui/src/pages/ShowOrderDetails.tsx
@@ -0,0 +1,566 @@
+/*
+ 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";
+import { TIME_DATE_FORMAT } from "../utils";
+
+/**
+ * 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" />
+ <meta name="taler-support" content="uri" />
+ <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 will 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}}`}
+
+ {btr && `{{#contract_terms.fulfillment_message}}`}
+ {(btr || contract_terms?.fulfillment_message) && (
+ <section>
+ <InfoBox>
+ <b>{contract_terms?.fulfillment_message || `{{ contract_terms.fulfillment_message }}`}</b>.
+ </InfoBox>
+ </section>
+ )}
+ {btr && `{{/contract_terms.fulfillment_message}}`}
+
+ <section>
+ <TableExpanded>
+ <dt>Order summary:</dt>
+ <dd>{contract_terms?.summary || `{{ contract_terms.summary }}`}</dd>
+ {btr && `{{#contract_terms.fulfillment_url}}`}
+ <dt>Fulfillment URL:</dt>
+ <dd><a href={contract_terms?.fulfillment_url || `{{ contract_terms.fulfillment_url }}`}>{contract_terms?.fulfillment_url || `{{ contract_terms.fulfillment_url }}`}</a></dd>
+ {btr && `{{/contract_terms.fulfillment_url}}`}
+ <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 * 1000,
+ TIME_DATE_FORMAT,
+ )
+ : "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,
+ TIME_DATE_FORMAT,
+ )
+ : "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,
+ TIME_DATE_FORMAT,
+ )
+ : "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>Exchange transfer deadline:</dt>
+ {btr && `{{` + `#contract_terms.wire_transfer_deadline_str}}`}
+ <dd>
+ {contract_terms?.wire_transfer_deadline
+ ? contract_terms?.wire_transfer_deadline.t_s != "never"
+ ? format(
+ contract_terms?.wire_transfer_deadline.t_s * 1000,
+ TIME_DATE_FORMAT,
+ )
+ : "never"
+ : `{{ contract_terms.wire_transfer_deadline_str }}`}{" "}
+ </dd>
+ {btr && `{{` + `/contract_terms.wire_transfer_deadline_str}}`}
+
+ {btr && `{{` + `^contract_terms.wire_transfer_deadline_str}}`}
+ <dd>
+ Wire transfer settled.
+ </dd>
+ {btr && `{{` + `/contract_terms.wire_transfer_deadline_str}}`}
+
+ {btr && `{{` + `#contract_terms.max_fee}}`}
+ <dt>Maximum deposit fee:</dt>
+ <dd>{contract_terms?.max_fee || `{{ contract_terms.max_fee }}`}</dd>
+ {btr && `{{` + `/contract_terms.max_fee}}`}
+
+ {btr && `{{` + `#contract_terms.max_wire_fee}}`}
+ <dt>Maximum wire fee:</dt>
+ <dd>
+ {contract_terms?.max_wire_fee ||
+ `{{ contract_terms.max_wire_fee }}`}
+ </dd>
+ {btr && `{{` + `/contract_terms.max_wire_fee}}`}
+
+ {btr && `{{` + `#contract_terms.wire_fee_amortization}}`}
+ <dt>Wire fee amortization:</dt>
+ <dd>
+ {contract_terms?.wire_fee_amortization ||
+ `{{ contract_terms.wire_fee_amortization }}`}{" "}
+ transactions
+ </dd>
+ {btr && `{{` + `/contract_terms.wire_fee_amortization}}`}
+ </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 * 1000,
+ TIME_DATE_FORMAT,
+ )
+ : "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 />),
+ };
+}