diff options
Diffstat (limited to 'packages/frontend')
7 files changed, 107 insertions, 49 deletions
diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts index 6717566..21199f4 100644 --- a/packages/frontend/src/declaration.d.ts +++ b/packages/frontend/src/declaration.d.ts @@ -56,45 +56,45 @@ export namespace ExchangeBackend { // Master public key of the exchange, must match the key returned in /keys. master_public_key: EddsaPublicKey; - + // Array of wire accounts operated by the exchange for // incoming wire transfers. accounts: WireAccount[]; - + // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank") // to wire fees. - fees: { method : AggregateTransferFee }; - } - interface WireAccount { + fees: { method: AggregateTransferFee }; + } + interface WireAccount { // payto:// URI identifying the account and wire method payto_uri: string; - + // Signature using the exchange's offline key // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. master_sig: EddsaSignature; - } - interface AggregateTransferFee { + } + interface AggregateTransferFee { // Per transfer wire transfer fee. wire_fee: Amount; - + // Per transfer closing fee. closing_fee: Amount; - + // What date (inclusive) does this fee go into effect? // The different fees must cover the full time period in which // any of the denomination keys are valid without overlap. start_date: Timestamp; - + // What date (exclusive) does this fee stop going into effect? // The different fees must cover the full time period in which // any of the denomination keys are valid without overlap. end_date: Timestamp; - + // Signature of TALER_MasterWireFeePS with // purpose TALER_SIGNATURE_MASTER_WIRE_FEES. sig: EddsaSignature; - } - + } + } export namespace MerchantBackend { interface ErrorDetail { @@ -1000,7 +1000,17 @@ export namespace MerchantBackend { // Is this reserve active (false if it was deleted but not purged)? active: boolean; + + // URI to use to fill the reserve, can be NULL + // if the reserve is inactive or was already filled + payto_uri: string; + + // URL of the exchange hosting the reserve, + // NULL if the reserve is inactive + exchange_url: string; + } + interface TipStatusEntry { // Unique identifier for the tip. diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx index 9d22fc8..ef25500 100644 --- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -44,7 +44,7 @@ interface Props { onBack?: () => void; } -function with_defaults(): Entity { +function with_defaults(): Partial<Entity> { return { inventoryProducts: {}, products: [], @@ -90,8 +90,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [value, valueHandler] = useState(with_defaults()) // const [errors, setErrors] = useState<FormErrors<Entity>>({}) - const inventoryList = Object.values(value.inventoryProducts) - const productList = Object.values(value.products) + const inventoryList = Object.values(value.inventoryProducts || {}) + const productList = Object.values(value.products || {}) let errors: FormErrors<Entity> = {} try { @@ -104,6 +104,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const submit = (): void => { const order = schema.cast(value) + if (!value.payments) return; const request: MerchantBackend.Orders.PostOrderRequest = { order: { @@ -149,14 +150,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const addNewProduct = async (product: MerchantBackend.Product) => { return valueHandler(v => { - const products = [...v.products, product] + const products = v.products ? [...v.products, product] : [] return ({ ...v, products }) }) } const removeFromNewProduct = (index: number) => { valueHandler(v => { - const products = [...v.products] + const products = v.products ? [...v.products] : [] products.splice(index, 1) return ({ ...v, products }) }) @@ -174,7 +175,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { valueHandler(v => { return ({ ...v, pricing: { - ...v.pricing, + ...v.pricing!, products_price: totalPrice, order_price: totalPrice, } @@ -183,17 +184,17 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { }, [hasProducts, totalPrice]) - const discountOrRise = rate(value.pricing.order_price, totalPrice) + const discountOrRise = rate(value.pricing?.order_price || `${config.currency}:0`, totalPrice) useEffect(() => { valueHandler(v => { return ({ ...v, pricing: { - ...v.pricing, + ...v.pricing! } }) }) - }, [value.pricing.order_price]) + }, [value.pricing?.order_price]) const details_response = useInstanceDetails() @@ -236,7 +237,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { </p> } tooltip={i18n`add products to the order that already exist in the inventory`}> <InventoryProductForm - currentProducts={value.inventoryProducts} + currentProducts={value.inventoryProducts || {}} onAddProduct={addProductToTheInventoryList} /> @@ -280,7 +281,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <InputCurrency name="pricing.products_price" label={i18n`Total price`} readonly tooltip={i18n`total product price added up`} /> <InputCurrency name="pricing.order_price" label={i18n`Order price`} - addonAfter={value.pricing.order_price !== totalPrice && (discountOrRise < 1 ? + addonAfter={value.pricing?.order_price !== totalPrice && (discountOrRise < 1 ? `discount of %${Math.round((1 - discountOrRise) * 100)}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } @@ -298,7 +299,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <InputDate name="payments.pay_deadline" label={i18n`Payment deadline`} tooltip={i18n`time until the order can be paid`} /> <InputDate name="payments.delivery_date" label={i18n`Delivery date`} tooltip={i18n`when the order will be delivered, if delivery required`} /> - {value.payments.delivery_date && <InputGroup name="payments.delivery_location" label={i18n`Location`} tooltip={i18n`where the order will be delivered`} > + {value.payments?.delivery_date && <InputGroup name="payments.delivery_location" label={i18n`Location`} tooltip={i18n`where the order will be delivered`} > <InputLocation name="payments.delivery_location" /> </InputGroup>} diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx index 2deae14..d173d7b 100644 --- a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx @@ -18,7 +18,7 @@ import { CreatedSuccessfully as Template } from "../../../../components/notifica import { MerchantBackend } from "../../../../declaration"; import { Translate } from "../../../../i18n"; -type Entity = {request: MerchantBackend.Tips.ReserveCreateRequest, response: MerchantBackend.Tips.ReserveCreateConfirmation}; +type Entity = { request: MerchantBackend.Tips.ReserveCreateRequest, response: MerchantBackend.Tips.ReserveCreateConfirmation }; interface Props { entity: Entity; @@ -81,7 +81,7 @@ export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Prop <p class="is-size-5"><Translate>For example:</Translate></p> <pre> - {entity.response.payto_uri}?message={entity.response.reserve_pub}&amount={entity.request.initial_balance} + {entity.response.payto_uri}?message={entity.response.reserve_pub}&amount={entity.request.initial_balance} </pre> </Template>; } diff --git a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx index 08b463a..08942f6 100644 --- a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx +++ b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { Amounts } from "@gnu-taler/taler-util"; import { format, isAfter } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -42,22 +43,55 @@ type CT = MerchantBackend.ContractTerms interface Props { onBack: () => void; selected: Entity; + id: string; } -export function DetailPage({ selected, onBack }: Props): VNode { +export function DetailPage({ id, selected, onBack }: Props): VNode { const i18n = useTranslator() + const didExchangeAckTransfer = Amounts.isNonZero(Amounts.parseOrThrow(selected.exchange_initial_amount)) return <div class="section main-section"> - <FormProvider object={selected} valueHandler={null} > + <FormProvider object={{ ...selected, id }} valueHandler={null} > <InputDate<Entity> name="creation_time" label={i18n`Created at`} readonly /> <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} readonly /> <InputCurrency<Entity> name="merchant_initial_amount" label={i18n`Created balance`} readonly /> - <InputCurrency<Entity> name="exchange_initial_amount" label={i18n`Exchange balance`} readonly /> - <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} readonly /> - <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} readonly /> + <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} readonly /> + + {didExchangeAckTransfer && <Fragment> + <InputCurrency<Entity> name="exchange_initial_amount" label={i18n`Exchange balance`} readonly /> + <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} readonly /> + <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} readonly /> + </Fragment> + } + <Input<Entity> name="payto_uri" label={i18n`Account address`} readonly /> + <Input name="id" label={i18n`Subject`} readonly /> </FormProvider> - {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <div> - no tips for this reserve - </div>} + + {didExchangeAckTransfer ? <Fragment> + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"><i class="mdi mdi-cash-register" /></span> + <Translate>Tips</Translate> + </p> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <EmptyTable />} + </div></div> + </div> + </div> + </Fragment> : <Fragment> + <p class="is-size-5"><Translate>Now you should transfer to the exchange into the account address indicated above and the transaction must carry the subject message.</Translate></p> + + <p class="is-size-5"><Translate>For example :</Translate></p> + <pre> + {selected.payto_uri}?message={id}&amount={selected.merchant_initial_amount} + </pre> + </Fragment> + } + <div class="columns"> <div class="column" /> <div class="column is-two-thirds"> @@ -71,6 +105,16 @@ export function DetailPage({ selected, onBack }: Props): VNode { </div> } +function EmptyTable(): VNode { + return <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> + </p> + <p><Translate>No tips has been authorized from this reserve</Translate></p> + </div> +} + + async function copyToClipboard(text: string) { return navigator.clipboard.writeText(text) } @@ -124,4 +168,5 @@ function TipRow({ id, entry }: { id: string, entry: MerchantBackend.Tips.TipStat <td>{info.reason}</td> <td>{info.expiration.t_ms === "never" ? "never" : format(info.expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> </tr> -}
\ No newline at end of file +} + diff --git a/packages/frontend/src/paths/instance/reserves/details/index.tsx b/packages/frontend/src/paths/instance/reserves/details/index.tsx index 569ef5b..aaf275b 100644 --- a/packages/frontend/src/paths/instance/reserves/details/index.tsx +++ b/packages/frontend/src/paths/instance/reserves/details/index.tsx @@ -34,7 +34,7 @@ interface Props { onDelete: () => void; onBack: () => void; } -export default function DetailReserve({rid, onUnauthorized, onLoadError, onNotFound, onBack, onDelete}: Props):VNode { +export default function DetailReserve({ rid, onUnauthorized, onLoadError, onNotFound, onBack, onDelete }: Props): VNode { const result = useReserveDetails(rid) if (result.clientError && result.isUnauthorized) return onUnauthorized() @@ -42,6 +42,6 @@ export default function DetailReserve({rid, onUnauthorized, onLoadError, onNotFo if (result.loading) return <Loading /> if (!result.ok) return onLoadError(result) return <Fragment> - <DetailPage selected={result.data} onBack={onBack} /> + <DetailPage selected={result.data} onBack={onBack} id={rid} /> </Fragment> } diff --git a/packages/frontend/src/paths/instance/reserves/list/Table.tsx b/packages/frontend/src/paths/instance/reserves/list/Table.tsx index 83e4fb8..c53dd2a 100644 --- a/packages/frontend/src/paths/instance/reserves/list/Table.tsx +++ b/packages/frontend/src/paths/instance/reserves/list/Table.tsx @@ -21,10 +21,8 @@ import { format } from "date-fns" import { Fragment, h, VNode } from "preact" -import { StateUpdater, useEffect, useState } from "preact/hooks" import { MerchantBackend, WithId } from "../../../../declaration" import { Translate } from "../../../../i18n" -import { Actions, buildActions } from "../../../../utils/table" type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId @@ -54,11 +52,6 @@ export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: {withoutFunds.length > 0 && <div class="card has-table"> <header class="card-header"> <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves not yet funded</Translate></p> - <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </div> </header> <div class="card-content"> <div class="b-table has-pagination"> @@ -73,6 +66,11 @@ export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: <header class="card-header"> <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</Translate></p> <div class="card-header-icon" aria-label="more options" /> + <div class="card-header-icon" aria-label="more options"> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </div> </header> <div class="card-content"> <div class="b-table has-pagination"> diff --git a/packages/frontend/src/utils/amount.ts b/packages/frontend/src/utils/amount.ts index 8fdf048..823ad9d 100644 --- a/packages/frontend/src/utils/amount.ts +++ b/packages/frontend/src/utils/amount.ts @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts } from "@gnu-taler/taler-util"; +import { amountFractionalBase, AmountJson, Amounts } from "@gnu-taler/taler-util"; import { MerchantBackend } from "../declaration"; /** @@ -71,8 +71,12 @@ export const subtractPrices = (one: string, two: string) => { export const rate = (one: string, two: string) => { const a = Amounts.parseOrThrow(one) const b = Amounts.parseOrThrow(two) - const af = Amounts.toFloat(a) - const bf = Amounts.toFloat(b) + const af = toFloat(a) + const bf = toFloat(b) + if (bf === 0) return 0 return af / bf } +function toFloat(amount: AmountJson) { + return amount.value + (amount.fraction / amountFractionalBase); +} |