diff options
Diffstat (limited to 'packages')
17 files changed, 3130 insertions, 4 deletions
diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx index 163438654..c661fb900 100644 --- a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -28,10 +28,9 @@ import { import { format } from "date-fns"; import { Fragment, FunctionComponent, h, VNode } from "preact"; import { Route, route, Router } from "preact-router"; -import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { Loading } from "./components/exception/loading.js"; import { Menu, NotificationCard } from "./components/menu/index.js"; -import { useBackendContext } from "./context/backend.js"; import { InstanceContextProvider } from "./context/instance.js"; import { useBackendDefaultToken, @@ -69,6 +68,7 @@ import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; + import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { @@ -371,6 +371,249 @@ export function InstanceRoutes({ route(InstancePaths.deposit_confirmation_list); }} /> + {/** + * Order pages + */} + <Route + path={InstancePaths.order_list} + component={OrderListPage} + onCreate={() => { + route(InstancePaths.order_new); + }} + onSelect={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.order_details} + component={OrderDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + <Route + path={InstancePaths.order_new} + component={OrderCreatePage} + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); + }} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + {/** + * Transfer pages + */} + <Route + path={InstancePaths.transfers_list} + component={TransferListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.transfers_new); + }} + /> + <Route + path={InstancePaths.transfers_new} + component={TransferCreatePage} + onConfirm={() => { + route(InstancePaths.transfers_list); + }} + onBack={() => { + route(InstancePaths.transfers_list); + }} + /> + {/** + * Webhooks pages + */} + <Route + path={InstancePaths.webhooks_list} + component={WebhookListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.webhooks_new); + }} + onSelect={(id: string) => { + route(InstancePaths.webhooks_update.replace(":tid", id)); + }} + /> + <Route + path={InstancePaths.webhooks_update} + component={WebhookUpdatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + <Route + path={InstancePaths.webhooks_new} + component={WebhookCreatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + {/** + * Validator pages + */} + <Route + path={InstancePaths.otp_devices_list} + component={ValidatorListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.otp_devices_new); + }} + onSelect={(id: string) => { + route(InstancePaths.otp_devices_update.replace(":vid", id)); + }} + /> + <Route + path={InstancePaths.otp_devices_update} + component={ValidatorUpdatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + <Route + path={InstancePaths.otp_devices_new} + component={ValidatorCreatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + {/** + * Templates pages + */} + <Route + path={InstancePaths.templates_list} + component={TemplateListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.templates_new); + }} + onNewOrder={(id: string) => { + route(InstancePaths.templates_use.replace(":tid", id)); + }} + onQR={(id: string) => { + route(InstancePaths.templates_qr.replace(":tid", id)); + }} + onSelect={(id: string) => { + route(InstancePaths.templates_update.replace(":tid", id)); + }} + /> + <Route + path={InstancePaths.templates_update} + component={TemplateUpdatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_new} + component={TemplateCreatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_use} + component={TemplateUsePage} + onOrderCreated={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_qr} + component={TemplateQrPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + + {/** + * reserves pages + */} + <Route + path={InstancePaths.reserves_list} + component={ReservesListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onSelect={(id: string) => { + route(InstancePaths.reserves_details.replace(":rid", id)); + }} + onCreate={() => { + route(InstancePaths.reserves_new); + }} + /> + <Route + path={InstancePaths.reserves_details} + component={ReservesDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route + path={InstancePaths.reserves_new} + component={ReservesCreatePage} + onConfirm={() => { + route(InstancePaths.reserves_list); + }} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route path={InstancePaths.kyc} component={ListKYCPage} /> <Route path={InstancePaths.interface} component={Settings} /> {/** * Example pages diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx index 665137415..4ffad2d6d 100644 --- a/packages/merchant-backoffice-ui/src/Routing.tsx +++ b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -60,6 +60,9 @@ import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js"; import TokenPage from "./paths/instance/token/index.js"; +import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js"; +import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js"; +import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { @@ -106,6 +109,10 @@ export enum InstancePaths { templates_use = "/templates/:tid/use", templates_qr = "/templates/:tid/qr", + token_family_list = "/tokenfamilies", + token_family_update = "/tokenfamilies/:slug/update", + token_family_new = "/tokenfamilies/new", + webhooks_list = "/webhooks", webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", @@ -472,6 +479,39 @@ export function Routing(_p: Props): VNode { route(InstancePaths.transfers_list); }} /> + {/* * + * Token family pages + */} + <Route + path={InstancePaths.token_family_list} + component={TokenFamilyListPage} + onCreate={() => { + route(InstancePaths.token_family_new); + }} + onSelect={(slug: string) => { + route(InstancePaths.token_family_update.replace(":slug", slug)); + }} + /> + <Route + path={InstancePaths.token_family_update} + component={TokenFamilyUpdatePage} + onConfirm={() => { + route(InstancePaths.token_family_list); + }} + onBack={() => { + route(InstancePaths.token_family_list); + }} + /> + <Route + path={InstancePaths.token_family_new} + component={TokenFamilyCreatePage} + onConfirm={() => { + route(InstancePaths.token_family_list); + }} + onBack={() => { + route(InstancePaths.token_family_list); + }} + /> {/** * Webhooks pages */} diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index 4a1f6a9df..aeb49e81d 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -118,6 +118,16 @@ export function Sidebar({ mobile }: Props): VNode { </span> </a> </li> + <li> + <a href={"/tokenfamilies"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-key" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Token Families</i18n.Translate> + </span> + </a> + </li> {needKYC && ( <li> <a href={"/kyc"} class="has-icon"> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index a35c07ace..123271f8d 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -68,6 +68,12 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: Use template`; case InstancePaths.interface: return `${id}: Interface`; + case InstancePaths.token_family_list: + return `${id}: Token families`; + case InstancePaths.token_family_new: + return `${id}: New token family`; + case InstancePaths.token_family_update: + return `${id}: Update token family`; default: return ""; } diff --git a/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx new file mode 100644 index 000000000..1492beb48 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx @@ -0,0 +1,140 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import * as yup from "yup"; +import { TokenFamilyCreateSchema } from "../../schemas/index.js"; +import { FormErrors, FormProvider } from "../form/FormProvider.js"; +import { Input } from "../form/Input.js"; +import { InputWithAddon } from "../form/InputWithAddon.js"; +import { InputDate } from "../form/InputDate.js"; +import { InputDuration } from "../form/InputDuration.js"; +import { InputSelector } from "../form/InputSelector.js"; +import { useSessionContext } from "../../context/session.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; + +type Entity = TalerMerchantApi.TokenFamilyCreateRequest; + +interface Props { + onSubscribe: (c?: () => Entity | undefined) => void; + initial?: Partial<Entity>; + alreadyExist?: boolean; +} + +export function TokenFamilyForm({ onSubscribe }: Props) { + const [value, valueHandler] = useState<Partial<Entity>>({ + slug: "", + name: "", + description: "", + description_i18n: {}, + kind: TalerMerchantApi.TokenFamilyKind.Discount, + duration: { d_us: "forever" }, + valid_after: { t_s: "never" }, + valid_before: { t_s: "never" }, + }); + let errors: FormErrors<Entity> = {}; + + try { + TokenFamilyCreateSchema.validateSync(value, { + abortEarly: false, + }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = useCallback((): Entity | undefined => { + // HACK: Think about how this can be done better + return value as Entity; + }, [value]); + + useEffect(() => { + onSubscribe(hasErrors ? undefined : submit); + }, [submit, hasErrors]); + + const { state } = useSessionContext(); + const { i18n } = useTranslationContext(); + + return ( + <div> + <FormProvider<Entity> + name="token_family" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <InputWithAddon<Entity> + name="slug" + addonBefore={new URL("tokenfamily/", state.backendUrl.href).href} + label={i18n.str`Slug`} + tooltip={i18n.str`token family slug to use in URLs (for internal use only)`} + /> + <InputSelector<Entity> + name="kind" + label={i18n.str`Kind`} + tooltip={i18n.str`token family kind`} + values={["discount", "subscription"]} + /> + <Input<Entity> + name="name" + inputType="text" + label={i18n.str`Name`} + tooltip={i18n.str`user-readable token family name`} + /> + <Input<Entity> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`token family description for customers`} + /> + <InputDate<Entity> + name="valid_after" + label={i18n.str`Valid After`} + tooltip={i18n.str`token family can issue tokens after this date`} + withTimestampSupport + /> + <InputDate<Entity> + name="valid_before" + label={i18n.str`Valid Before`} + tooltip={i18n.str`token family can issue tokens until this date`} + withTimestampSupport + /> + <InputDuration<Entity> + name="duration" + label={i18n.str`Duration`} + tooltip={i18n.str`validity duration of a issued token`} + withForever + /> + </FormProvider> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts index 1baf80ba6..6f6e23b42 100644 --- a/packages/merchant-backoffice-ui/src/declaration.d.ts +++ b/packages/merchant-backoffice-ui/src/declaration.d.ts @@ -22,3 +22,1707 @@ interface WithId { id: string; } + +type Amount = string; +type UUID = string; +type Integer = number; + +interface WireAccount { + // payto:// URI identifying the account and wire method + payto_uri: string; + + // URI to convert amounts from or to the currency used by + // this wire account of the exchange. Missing if no + // conversion is applicable. + conversion_url?: string; + + // Restrictions that apply to bank accounts that would send + // funds to the exchange (crediting this exchange bank account). + // Optional, empty array for unrestricted. + credit_restrictions: AccountRestriction[]; + + // Restrictions that apply to bank accounts that would receive + // funds from the exchange (debiting this exchange bank account). + // Optional, empty array for unrestricted. + debit_restrictions: AccountRestriction[]; + + // Signature using the exchange's offline key over + // a TALER_MasterWireDetailsPS + // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS. + master_sig: EddsaSignature; +} + +type AccountRestriction = RegexAccountRestriction | DenyAllAccountRestriction; + +// Account restriction that disables this type of +// account for the indicated operation categorically. +interface DenyAllAccountRestriction { + type: "deny"; +} + +// Accounts interacting with this type of account +// restriction must have a payto://-URI matching +// the given regex. +interface RegexAccountRestriction { + type: "regex"; + + // Regular expression that the payto://-URI of the + // partner account must follow. The regular expression + // should follow posix-egrep, but without support for character + // classes, GNU extensions, back-references or intervals. See + // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html + // for a description of the posix-egrep syntax. Applications + // may support regexes with additional features, but exchanges + // must not use such regexes. + payto_regex: string; + + // Hint for a human to understand the restriction + // (that is hopefully easier to comprehend than the regex itself). + human_hint: string; + + // Map from IETF BCP 47 language tags to localized + // human hints. + human_hint_i18n?: { [lang_tag: string]: string }; +} +interface LoginToken { + token: string, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +type AccessToken = string & { + [__ac_token]: true; +}; + +export namespace ExchangeBackend { + interface WireResponse { + // 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 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 { + // Numeric error code unique to the condition. + // The other arguments are specific to the error value reported here. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... + // Should give a human-readable hint about the error's nature. Optional, may change without notice! + hint?: string; + + // Optional detail about the specific input value that failed. May change without notice! + detail?: string; + + // Name of the parameter that was bogus (if applicable). + parameter?: string; + + // Path to the argument that was bogus (if applicable). + path?: string; + + // Offset of the argument that was bogus (if applicable). + offset?: string; + + // Index of the argument that was bogus (if applicable). + index?: string; + + // Name of the object that was bogus (if applicable). + object?: string; + + // Name of the currency than was problematic (if applicable). + currency?: string; + + // Expected type (if applicable). + type_expected?: string; + + // Type that was provided instead (if applicable). + type_actual?: string; + } + + // Delivery location, loosely modeled as a subset of + // ISO20022's PostalAddress25. + interface Tax { + // the name of the tax + name: string; + + // amount paid in tax + tax: Amount; + } + + interface Auditor { + // official name + name: string; + + // Auditor's public key + auditor_pub: EddsaPublicKey; + + // Base URL of the auditor + url: string; + } + interface Exchange { + // the exchange's base URL + url: string; + + // master public key of the exchange + master_pub: EddsaPublicKey; + } + + interface Product { + // merchant-internal identifier for the product. + product_id?: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n?: { [lang_tag: string]: string }; + + // The number of units of the product to deliver to the customer. + quantity: Integer; + + // The unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price of the product; this is the total price for quantity times unit of this product. + price?: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for this product. Can be empty. + taxes: Tax[]; + + // time indicating when this product should be delivered + delivery_date?: TalerProtocolTimestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + interface Merchant { + // label for a location with the business address of the merchant + address: Location; + + // the merchant's legal name of business + name: string; + + // label for a location that denotes the jurisdiction for disputes. + // Some of the typical fields for a location (such as a street address) may be absent. + jurisdiction: Location; + } + + interface VersionResponse { + // libtool-style representation of the Merchant protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Name of the protocol. + name: "taler-merchant"; + + // Currency supported by this backend. + currency: string; + } + interface Location { + // Nation with its own government. + country?: string; + + // Identifies a subdivision of a country such as state, region, county. + country_subdivision?: string; + + // Identifies a subdivision within a country sub-division. + district?: string; + + // Name of a built-up area, with defined boundaries, and a local government. + town?: string; + + // Specific location name within the town. + town_location?: string; + + // Identifier consisting of a group of letters and/or numbers that + // is added to a postal address to assist the sorting of mail. + post_code?: string; + + // Name of a street or thoroughfare. + street?: string; + + // Name of the building or house. + building_name?: string; + + // Number that identifies the position of a building on a street. + building_number?: string; + + // Free-form address lines, should not exceed 7 elements. + address_lines?: string[]; + } + namespace Instances { + //POST /private/instances/$INSTANCE/auth + interface InstanceAuthConfigurationMessage { + // Type of authentication. + // "external": The mechant backend does not do + // any authentication checks. Instead an API + // gateway must do the authentication. + // "token": The merchant checks an auth token. + // See "token" for details. + method: "external" | "token"; + + // For method "external", this field is mandatory. + // The token MUST begin with the string "secret-token:". + // After the auth token has been set (with method "token"), + // the value must be provided in a "Authorization: Bearer $token" + // header. + token?: string; + } + //POST /private/instances + interface InstanceConfigurationMessage { + // Name of the merchant instance to create (will become $INSTANCE). + id: string; + + // Merchant name corresponding to this instance. + name: string; + + // Type of the user (business or individual). + // Defaults to 'business'. Should become mandatory field + // in the future, left as optional for API compatibility for now. + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // "Authentication" header required to authorize management access the instance. + // Optional, if not given authentication will be disabled for + // this instance (hopefully authentication checks are still + // done by some reverse proxy). + auth: InstanceAuthConfigurationMessage; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + } + + // PATCH /private/instances/$INSTANCE + interface InstanceReconfigurationMessage { + + // Merchant name corresponding to this instance. + name: string; + + // Type of the user (business or individual). + // Defaults to 'business'. Should become mandatory field + // in the future, left as optional for API compatibility for now. + user_type?: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + } + + // GET /private/instances + interface InstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: Instance[]; + } + + interface Instance { + // Merchant name corresponding to this instance. + name: string; + + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; + + // Has this instance been deleted (but not purged)? + deleted: boolean; + } + + //GET /private/instances/$INSTANCE + interface QueryInstancesResponse { + + // Merchant name corresponding to this instance. + name: string; + // Type of the user ("business" or "individual"). + user_type: MerchantUserType; + + // Merchant email for customer contact. + email?: string; + + // Merchant public website. + website?: string; + + // Merchant logo. + logo?: ImageDataUrl; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; + + // The merchant's physical address (to be put into contracts). + address: Location; + + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Use STEFAN curves to determine default fees? + // If false, no fees are allowed by default. + // Can always be overridden by the frontend on a per-order basis. + use_stefan: boolean; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; + + // Authentication configuration. + // Does not contain the token when token auth is configured. + auth: { + method: "external" | "token"; + }; + } + // DELETE /private/instances/$INSTANCE + interface LoginTokenRequest { + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Can this token be refreshed? + // Defaults to false. + refreshable?: boolean; + } + interface LoginTokenSuccessResponse { + // The login token that can be used to access resources + // that are in scope for some time. Must be prefixed + // with "Bearer " when used in the "Authorization" HTTP header. + // Will already begin with the RFC 8959 prefix. + token: string; + + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + expiration: Timestamp; + + // Can this token be refreshed? + refreshable: boolean; + } + } + + namespace KYC { + //GET /private/instances/$INSTANCE/kyc + interface AccountKycRedirects { + // Array of pending KYCs. + pending_kycs: MerchantAccountKycRedirect[]; + + // Array of exchanges with no reply. + timeout_kycs: ExchangeKycTimeout[]; + } + interface MerchantAccountKycRedirect { + // URL that the user should open in a browser to + // proceed with the KYC process (as returned + // by the exchange's /kyc-check/ endpoint). + // Optional, missing if the account is blocked + // due to AML and not due to KYC. + kyc_url?: string; + + // Base URL of the exchange this is about. + exchange_url: string; + + // AML status of the account. + aml_status: number; + + // Our bank wire account this is about. + payto_uri: string; + } + interface ExchangeKycTimeout { + // Base URL of the exchange this is about. + exchange_url: string; + + // Numeric error code indicating errors the exchange + // returned, or TALER_EC_INVALID for none. + exchange_code: number; + + // HTTP status code returned by the exchange when we asked for + // information about the KYC status. + // 0 if there was no response at all. + exchange_http_status: number; + } + + } + + namespace BankAccounts { + + interface AccountAddDetails { + + // payto:// URI of the account. + payto_uri: string; + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; + + } + + type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; + + interface NoFacadeCredentials { + type: "none"; + } + + interface BasicAuthFacadeCredentials { + type: "basic"; + + // Username to use to authenticate + username: string; + + // Password to use to authenticate + password: string; + } + + interface AccountAddResponse { + // Hash over the wire details (including over the salt). + h_wire: HashCode; + + // Salt used to compute h_wire. + salt: HashCode; + } + + interface AccountPatchDetails { + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + // To really delete credentials, set them to the type: "none". + credit_facade_credentials?: FacadeCredentials; + } + + + interface AccountsSummaryResponse { + + // List of accounts that are known for the instance. + accounts: BankAccountEntry[]; + } + + interface BankAccountEntry { + // payto:// URI of the account. + payto_uri: string; + + // Hash over the wire details (including over the salt) + h_wire: HashCode; + + // salt used to compute h_wire + salt: HashCode; + + // URL from where the merchant can download information + // about incoming wire transfers to this account. + credit_facade_url?: string; + + // Credentials to use when accessing the credit facade. + // Never returned on a GET (as this may be somewhat + // sensitive data). Can be set in POST + // or PATCH requests to update (or delete) credentials. + credit_facade_credentials?: FacadeCredentials; + + // true if this account is active, + // false if it is historic. + active: boolean; + } + + } + + namespace Products { + // POST /private/products + interface ProductAddDetail { + // product ID to use. + product_id: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + // PATCH /private/products/$PRODUCT_ID + interface ProductPatchDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + + // GET /private/products + interface InventorySummaryResponse { + // List of products that are present in the inventory + products: InventoryEntry[]; + } + interface InventoryEntry { + // Product identifier, as found in the product. + product_id: string; + } + + // GET /private/products/$PRODUCT_ID + interface ProductDetail { + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one unit of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that have already been sold. + total_sold: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } + + // POST /private/products/$PRODUCT_ID/lock + interface LockRequest { + // UUID that identifies the frontend performing the lock + // It is suggested that clients use a timeflake for this, + // see https://github.com/anthonynsimon/timeflake + lock_uuid: UUID; + + // How long does the frontend intend to hold the lock + duration: RelativeTime; + + // How many units should be locked? + quantity: Integer; + } + + // DELETE /private/products/$PRODUCT_ID + } + + namespace Orders { + type MerchantOrderStatusResponse = + | CheckPaymentPaidResponse + | CheckPaymentClaimedResponse + | CheckPaymentUnpaidResponse; + interface CheckPaymentPaidResponse { + // The customer paid for this contract. + order_status: "paid"; + + // Was the payment refunded (even partially)? + refunded: boolean; + + // True if there are any approved refunds that the wallet has + // not yet obtained. + refund_pending: boolean; + + // Did the exchange wire us the funds? + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: Amount; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: Amount; + + // Contract terms. + contract_terms: ContractTerms; + + // The wire transfer status from the exchange for this order if + // available, otherwise empty array. + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, + // empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + } + interface CheckPaymentClaimedResponse { + // A wallet claimed the order, but did not yet pay for the contract. + order_status: "claimed"; + + // Contract terms. + contract_terms: ContractTerms; + } + interface CheckPaymentUnpaidResponse { + // The order was neither claimed nor paid. + order_status: "unpaid"; + + // when was the order created + creation_time: Timestamp; + + // Order summary text. + summary: string; + + // Total amount of the order (to be paid by the customer). + total_amount: Amount; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // Fulfillment URL of an already paid order. Only given if under this + // session an already paid order with a fulfillment URL exists. + already_paid_fulfillment_url?: string; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. + } + interface RefundDetails { + // Reason given for the refund. + reason: string; + + // When was the refund approved. + timestamp: Timestamp; + + // Set to true if a refund is still available for the wallet for this payment. + pending: boolean; + + // Total amount that was refunded (minus a refund fee). + amount: Amount; + } + interface TransactionWireTransfer { + // Responsible exchange. + exchange_url: string; + + // 32-byte wire transfer identifier. + wtid: Base32; + + // Execution time of the wire transfer. + execution_time: Timestamp; + + // Total amount that has been wire transferred + // to the merchant. + amount: Amount; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; + } + interface TransactionWireReport { + // Numerical error code. + code: number; + + // Human-readable error description. + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKey; + } + + interface OrderHistory { + // timestamp-sorted array of all orders matching the query. + // The order of the sorting depends on the sign of delta. + orders: OrderHistoryEntry[]; + } + interface OrderHistoryEntry { + // order ID of the transaction related to this entry. + order_id: string; + + // row ID of the order in the database + row_id: number; + + // when the order was created + timestamp: Timestamp; + + // the amount of money the order is for + amount: Amount; + + // the summary of the order + summary: string; + + // whether some part of the order is refundable, + // that is the refund deadline has not yet expired + // and the total amount refunded so far is below + // the value of the original transaction. + refundable: boolean; + + // whether the order has been paid or not + paid: boolean; + } + + interface PostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Order; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. If it's not set, refunds will not be + // possible. + refund_delay?: RelativeTime; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // specifies that some products are to be included in the + // order from the inventory. For these inventory management + // is performed (so the products must be in stock) and + // details are completed from the product data of the backend. + inventory_products?: MinimalInventoryProduct[]; + + // Specifies a lock identifier that was used to + // lock a product in the inventory. Only useful if + // manage_inventory is set. Used in case a frontend + // reserved quantities of the individual products while + // the shopping card was being built. Multiple UUIDs can + // be used in case different UUIDs were used for different + // products (i.e. in case the user started with multiple + // shopping sessions that were combined during checkout). + lock_uuids?: UUID[]; + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; + + // OTP device ID to associate with the order. + // This parameter is optional. + otp_id?: string; + } + type Order = MinimalOrderDetail | ContractTerms; + + interface MinimalOrderDetail { + // Amount to be paid by the customer + amount: Amount; + + // Short summary of the order + summary: string; + + // URL that will show that the order was successful after + // it has been paid for. Optional. When POSTing to the + // merchant, the placeholder "${ORDER_ID}" will be + // replaced with the actual order ID (useful if the + // order ID is generated server-side and needs to be + // in the URL). + fulfillment_url?: string; + } + + interface MinimalInventoryProduct { + // Which product is requested (here mandatory!) + product_id: string; + + // How many units of the product are requested + quantity: Integer; + } + interface PostOrderResponse { + // Order ID of the response that was just created + order_id: string; + + // Token that authorizes the wallet to claim the order. + // Provided only if "create_token" was set to 'true' + // in the request. + token?: ClaimToken; + } + interface OutOfStockResponse { + // Product ID of an out-of-stock item + product_id: string; + + // Requested quantity + requested_quantity: Integer; + + // Available quantity (must be below requested_quanitity) + available_quantity: Integer; + + // When do we expect the product to be again in stock? + // Optional, not given if unknown. + restock_expected?: Timestamp; + } + + interface ForgetRequest { + // Array of valid JSON paths to forgettable fields in the order's + // contract terms. + fields: string[]; + } + interface RefundRequest { + // Amount to be refunded + refund: Amount; + + // Human-readable refund justification + reason: string; + } + interface MerchantRefundResponse { + // URL (handled by the backend) that the wallet should access to + // trigger refund processing. + // taler://refund/... + taler_refund_uri: string; + + // Contract hash that a client may need to authenticate an + // HTTP request to obtain the above URI in a wallet-friendly way. + h_contract: HashCode; + } + } + + namespace Rewards { + // GET /private/reserves + interface RewardReserveStatus { + // Array of all known reserves (possibly empty!) + reserves: ReserveStatusEntry[]; + } + interface ReserveStatusEntry { + // Public key of the reserve + reserve_pub: EddsaPublicKey; + + // Timestamp when it was established + creation_time: Timestamp; + + // Timestamp when it expires + expiration_time: Timestamp; + + // Initial amount as per reserve creation call + merchant_initial_amount: Amount; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: Amount; + + // Amount picked up so far. + pickup_amount: Amount; + + // Amount approved for rewards that exceeds the pickup_amount. + committed_amount: Amount; + + // Is this reserve active (false if it was deleted but not purged) + active: boolean; + } + + interface ReserveCreateRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: Amount; + + // Exchange the merchant intends to use for reward + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; + } + interface ReserveCreateConfirmation { + // Public key identifying the reserve + reserve_pub: EddsaPublicKey; + + // Wire accounts of the exchange where to transfer the funds. + accounts: WireAccount[]; + } + interface RewardCreateRequest { + // Amount that the customer should be reward + amount: Amount; + + // Justification for giving the reward + justification: string; + + // URL that the user should be directed to after rewarding, + // will be included in the reward_token. + next_url: string; + } + interface RewardCreateConfirmation { + // Unique reward identifier for the reward that was created. + reward_id: HashCode; + + // taler://reward URI for the reward + taler_reward_uri: string; + + // URL that will directly trigger processing + // the reward when the browser is redirected to it + reward_status_url: string; + + // when does the reward expire + reward_expiration: Timestamp; + } + + interface ReserveDetail { + // Timestamp when it was established. + creation_time: Timestamp; + + // Timestamp when it expires. + expiration_time: Timestamp; + + // Initial amount as per reserve creation call. + merchant_initial_amount: Amount; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: Amount; + + // Amount picked up so far. + pickup_amount: Amount; + + // Amount approved for rewards that exceeds the pickup_amount. + committed_amount: Amount; + + // Array of all rewards created by this reserves (possibly empty!). + // Only present if asked for explicitly. + rewards?: RewardStatusEntry[]; + + // Is this reserve active (false if it was deleted but not purged)? + active: boolean; + + // Array of wire accounts of the exchange that could + // be used to fill the reserve, can be NULL + // if the reserve is inactive or was already filled + accounts?: WireAccount[]; + + // URL of the exchange hosting the reserve, + // NULL if the reserve is inactive + exchange_url: string; + } + + interface RewardStatusEntry { + // Unique identifier for the reward. + reward_id: HashCode; + + // Total amount of the reward that can be withdrawn. + total_amount: Amount; + + // Human-readable reason for why the reward was granted. + reason: string; + } + + interface RewardDetails { + // Amount that we authorized for this reward. + total_authorized: Amount; + + // Amount that was picked up by the user already. + total_picked_up: Amount; + + // Human-readable reason given when authorizing the reward. + reason: string; + + // Timestamp indicating when the reward is set to expire (may be in the past). + expiration: Timestamp; + + // Reserve public key from which the reward is funded. + reserve_pub: EddsaPublicKey; + + // Array showing the pickup operations of the wallet (possibly empty!). + // Only present if asked for explicitly. + pickups?: PickupDetail[]; + } + interface PickupDetail { + // Unique identifier for the pickup operation. + pickup_id: HashCode; + + // Number of planchets involved. + num_planchets: Integer; + + // Total amount requested for this pickup_id. + requested_amount: Amount; + } + } + + namespace Transfers { + interface TransferList { + // list of all the transfers that fit the filter that we know + transfers: TransferDetails[]; + } + interface TransferDetails { + // how much was wired to the merchant (minus fees) + credit_amount: Amount; + + // raw wire transfer identifier identifying the wire transfer (a base32-encoded value) + wtid: string; + + // target account that received the wire transfer + payto_uri: string; + + // base URL of the exchange that made the wire transfer + exchange_url: string; + + // Serial number identifying the transfer in the merchant backend. + // Used for filgering via offset. + transfer_serial_id: number; + + // Time of the execution of the wire transfer by the exchange, according to the exchange + // Only provided if we did get an answer from the exchange. + execution_time?: Timestamp; + + // True if we checked the exchange's answer and are happy with it. + // False if we have an answer and are unhappy, missing if we + // do not have an answer from the exchange. + verified?: boolean; + + // True if the merchant uses the POST /transfers API to confirm + // that this wire transfer took place (and it is thus not + // something merely claimed by the exchange). + confirmed?: boolean; + } + + interface TransferInformation { + // how much was wired to the merchant (minus fees) + credit_amount: Amount; + + // raw wire transfer identifier identifying the wire transfer (a base32-encoded value) + wtid: WireTransferIdentifierRawP; + + // target account that received the wire transfer + payto_uri: string; + + // base URL of the exchange that made the wire transfer + exchange_url: string; + } + } + + namespace OTP { + interface OtpDeviceAddDetails { + // Device ID to use. + otp_device_id: string; + + // Human-readable description for the device. + otp_device_description: string; + + // A base64-encoded key + otp_key: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDevicePatchDetails { + // Human-readable description for the device. + otp_device_description: string; + + // A base64-encoded key + otp_key: string | undefined; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + interface OtpDeviceSummaryResponse { + // Array of devices that are present in our backend. + otp_devices: OtpDeviceEntry[]; + } + interface OtpDeviceEntry { + // Device identifier. + otp_device_id: string; + + // Human-readable description for the device. + device_description: string; + } + + interface OtpDeviceDetails { + // Human-readable description for the device. + device_description: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: Integer; + + // Counter for counter-based OTP devices. + otp_ctr?: Integer; + } + + + } + namespace Template { + interface TemplateAddDetails { + // Template ID to use. + template_id: string; + + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + interface TemplateContractDetails { + // Human-readable summary for the template. + summary?: string; + + // The price is imposed by the merchant and cannot be changed by the customer. + // This parameter is optional. + amount?: Amount; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age: Integer; + + // The time the customer need to pay before his order will be deleted. + // It is deleted if the customer did not pay and if the duration is over. + pay_duration: RelativeTime; + } + interface TemplatePatchDetails { + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + + interface TemplateSummaryResponse { + // List of templates that are present in our backend. + templates: TemplateEntry[]; + } + + interface TemplateEntry { + // Template identifier, as found in the template. + template_id: string; + + // Human-readable description for the template. + template_description: string; + } + + interface TemplateDetails { + // Human-readable description for the template. + template_description: string; + + // OTP device ID. + // This parameter is optional. + otp_id?: string; + + // Additional information in a separate template. + template_contract: TemplateContractDetails; + } + + interface UsingTemplateDetails { + // Subject of the template + summary?: string; + + // The amount entered by the customer. + amount?: Amount; + } + + interface UsingTemplateResponse { + // After enter the request. The user will be pay with a taler URL. + order_id: string; + token: string; + } + } + + namespace Webhooks { + type MerchantWebhookType = "pay" | "refund"; + interface WebhookAddDetails { + // Webhook ID to use. + webhook_id: string; + + // The event of the webhook: why the webhook is used. + event_type: MerchantWebhookType; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + interface WebhookPatchDetails { + // The event of the webhook: why the webhook is used. + event_type: string; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + interface WebhookSummaryResponse { + // List of webhooks that are present in our backend. + webhooks: WebhookEntry[]; + } + interface WebhookEntry { + // Webhook identifier, as found in the webhook. + webhook_id: string; + + // The event of the webhook: why the webhook is used. + event_type: string; + } + interface WebhookDetails { + // The event of the webhook: why the webhook is used. + event_type: string; + + // URL of the webhook where the customer will be redirected. + url: string; + + // Method used by the webhook + http_method: string; + + // Header template of the webhook + header_template?: string; + + // Body template by the webhook + body_template?: string; + } + } + + namespace TokenFamilies { + // Kind of the token family. + type TokenFamilyKind = "discount" | "subscription"; + + // POST /private/tokenfamilies + interface TokenFamilyAddDetail { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n?: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + // If not specified, merchant backend will use the current time. + valid_after?: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + + // Kind of the token family. + kind: TokenFamilyKind; + } + + // PATCH /private/tokenfamilies/$SLUG + interface TokenFamilyPatchDetail { + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + } + + // GET /private/tokenfamilies + interface TokenFamilySummaryResponse { + // All configured token families of this instance. + token_families: TokenFamilyEntry[]; + } + + interface TokenFamilyEntry { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Kind of the token family. + kind: TokenFamilyKind; + } + + // GET /private/tokenfamilies/$SLUG + interface TokenFamilyDetail { + // Identifier for the token family consisting of unreserved characters + // according to RFC 3986. + slug: string; + + // Human-readable name for the token family. + name: string; + + // Human-readable description for the token family. + description: string; + + // Optional map from IETF BCP 47 language tags to localized descriptions. + description_i18n?: { [lang_tag: string]: string }; + + // Start time of the token family's validity period. + valid_after: Timestamp; + + // End time of the token family's validity period. + valid_before: Timestamp; + + // Validity duration of an issued token. + duration: RelativeTime; + + // Kind of the token family. + kind: TokenFamilyKind; + + // How many tokens have been issued for this family. + issued: Integer; + + // How many tokens have been redeemed for this family. + redeemed: Integer; + } + + } + + interface ContractTerms { + // Human-readable description of the whole purchase + summary: string; + + // Map from IETF BCP 47 language tags to localized summaries + summary_i18n?: { [lang_tag: string]: string }; + + // Unique, free-form identifier for the proposal. + // Must be unique within a merchant instance. + // For merchants that do not store proposals in their DB + // before the customer paid for them, the order_id can be used + // by the frontend to restore a proposal from the information + // encoded in it (such as a short product identifier and timestamp). + order_id: string; + + // Total price for the transaction. + // The exchange will subtract deposit fees from that amount + // before transferring it to the merchant. + amount: Amount; + + // The URL for this purchase. Every time is is visited, the merchant + // will send back to the customer the same proposal. Clearly, this URL + // can be bookmarked and shared by users. + fulfillment_url?: string; + + // Maximum total deposit fee accepted by the merchant for this contract + max_fee: Amount; + + // List of products that are part of the purchase (see Product). + products: Product[]; + + // Time when this contract was generated + timestamp: TalerProtocolTimestamp; + + // After this deadline has passed, no refunds will be accepted. + refund_deadline: TalerProtocolTimestamp; + + // After this deadline, the merchant won't accept payments for the contact + pay_deadline: TalerProtocolTimestamp; + + // Transfer deadline for the exchange. Must be in the + // deposit permissions of coins used to pay for this order. + wire_transfer_deadline: TalerProtocolTimestamp; + + // Merchant's public key used to sign this proposal; this information + // is typically added by the backend Note that this can be an ephemeral key. + merchant_pub: EddsaPublicKey; + + // Base URL of the (public!) merchant backend API. + // Must be an absolute URL that ends with a slash. + merchant_base_url: string; + + // More info about the merchant, see below + merchant: Merchant; + + // The hash of the merchant instance's wire details. + h_wire: HashCode; + + // Wire transfer method identifier for the wire method associated with h_wire. + // The wallet may only select exchanges via a matching auditor if the + // exchange also supports this wire method. + // The wire transfer fees must be added based on this wire transfer method. + wire_method: string; + + // Any exchanges audited by these auditors are accepted by the merchant. + auditors: Auditor[]; + + // Exchanges that the merchant accepts even if it does not accept any auditors that audit them. + exchanges: Exchange[]; + + // Delivery location for (all!) products. + delivery_location?: Location; + + // Time indicating when the order should be delivered. + // May be overwritten by individual products. + delivery_date?: TalerProtocolTimestamp; + + // Nonce generated by the wallet and echoed by the merchant + // in this field when the proposal is generated. + nonce: string; + + // Specifies for how long the wallet should try to get an + // automatic refund for the purchase. If this field is + // present, the wallet should wait for a few seconds after + // the purchase and then automatically attempt to obtain + // a refund. The wallet should probe until "delay" + // after the payment was successful (i.e. via long polling + // or via explicit requests with exponential back-off). + // + // In particular, if the wallet is offline + // at that time, it MUST repeat the request until it gets + // one response from the merchant after the delay has expired. + // If the refund is granted, the wallet MUST automatically + // recover the payment. This is used in case a merchant + // knows that it might be unable to satisfy the contract and + // desires for the wallet to attempt to get the refund without any + // customer interaction. Note that it is NOT an error if the + // merchant does not grant a refund. + auto_refund?: RelativeTime; + + // Extra data that is only interpreted by the merchant frontend. + // Useful when the merchant needs to store extra information on a + // contract without storing it separately in their database. + extra?: any; + + // Minimum age buyer must have (in years). Default is 0. + minimum_age?: Integer; + } +} diff --git a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts new file mode 100644 index 000000000..62f364972 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts @@ -0,0 +1,78 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ +import { MerchantBackend } from "../declaration.js"; +import { useSessionContext } from "../context/session.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +const useSWR = _useSWR as unknown as SWRHook; + +export function useInstanceTokenFamilies() { + const { state: session, lib: { instance } } = useSessionContext(); + + // const [offset, setOffset] = useState<number | undefined>(); + + async function fetcher([token, bid]: [AccessToken, number]) { + return await instance.listTokenFamilies(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid === undefined ? undefined: String(bid), + // order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listTokenFamilies">, + TalerHttpError + >([session.token, "offset", "listTokenFamilies"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return data; +} + +export function useTokenFamilyDetails(tokenFamilySlug: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + async function fetcher([slug, token]: [string, AccessToken]) { + return await instance.getTokenFamilyDetails(token, slug); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getTokenFamilyDetails">, + TalerHttpError + >([tokenFamilySlug, session.token, "getTokenFamilyDetails"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return data; +} + +export interface TokenFamilyAPI { + createTokenFamily: ( + data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail, + ) => Promise<void>; + updateTokenFamily: ( + slug: string, + data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + ) => Promise<void>; + deleteTokenFamily: (slug: string) => Promise<void>; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx new file mode 100644 index 000000000..d9ac4202c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/TokenFamily/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + onBack: { action: "onBack" }, + }, +}; + +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-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx new file mode 100644 index 000000000..cec1f3426 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { useListener } from "../../../../hooks/listener.js"; +import { TokenFamilyForm } from "../../../../components/tokenfamily/TokenFamilyForm.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; + +type Entity = TalerMerchantApi.TokenFamilyCreateRequest; + +interface Props { + onCreate: (d: Entity) => Promise<void>; + onBack?: () => void; +} + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( + (result) => { + if (result) return onCreate(result); + return Promise.reject(); + }, + ); + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <TokenFamilyForm onSubscribe={addFormSubmitter} /> + + {/* <Test /> */} + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submitForm} + data-tooltip={ + !submitForm + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + disabled={!submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx new file mode 100644 index 000000000..deee7d0d5 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { Notification } from "../../../../utils/types.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { CreatePage } from "./CreatePage.js"; + +export type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail; +interface Props { + onBack?: () => void; + onConfirm: () => void; +} +export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request) => { + return lib.instance.createTokenFamily(state.token, request) + .then(() => onConfirm()) + .catch((error) => { + setNotif({ + message: i18n.str`could not create token family`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx new file mode 100644 index 000000000..b5ca03cfd --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx @@ -0,0 +1,245 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { format } from "date-fns"; +import { MerchantBackend } from "../../../../declaration.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; + +type Entity = TalerMerchantApi.TokenFamilySummary; + +interface Props { + instances: Entity[]; + onDelete: (tokenFamily: Entity) => void; + onSelect: (tokenFamily: Entity) => void; + onUpdate: ( + slug: string, + data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + ) => Promise<void>; + onCreate: () => void; + selected?: boolean; +} + +export function CardTable({ + instances, + onCreate, + onSelect, + onUpdate, + onDelete, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string | undefined>( + undefined, + ); + const { i18n } = useTranslationContext(); + return ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-shopping" /> + </span> + <i18n.Translate>Token Families</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`add token family`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? ( + <Table + instances={instances} + onSelect={onSelect} + onDelete={onDelete} + onUpdate={onUpdate} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ); +} +interface TableProps { + rowSelection: string | undefined; + instances: Entity[]; + onSelect: (tokenFamily: Entity) => void; + onUpdate: ( + slug: string, + data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + ) => Promise<void>; + onDelete: (tokenFamily: Entity) => void; + rowSelectionHandler: StateUpdater<string | undefined>; +} + +function Table({ + rowSelection, + rowSelectionHandler, + instances, + onSelect, + onUpdate, + onDelete, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Slug</i18n.Translate> + </th> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th> + <i18n.Translate>Valid After</i18n.Translate> + </th> + <th> + <i18n.Translate>Valid Before</i18n.Translate> + </th> + <th> + <i18n.Translate>Kind</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <Fragment key={i.slug}> + <tr key="info"> + <td + onClick={() => + rowSelection !== i.slug && rowSelectionHandler(i.slug) + } + style={{ cursor: "pointer" }} + > + {i.slug} + </td> + <td + onClick={() => + rowSelection !== i.slug && rowSelectionHandler(i.slug) + } + style={{ cursor: "pointer" }} + > + {i.name} + </td> + <td + onClick={() => + rowSelection !== i.slug && rowSelectionHandler(i.slug) + } + style={{ cursor: "pointer" }} + > + {i.valid_after.t_s === "never" + ? "never" + : format(new Date(i.valid_after.t_s * 1000), "yyyy/MM/dd hh:mm:ss")} + </td> + <td + onClick={() => + rowSelection !== i.slug && rowSelectionHandler(i.slug) + } + style={{ cursor: "pointer" }} + > + {i.valid_before.t_s === "never" + ? "never" + : format(new Date(i.valid_before.t_s * 1000), "yyyy/MM/dd hh:mm:ss")} + </td> + <td + onClick={() => + rowSelection !== i.slug && rowSelectionHandler(i.slug) + } + style={{ cursor: "pointer" }} + > + {i.kind} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <span + class="has-tooltip-bottom" + data-tooltip={i18n.str`go to token family update page`} + > + <button + class="button is-small is-success " + type="button" + onClick={(): void => onSelect(i)} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </span> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`remove this token family from the database`} + > + <button + class="button is-small is-danger" + type="button" + onClick={(): void => onDelete(i)} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + </span> + </div> + </td> + </tr> + </Fragment> + ); + })} + </tbody> + </table> + </div> + ); +} + + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + 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> + <i18n.Translate> + There are no token families yet, add the first one by pressing the + sign. + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx new file mode 100644 index 000000000..006c2a49c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx @@ -0,0 +1,143 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { MerchantBackend } from "../../../../declaration.js"; +import { + useInstanceTokenFamilies, +} from "../../../../hooks/tokenfamily.js"; +import { Notification } from "../../../../utils/types.js"; +import { CardTable } from "./Table.js"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../../../context/session.js"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; + +interface Props { + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onCreate: () => void; + onSelect: (slug: string) => void; + onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; +} +export default function TokenFamilyList({ + onCreate, + onSelect, +}: Props): VNode { + const result = useInstanceTokenFamilies(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib, state } = useSessionContext(); + const [deleting, setDeleting] = + useState<TalerMerchantApi.TokenFamilySummary | null>(null); + + const { i18n } = useTranslationContext(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + <CardTable + instances={result.body.token_families} + onCreate={onCreate} + onUpdate={async (slug, fam) => { + try { + await lib.instance.updateTokenFamily(state.token, slug, fam); + setNotif({ + message: i18n.str`token family updated successfully`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`could not update the token family`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + return; + }} + onSelect={(tokenFamily) => onSelect(tokenFamily.slug)} + onDelete={(fam) => setDeleting(fam)} + /> + + {deleting && ( + <ConfirmModal + label={`Delete token family`} + description={`Delete the token family "${deleting.name}"`} + danger + active + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await lib.instance.deleteTokenFamily(state.token, deleting.slug); + setNotif({ + message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`Failed to delete token family`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setDeleting(null); + }} + > + <p> + If you delete the <b>"{deleting.name}"</b> token family (Slug:{" "} + <b>{deleting.slug}</b>), all issued tokens will become invalid. + </p> + <p class="warning"> + Deleting a token family <b>cannot be undone</b>. + </p> + </ConfirmModal> + )} + </section> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx new file mode 100644 index 000000000..184e37b9c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -0,0 +1,173 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 Christian Blättler + */ + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h } from "preact"; +import { useState } from "preact/hooks"; +import * as yup from "yup"; +import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { TokenFamilyUpdateSchema } from "../../../../schemas/index.js"; +import { FormErrors, FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputDate } from "../../../../components/form/InputDate.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; +import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util"; + +type Entity = Omit<TalerMerchantApi.TokenFamilyUpdateRequest, "duration"> & { + duration: Duration, +}; + +interface Props { + onUpdate: (d: TalerMerchantApi.TokenFamilyUpdateRequest) => Promise<void>; + onBack?: () => void; + tokenFamily: TalerMerchantApi.TokenFamilyUpdateRequest; +} + +function convert(from: TalerMerchantApi.TokenFamilyUpdateRequest) { + const { duration, ...rest } = from; + + const converted = { + duration: Duration.fromTalerProtocolDuration(duration), + }; + return { ...converted, ...rest }; +} + +export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { + const [value, valueHandler] = useState<Partial<Entity>>(convert(tokenFamily)); + let errors: FormErrors<Entity> = {}; + + try { + TokenFamilyUpdateSchema.validateSync(value, { + abortEarly: false, + }); + } catch (err) { + if (err instanceof yup.ValidationError) { + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); + } + } + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submitForm = () => { + if (hasErrors) return Promise.reject(); + + const { duration, ...rest } = value as Required<Entity>; + const result: TalerMerchantApi.TokenFamilyUpdateRequest = { + ...rest, + duration: Duration.toTalerProtocolDuration(duration), + }; + + return onUpdate(result); + } + + const { i18n } = useTranslationContext(); + + return ( + <div> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + Token Family: <b>{tokenFamily.name}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider<Entity> + name="token_family" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <Input<Entity> + name="name" + inputType="text" + label={i18n.str`Name`} + tooltip={i18n.str`user-readable token family name`} + /> + <Input<Entity> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`token family description for customers`} + /> + <InputDate<Entity> + name="valid_after" + label={i18n.str`Valid After`} + tooltip={i18n.str`token family can issue tokens after this date`} + withTimestampSupport + /> + <InputDate<Entity> + name="valid_before" + label={i18n.str`Valid Before`} + tooltip={i18n.str`token family can issue tokens until this date`} + withTimestampSupport + /> + <InputDuration<Entity> + name="duration" + label={i18n.str`Duration`} + tooltip={i18n.str`validity duration of a issued token`} + withForever + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx new file mode 100644 index 000000000..068235e14 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx @@ -0,0 +1,105 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 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 Christian Blättler + */ + +import { + ErrorType, + HttpError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../../components/exception/loading.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; +import { Notification } from "../../../../utils/types.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { useTokenFamilyDetails } from "../../../../hooks/tokenfamily.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; + +type Entity = TalerMerchantApi.TokenFamilyUpdateRequest; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + slug: string; +} +export default function UpdateTokenFamily({ + slug, + onConfirm, + onBack, +}: Props): VNode { + const result = useTokenFamilyDetails(slug); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib, state } = useSessionContext(); + + const { i18n } = useTranslationContext(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } + } + + const family: Entity = { + name: result.body.name, + description: result.body.description, + description_i18n: result.body.description_i18n || {}, + duration: result.body.duration, + valid_after: result.body.valid_after, + valid_before: result.body.valid_before, + }; + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + tokenFamily={family} + onBack={onBack} + onUpdate={(data) => { + return lib.instance.updateTokenFamily(state.token, slug, data) + .then(onConfirm) + .catch((error) => { + setNotif({ + message: i18n.str`could not update token family`, + type: "ERROR", + description: error.message, + }); + }); + }} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts index 693894ae0..43384299b 100644 --- a/packages/merchant-backoffice-ui/src/schemas/index.ts +++ b/packages/merchant-backoffice-ui/src/schemas/index.ts @@ -23,6 +23,9 @@ import { Amounts } from "@gnu-taler/taler-util"; import { isAfter, isFuture } from "date-fns"; import * as yup from "yup"; import { PAYTO_REGEX } from "../utils/constants.js"; +import { Amounts } from "@gnu-taler/taler-util"; +import { MerchantBackend } from "../declaration.js"; +// import { MerchantBackend } from "../declaration.js"; yup.setLocale({ mixed: { @@ -222,3 +225,55 @@ export const NonInventoryProductSchema = yup.object().shape({ .required() .test("amount", "the amount is not valid", currencyWithAmountIsValid), }); + +const timestampSchema = yup.object().shape({ + t_s: yup.mixed().test( + 'is-timestamp', + 'Invalid timestamp', + value => typeof value === 'number' || value === 'never' + ) +}).required(); + +const durationSchema = yup.object().shape({ + d_us: yup.mixed().test( + 'is-duration', + 'Invalid duration', + value => typeof value === 'number' || value === 'forever' + ) +}).required(); + +const tokenFamilyKindSchema = yup.mixed().oneOf<MerchantBackend.TokenFamilies.TokenFamilyKind>(["discount", "subscription"]).required(); + +export const TokenFamilyCreateSchema = yup.object().shape({ + slug: yup.string().ensure().required(), + name: yup.string().required(), + description: yup.string().required(), + // description_i18n: yup.lazy((obj) => + // yup.object().shape( + // Object.keys(obj || {}).reduce((acc, key) => { + // acc[key] = yup.string().required(); + // return acc; + // }, {}) + // ) + // ).optional(), + valid_after: timestampSchema.optional(), + valid_before: timestampSchema, + duration: durationSchema, + kind: tokenFamilyKindSchema, +}); + +export const TokenFamilyUpdateSchema = yup.object().shape({ + name: yup.string().required(), + description: yup.string().required(), + // description_i18n: yup.lazy((obj) => + // yup.object().shape( + // Object.keys(obj).reduce((acc, key) => { + // acc[key] = yup.string().required(); + // return acc; + // }, {}) + // ) + // ), + valid_after: timestampSchema, + valid_before: timestampSchema, + duration: durationSchema, +}); diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index 45a12c258..77bdf575a 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -188,7 +188,7 @@ export class HttpLibImpl implements HttpRequestLibrary { ); } - let timeoutHandle: NodeJS.Timer | undefined = undefined; + let timeoutHandle: NodeJS.Timeout | undefined = undefined; let cancelCancelledHandler: (() => void) | undefined = undefined; const doCleanup = () => { diff --git a/packages/taler-util/src/url.ts b/packages/taler-util/src/url.ts index 149997f3f..1b5626626 100644 --- a/packages/taler-util/src/url.ts +++ b/packages/taler-util/src/url.ts @@ -94,7 +94,7 @@ if (useOwnUrlImp || !_URL) { _URL = URLImpl; } -export const URL: URLCtor = _URL; +export const URL = _URL; // @ts-ignore let _URLSearchParams = globalThis.URLSearchParams; |