diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/update')
3 files changed, 433 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx new file mode 100644 index 000000000..3239d9c5c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode, FunctionalComponent } from "preact"; +import { UpdatePage as TestedComponent } from "./UpdatePage"; + +export default { + title: "Pages/Instance/Update", + component: TestedComponent, + argTypes: { + onUpdate: { action: "onUpdate" }, + 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, { + selected: { + accounts: [], + name: "name", + auth: { method: "external" }, + address: {}, + jurisdiction: {}, + default_max_deposit_fee: "TESTKUDOS:2", + default_max_wire_fee: "TESTKUDOS:1", + default_pay_delay: { + d_us: 1000000, + }, + default_wire_fee_amortization: 1, + default_wire_transfer_delay: { + d_us: 100000, + }, + merchant_pub: "ASDWQEKASJDKSADJ", + }, +}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx new file mode 100644 index 000000000..4c7a51121 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -0,0 +1,259 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import * as yup from "yup"; +import { AsyncButton } from "../../../components/exception/AsyncButton"; +import { + FormProvider, + FormErrors, +} from "../../../components/form/FormProvider"; +import { UpdateTokenModal } from "../../../components/modal"; +import { useInstanceContext } from "../../../context/instance"; +import { MerchantBackend } from "../../../declaration"; +import { Translate, useTranslator } from "../../../i18n"; +import { InstanceUpdateSchema as schema } from "../../../schemas"; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields"; +import { PAYTO_REGEX } from "../../../utils/constants"; +import { Amounts } from "@gnu-taler/taler-util"; + +type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { + auth_token?: string; +}; + +//MerchantBackend.Instances.InstanceAuthConfigurationMessage +interface Props { + onUpdate: (d: Entity) => void; + onChangeAuth: ( + d: MerchantBackend.Instances.InstanceAuthConfigurationMessage + ) => Promise<void>; + selected: MerchantBackend.Instances.QueryInstancesResponse; + isLoading: boolean; + onBack: () => void; +} + +function convert( + from: MerchantBackend.Instances.QueryInstancesResponse +): Entity { + const { accounts, ...rest } = from; + const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri); + const defaults = { + default_wire_fee_amortization: 1, + default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours + default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours + }; + return { ...defaults, ...rest, payto_uris }; +} + +function getTokenValuePart(t?: string): string | undefined { + if (!t) return t; + const match = /secret-token:(.*)/.exec(t); + if (!match || !match[1]) return undefined; + return match[1]; +} + +function undefinedIfEmpty<T>(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) + ? obj + : undefined; +} + +export function UpdatePage({ + onUpdate, + onChangeAuth, + selected, + onBack, +}: Props): VNode { + const { id, token } = useInstanceContext(); + const currentTokenValue = getTokenValuePart(token); + + function updateToken(token: string | undefined | null) { + const value = + token && token.startsWith("secret-token:") + ? token.substring("secret-token:".length) + : token; + + if (!token) { + onChangeAuth({ method: "external" }); + } else { + onChangeAuth({ method: "token", token: `secret-token:${value}` }); + } + } + + const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); + + const i18n = useTranslator(); + + const errors: FormErrors<Entity> = { + name: !value.name ? i18n`required` : undefined, + payto_uris: + !value.payto_uris || !value.payto_uris.length + ? i18n`required` + : undefinedIfEmpty( + value.payto_uris.map((p) => { + return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined; + }) + ), + default_max_deposit_fee: !value.default_max_deposit_fee + ? i18n`required` + : !Amounts.parse(value.default_max_deposit_fee) + ? i18n`invalid format` + : undefined, + default_max_wire_fee: !value.default_max_wire_fee + ? i18n`required` + : !Amounts.parse(value.default_max_wire_fee) + ? i18n`invalid format` + : undefined, + default_wire_fee_amortization: + value.default_wire_fee_amortization === undefined + ? i18n`required` + : isNaN(value.default_wire_fee_amortization) + ? i18n`is not a number` + : value.default_wire_fee_amortization < 1 + ? i18n`must be 1 or greater` + : undefined, + default_pay_delay: !value.default_pay_delay ? i18n`required` : undefined, + default_wire_transfer_delay: !value.default_wire_transfer_delay + ? i18n`required` + : undefined, + address: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n`max 7 lines` + : undefined, + }), + jurisdiction: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n`max 7 lines` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined + ); + const submit = async (): Promise<void> => { + await onUpdate(value as Entity); + }; + const [active, setActive] = useState(false); + + 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"> + <Translate>Instance id</Translate>: <b>{id}</b> + </span> + </div> + </div> + <div class="level-right"> + <div class="level-item"> + <h1 class="title"> + <button + class="button is-danger" + data-tooltip={i18n`Change the authorization method use for this instance.`} + onClick={(): void => { + setActive(!active); + }} + > + <div class="icon is-left"> + <i class="mdi mdi-lock-reset" /> + </div> + <span> + <Translate>Manage access token</Translate> + </span> + </button> + </h1> + </div> + </div> + </div> + </div> + </section> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + {active && ( + <UpdateTokenModal + oldToken={currentTokenValue} + onCancel={() => { + setActive(false); + }} + onClear={() => { + updateToken(null); + setActive(false); + }} + onConfirm={(newToken) => { + updateToken(newToken); + setActive(false); + }} + /> + )} + </div> + <div class="column" /> + </div> + <hr /> + + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider<Entity> + errors={errors} + object={value} + valueHandler={valueHandler} + > + <DefaultInstanceFormFields showId={false} /> + </FormProvider> + + <div class="buttons is-right mt-4"> + <button + class="button" + onClick={onBack} + data-tooltip="cancel operation" + > + <Translate>Cancel</Translate> + </button> + + <AsyncButton + onClick={submit} + data-tooltip={ + hasErrors + ? i18n`Need to complete marked fields` + : "confirm operation" + } + disabled={hasErrors} + > + <Translate>Confirm</Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx new file mode 100644 index 000000000..bd5f4c727 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -0,0 +1,113 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { NotificationCard } from "../../../components/menu"; +import { useInstanceContext } from "../../../context/instance"; +import { MerchantBackend } from "../../../declaration"; +import { HttpError, HttpResponse } from "../../../hooks/backend"; +import { + useInstanceAPI, + useInstanceDetails, + useManagedInstanceDetails, + useManagementAPI, +} from "../../../hooks/instance"; +import { useTranslator } from "../../../i18n"; +import { Notification } from "../../../utils/types"; +import { UpdatePage } from "./UpdatePage"; + +export interface Props { + onBack: () => void; + onConfirm: () => void; + + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: HttpError) => VNode; + onUpdateError: (e: HttpError) => void; +} + +export default function Update(props: Props): VNode { + const { updateInstance, clearToken, setNewToken } = useInstanceAPI(); + const result = useInstanceDetails(); + return CommonUpdate(props, result, updateInstance, clearToken, setNewToken); +} + +export function AdminUpdate(props: Props & { instanceId: string }): VNode { + const { updateInstance, clearToken, setNewToken } = useManagementAPI( + props.instanceId + ); + const result = useManagedInstanceDetails(props.instanceId); + return CommonUpdate(props, result, updateInstance, clearToken, setNewToken); +} + +function CommonUpdate( + { + onBack, + onConfirm, + onLoadError, + onNotFound, + onUpdateError, + onUnauthorized, + }: Props, + result: HttpResponse<MerchantBackend.Instances.QueryInstancesResponse>, + updateInstance: any, + clearToken: any, + setNewToken: any +): VNode { + const { changeToken } = useInstanceContext(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const i18n = useTranslator(); + + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); + + return ( + <Fragment> + <NotificationCard notification={notif} /> + <UpdatePage + onBack={onBack} + isLoading={false} + selected={result.data} + onUpdate={( + d: MerchantBackend.Instances.InstanceReconfigurationMessage + ): Promise<void> => { + return updateInstance(d) + .then(onConfirm) + .catch((error: Error) => + setNotif({ + message: i18n`Failed to create instance`, + type: "ERROR", + description: error.message, + }) + ); + }} + onChangeAuth={( + d: MerchantBackend.Instances.InstanceAuthConfigurationMessage + ): Promise<void> => { + const apiCall = + d.method === "external" ? clearToken() : setNewToken(d.token!); + return apiCall + .then(() => changeToken(d.token)) + .then(onConfirm) + .catch(onUpdateError); + }} + /> + </Fragment> + ); +} |