commit f4f672a3675a54f101af2358519fbce3b1919a21 parent 26d5d2ccbe443184aaa68757cd33dc3e9a6d7ca6 Author: Sebastian <sebasjm@gmail.com> Date: Fri, 26 Mar 2021 18:03:29 -0300 added copy payment url and order creation page Diffstat:
13 files changed, 326 insertions(+), 110 deletions(-)
diff --git a/packages/frontend/src/AdminRoutes.tsx b/packages/frontend/src/AdminRoutes.tsx @@ -24,7 +24,6 @@ import InstanceCreatePage from "./paths/admin/create"; export enum AdminPaths { list_instances = '/instances', new_instance = '/instance/new', - instance_id_route = '/instance/:id/:rest*', } export function AdminRoutes(): VNode { diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -63,6 +63,7 @@ export enum InstancePaths { product_new = '/product/new', order_list = '/orders', + order_new = '/order/new', order_details = '/order/:oid/details', // order_new = '/order/new', @@ -370,7 +371,12 @@ export function InstanceRoutes({ id, admin }: Props): VNode { <LoginPage onConfirm={updateLoginStatus} /> </Fragment> }} + + onCreate={() => { + route(InstancePaths.order_new) + }} /> + <Route path={InstancePaths.order_details} component={OrderDetailsPage} @@ -396,7 +402,18 @@ export function InstanceRoutes({ id, admin }: Props): VNode { route(InstancePaths.order_list) }} /> - {/* + <Route path={InstancePaths.order_new} + component={OrderCreatePage} + + onConfirm={() => { + route(InstancePaths.order_list) + }} + + onBack={() => { + route(InstancePaths.order_list) + }} + /> + {/* <Route path={InstancePaths.tips_list} component={TipListPage} diff --git a/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx b/packages/frontend/src/components/notifications/CreatedSuccessfully.tsx @@ -0,0 +1,31 @@ +import { ComponentChildren, h } from "preact"; + +interface Props { + onCreateAnother?: () => void; + onConfirm: () => void; + children: ComponentChildren; +} + +export function CreatedSuccessfully({ children, onConfirm, onCreateAnother }: Props) { + return <div class="columns is-fullwidth is-vcentered content-full-size"> + <div class="column" /> + <div class="column is-three-quarters"> + <div class="card"> + <header class="card-header has-background-success"> + <p class="card-header-title has-text-white-ter"> + Created successfully + </p> + </header> + <div class="card-content"> + {children} + </div> + </div> + <div class="buttons is-right"> + {onCreateAnother && <button class="button is-info" onClick={onCreateAnother}>Create another</button>} + <button class="button is-info" onClick={onConfirm}>Continue</button> + </div> + </div> + <div class="column" /> + </div> +} + diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -220,6 +220,7 @@ interface OrderMutateAPI { forgetOrder: (id: string, data: MerchantBackend.Orders.ForgetRequest) => Promise<void>; refundOrder: (id: string, data: MerchantBackend.Orders.RefundRequest) => Promise<MerchantBackend.Orders.MerchantRefundResponse>; deleteOrder: (id: string) => Promise<void>; + getPaymentURL: (id: string) => Promise<string>; } export function useOrderMutateAPI(): OrderMutateAPI { @@ -270,7 +271,16 @@ export function useOrderMutateAPI(): OrderMutateAPI { mutateAll(/@"\/private\/orders"@/) } - return { createOrder, forgetOrder, deleteOrder, refundOrder } + + const getPaymentURL = async (orderId: string): Promise<string> => { + const data = await request(`${url}/private/orders/${orderId}`, { + method: 'get', + token + }) + return data.taler_pay_uri || data.contract_terms?.fulfillment_url + } + + return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL } } export function useOrderDetails(oderId:string): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> { @@ -284,7 +294,6 @@ export function useOrderDetails(oderId:string): HttpResponse<MerchantBackend.Ord return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } - interface TransferMutateAPI { informTransfer: (data: MerchantBackend.Transfers.TransferInformation) => Promise<MerchantBackend.Transfers.MerchantTrackTransferResponse>; } diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -341,3 +341,20 @@ msgstr "Order status URL" msgid "fields.instance.taler_pay_uri.label" msgstr "Taler Pay URI" +# msgid "fields.instance.amount.placeholder" +# msgstr "" + +# msgid "fields.instance.amount.tooltip" +# msgstr "" + +msgid "fields.instance.amount.label" +msgstr "Amount" + +# msgid "fields.instance.summary.placeholder" +# msgstr "" + +# msgid "fields.instance.summary.tooltip" +# msgstr "" + +msgid "fields.instance.summary.label" +msgstr "Summary" diff --git a/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx diff --git a/packages/frontend/src/paths/admin/create/index.tsx b/packages/frontend/src/paths/admin/create/index.tsx @@ -20,6 +20,7 @@ import { MerchantBackend } from "../../../declaration"; import { useAdminMutateAPI } from "../../../hooks/backend"; import { Notification } from "../../../utils/types"; import { CreatePage } from "./CreatePage"; +import { InstanceCreatedSuccessfully } from "./InstanceCreatedSuccessfully"; interface Props { onBack?: () => void; @@ -27,7 +28,7 @@ interface Props { onError: (error: any) => void; forceId?: string; } -type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; +export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; export default function Create({ onBack, onConfirm, onError, forceId }: Props): VNode { const { createInstance } = useAdminMutateAPI(); @@ -35,47 +36,7 @@ export default function Create({ onBack, onConfirm, onError, forceId }: Props): const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined); if (createdOk) { - return <CreatedSuccessfully onConfirm={onConfirm}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">ID</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={createdOk.id} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Business Name</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={createdOk.name} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Token</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - {createdOk.auth.method === 'external' && 'external'} - {createdOk.auth.method === 'token' && - <input class="input" readonly value={createdOk.auth.token} /> - } - </p> - </div> - </div> - </div> - </CreatedSuccessfully> + return <InstanceCreatedSuccessfully entity={createdOk} onConfirm={onConfirm} /> } return <Fragment> @@ -97,39 +58,6 @@ export default function Create({ onBack, onConfirm, onError, forceId }: Props): }) }} /> </Fragment> + } -interface CreatedSuccessfullyProps { - onConfirm: () => void; - children: ComponentChildren; -} - -function CreatedSuccessfully({ children, onConfirm }: CreatedSuccessfullyProps) { - return <div class="columns is-fullwidth is-vcentered content-full-size"> - <div class="column" /> - <div class="column is-three-quarters"> - <div class="card"> - <header class="card-header has-background-success"> - <p class="card-header-title has-text-white-ter"> - Instance created successfully - </p> - </header> - <div class="card-content"> - {children} - </div> - <footer class="card-footer"> - <p class="card-footer-item" style={{ border: 'none' }}> - <span /> - </p> - <p class="card-footer-item" style={{ border: 'none' }}> - <span /> - </p> - <p class="card-footer-item"> - <button class="button is-info" onClick={onConfirm}>Continue</button> - </p> - </footer> - </div> - </div> - <div class="column" /> - </div> -} -\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -0,0 +1,90 @@ +/* + 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 } from "preact"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration"; +import * as yup from 'yup'; +import { FormErrors, FormProvider } from "../../../../components/form/Field" +import { OrderCreateSchema as schema } from "../../../../schemas" +import { Message } from "preact-messages"; +import { Input } from "../../../../components/form/Input"; +import { useConfigContext } from "../../../../context/backend"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; + +type Entity = MerchantBackend.Orders.MinimalOrderDetail + +interface Props { + onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; + onBack?: () => void; +} + +interface KeyValue { + [key: string]: string; +} + +function with_defaults(id?: string): Partial<Entity> { + return {}; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [value, valueHandler] = useState(with_defaults()) + const [errors, setErrors] = useState<FormErrors<Entity>>({}) + + const submit = (): void => { + try { + schema.validateSync(value, { abortEarly: false }) + const order = schema.cast(value) as Entity + onCreate({ order }); + } catch (err) { + const errors = err.inner as yup.ValidationError[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) + setErrors(pathMessages) + } + } + const config = useConfigContext() + + return <div> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > + + <InputCurrency<Entity> name="amount" currency={config.currency} /> + + <Input<Entity> name="summary" /> + + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button>} + <button class="button is-success" onClick={submit} ><Message id="Confirm" /></button> + </div> + + </div> + <div class="column" /> + </div> + </section> + + </div> +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx @@ -0,0 +1,73 @@ +import { h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully"; +import { useOrderMutateAPI } from "../../../../hooks/backend"; +import { Entity } from "./index"; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props) { + const { getPaymentURL } = useOrderMutateAPI() + const [url, setURL] = useState<string | undefined>(undefined) + + useEffect(() => { + getPaymentURL(entity.response.order_id).then(url => { + setURL(url) + }) + },[entity.response.order_id]) + + return <CreatedSuccessfully onConfirm={onConfirm} onCreateAnother={onCreateAnother}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.request.order.amount} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Summary</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.request.order.summary} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Order ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.response.order_id} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Payment URL</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={url} /> + </p> + </div> + </div> + </div> + </CreatedSuccessfully>; +} diff --git a/packages/frontend/src/paths/instance/orders/create/index.tsx b/packages/frontend/src/paths/instance/orders/create/index.tsx @@ -19,8 +19,47 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from 'preact'; +import { Fragment, h, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend } from '../../../../declaration'; +import { useOrderMutateAPI } from '../../../../hooks/backend'; +import { Notification } from '../../../../utils/types'; +import { CreatePage } from './CreatePage'; +import { OrderCreatedSuccessfully } from './OrderCreatedSuccessfully'; -export default function ():VNode { - return <div>order create page</div> +export type Entity = { + request: MerchantBackend.Orders.PostOrderRequest, + response: MerchantBackend.Orders.PostOrderResponse +} +interface Props { + onBack?: () => void; + onConfirm: () => void; +} +export default function ({ onConfirm, onBack }: Props): VNode { + const { createOrder } = useOrderMutateAPI() + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined); + + if (createdOk) { + return <OrderCreatedSuccessfully entity={createdOk} onConfirm={onConfirm} onCreateAnother={() => setCreatedOk(undefined)} /> + } + + return <Fragment> + <NotificationCard notification={notif} /> + + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => { + createOrder(request).then((response) => { + setCreatedOk({ request, response }) + }).catch((error) => { + setNotif({ + message: 'could not create order', + type: "ERROR", + description: error.message + }) + }) + }} /> + </Fragment> } \ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -38,6 +38,7 @@ type Entity = MerchantBackend.Orders.OrderHistoryEntry & { id: string } interface Props { instances: Entity[]; onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; + onCopyURL: (id: string) => void; onCreate: () => void; onSelect: (order: Entity) => void; onLoadMoreBefore?: () => void; @@ -49,7 +50,7 @@ interface Props { // onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} -export function CardTable({ instances, onCreate, onRefund, onSelect, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { +export function CardTable({ instances, onCreate, onRefund, onCopyURL, onSelect, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); const [rowSelection, rowSelectionHandler] = useState<string[]>([]) @@ -74,6 +75,7 @@ export function CardTable({ instances, onCreate, onRefund, onSelect, onLoadMoreA <div class="table-wrapper has-mobile-cards"> {instances.length > 0 ? <Table instances={instances} onSelect={onSelect} onRefund={(order) => setShowRefund(order.id)} + onCopyURL={o => onCopyURL(o.id)} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} hasMoreAfter={hasMoreAfter} hasMoreBefore={hasMoreBefore} @@ -96,6 +98,7 @@ interface TableProps { rowSelection: string[]; instances: Entity[]; onRefund: (id: Entity) => void; + onCopyURL: (id: Entity) => void; onSelect: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; @@ -104,9 +107,9 @@ interface TableProps { onLoadMoreAfter?: () => void; } -function Table({ instances, onSelect, onRefund, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { +function Table({ instances, onSelect, onRefund, onCopyURL, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { return <div class="table-container"> - {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more after </button>} + {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load newer orders </button>} <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -129,13 +132,18 @@ function Table({ instances, onSelect, onRefund, onLoadMoreAfter, onLoadMoreBefor Refund </button> } + {(!i.paid || pos === 0) && + <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onCopyURL(i)}> + copy url + </button> + } </div> </td> </tr> })} </tbody> </table> - {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more before </button>} + {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load older orders </button>} </div> } diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -40,13 +40,13 @@ interface Props { export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode { - const [filter, setFilter] = useState<InstanceOrderFilter>({paid:'yes'}) + const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: 'yes' }) const [pickDate, setPickDate] = useState(false) - const setNewDate = (date:Date) => setFilter(prev => ({...prev,date})) + const setNewDate = (date: Date) => setFilter(prev => ({ ...prev, date })) const result = useInstanceOrders(filter, setNewDate) - const { createOrder, refundOrder } = useOrderMutateAPI() + const { createOrder, refundOrder, getPaymentURL } = useOrderMutateAPI() const { currency } = useConfigContext() let instances: (MerchantBackend.Orders.OrderHistoryEntry & { id: string })[]; @@ -73,17 +73,11 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo return <section class="section is-main-section"> <NotificationCard notification={{ message: 'DEMO WARNING', - type:'WARN', + type: 'WARN', description: 'refund button is being forced in the first row, other depends on the refundable property' }} /> <NotificationCard notification={notif} /> - <DatePicker - opened={pickDate} - closeFunction={() => setPickDate(false)} - dateReceiver={setNewDate} - /> - <div class="columns"> <div class="column"> <div class="tabs"> @@ -98,11 +92,11 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo <div class="column "> <div class="buttons is-right"> <div class="field has-addons"> - { filter.date && <div class="control"> - <a class="button" onClick={() => { setFilter(prev => ({ ...prev, date:undefined }) ) }}> + {filter.date && <div class="control"> + <a class="button" onClick={() => { setFilter(prev => ({ ...prev, date: undefined })) }}> <span class="icon"><i class="mdi mdi-close" /></span> </a> - </div> } + </div>} <div class="control"> <input class="input" type="text" readonly value={!filter.date ? '' : format(filter.date, 'yyyy/MM/dd')} placeholder="pick a date" /> </div> @@ -116,19 +110,23 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo </div> </div> + <DatePicker + opened={pickDate} + closeFunction={() => setPickDate(false)} + dateReceiver={setNewDate} + /> + <CardTable instances={instances} - onCreate={() => createOrder({ - order: { - amount: `${currency}:${Math.floor(Math.random() * 20 + 1)}`, - summary: `some summary with a random number ${Math.floor(Math.random() * 20 + 1)}`, - } - })} + onCreate={onCreate} onSelect={(order) => onSelect(order.id)} + onCopyURL={(id) => getPaymentURL(id).then(url => { + navigator.clipboard.writeText(url) + })} onRefund={(id, value) => refundOrder(id, value) .then(() => setNotif({ message: 'refund created successfully', type: "SUCCESS" - })).catch((error) => setNotif({ + })).catch((error) => setNotif({ message: 'could not create the refund', type: "ERROR", description: error.message diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -111,4 +111,12 @@ export const RefoundSchema = yup.object().shape({ refund: yup.string() .required() .test('amount', 'the amount is not valid', currencyWithAmountIsValid), -}) -\ No newline at end of file +}) + + +export const OrderCreateSchema = yup.object().shape({ + summary: yup.string().required(), + amount: yup.string() + .required() + .test('amount', 'the amount is not valid', currencyWithAmountIsValid), +})