more ui
This commit is contained in:
parent
dfd23f63ba
commit
a59df74fb2
@ -45,6 +45,20 @@ export function Routing(): VNode {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/operation/:wopid"
|
||||||
|
component={({ wopid }: { wopid: string }) => (
|
||||||
|
<WithdrawalOperationPage
|
||||||
|
operationId={wopid}
|
||||||
|
onContinue={() => {
|
||||||
|
route("/account");
|
||||||
|
}}
|
||||||
|
// onLoadNotOk={() => {
|
||||||
|
// route("/account");
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/register"
|
path="/register"
|
||||||
component={() => (
|
component={() => (
|
||||||
@ -64,10 +78,6 @@ export function Routing(): VNode {
|
|||||||
return (
|
return (
|
||||||
<BankFrame account={backend.state.username}>
|
<BankFrame account={backend.state.username}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Route
|
|
||||||
path="/test"
|
|
||||||
component={Test}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/operation/:wopid"
|
path="/operation/:wopid"
|
||||||
component={({ wopid }: { wopid: string }) => (
|
component={({ wopid }: { wopid: string }) => (
|
||||||
@ -76,9 +86,6 @@ export function Routing(): VNode {
|
|||||||
onContinue={() => {
|
onContinue={() => {
|
||||||
route("/account");
|
route("/account");
|
||||||
}}
|
}}
|
||||||
// onLoadNotOk={() => {
|
|
||||||
// route("/account");
|
|
||||||
// }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -108,9 +115,9 @@ export function Routing(): VNode {
|
|||||||
} else {
|
} else {
|
||||||
return <HomePage
|
return <HomePage
|
||||||
account={username}
|
account={username}
|
||||||
// onPendingOperationFound={(wopid) => {
|
goToConfirmOperation={(wopid) => {
|
||||||
// route(`/operation/${wopid}`);
|
route(`/operation/${wopid}`);
|
||||||
// }}
|
}}
|
||||||
goToBusinessAccount={() => {
|
goToBusinessAccount={() => {
|
||||||
route("/business");
|
route("/business");
|
||||||
}}
|
}}
|
||||||
|
@ -30,6 +30,7 @@ interface Settings {
|
|||||||
currentWithdrawalOperationId: string | undefined;
|
currentWithdrawalOperationId: string | undefined;
|
||||||
showWithdrawalSuccess: boolean;
|
showWithdrawalSuccess: boolean;
|
||||||
showDemoDescription: boolean;
|
showDemoDescription: boolean;
|
||||||
|
showInstallWallet: boolean;
|
||||||
maxWithdrawalAmount: number;
|
maxWithdrawalAmount: number;
|
||||||
fastWithdrawal: boolean;
|
fastWithdrawal: boolean;
|
||||||
}
|
}
|
||||||
@ -39,6 +40,7 @@ export const codecForSettings = (): Codec<Settings> =>
|
|||||||
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
|
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
|
||||||
.property("showWithdrawalSuccess", (codecForBoolean()))
|
.property("showWithdrawalSuccess", (codecForBoolean()))
|
||||||
.property("showDemoDescription", (codecForBoolean()))
|
.property("showDemoDescription", (codecForBoolean()))
|
||||||
|
.property("showInstallWallet", (codecForBoolean()))
|
||||||
.property("fastWithdrawal", (codecForBoolean()))
|
.property("fastWithdrawal", (codecForBoolean()))
|
||||||
.property("maxWithdrawalAmount", codecForNumber())
|
.property("maxWithdrawalAmount", codecForNumber())
|
||||||
.build("Settings");
|
.build("Settings");
|
||||||
@ -47,6 +49,7 @@ const defaultSettings: Settings = {
|
|||||||
currentWithdrawalOperationId: undefined,
|
currentWithdrawalOperationId: undefined,
|
||||||
showWithdrawalSuccess: true,
|
showWithdrawalSuccess: true,
|
||||||
showDemoDescription: true,
|
showDemoDescription: true,
|
||||||
|
showInstallWallet: true,
|
||||||
maxWithdrawalAmount: 25,
|
maxWithdrawalAmount: 25,
|
||||||
fastWithdrawal: false,
|
fastWithdrawal: false,
|
||||||
};
|
};
|
||||||
|
@ -29,6 +29,7 @@ export interface Props {
|
|||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
) => VNode;
|
) => VNode;
|
||||||
goToBusinessAccount: () => void;
|
goToBusinessAccount: () => void;
|
||||||
|
goToConfirmOperation: (id:string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
|
export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
|
||||||
@ -54,6 +55,7 @@ export namespace State {
|
|||||||
account: string,
|
account: string,
|
||||||
limit: AmountJson,
|
limit: AmountJson,
|
||||||
goToBusinessAccount: () => void;
|
goToBusinessAccount: () => void;
|
||||||
|
goToConfirmOperation: (id:string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvalidIban {
|
export interface InvalidIban {
|
||||||
|
@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js";
|
|||||||
import { useAccountDetails } from "../../hooks/access.js";
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
import { Props, State } from "./index.js";
|
import { Props, State } from "./index.js";
|
||||||
|
|
||||||
export function useComponentState({ account, goToBusinessAccount }: Props): State {
|
export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State {
|
||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -75,6 +75,7 @@ export function useComponentState({ account, goToBusinessAccount }: Props): Stat
|
|||||||
return {
|
return {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
goToBusinessAccount,
|
goToBusinessAccount,
|
||||||
|
goToConfirmOperation,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
account,
|
account,
|
||||||
limit,
|
limit,
|
||||||
|
@ -123,7 +123,7 @@ function ShowDemoInfo():VNode {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> {
|
export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
@ -131,7 +131,7 @@ export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready):
|
|||||||
|
|
||||||
<ShowDemoInfo />
|
<ShowDemoInfo />
|
||||||
|
|
||||||
<PaymentOptions limit={limit} />
|
<PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
|
||||||
<Transactions account={account} />
|
<Transactions account={account} />
|
||||||
</Fragment>;
|
</Fragment>;
|
||||||
}
|
}
|
||||||
|
@ -206,6 +206,21 @@ export function BankFrame({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mt-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="flex flex-grow flex-col">
|
||||||
|
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
|
||||||
|
<i18n.Translate>Show install wallet first</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" data-enabled={settings.showInstallWallet} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
|
||||||
|
onClick={() => {
|
||||||
|
updateSettings("showInstallWallet", !settings.showInstallWallet);
|
||||||
|
}}>
|
||||||
|
<span aria-hidden="true" data-enabled={settings.showInstallWallet} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<li class="mt-2">
|
<li class="mt-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex flex-grow flex-col">
|
<span class="flex flex-grow flex-col">
|
||||||
|
@ -36,6 +36,7 @@ import { useSettings } from "../hooks/settings.js";
|
|||||||
import { AccountPage } from "./AccountPage/index.js";
|
import { AccountPage } from "./AccountPage/index.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
||||||
|
import { route } from "preact-router";
|
||||||
|
|
||||||
const logger = new Logger("AccountPage");
|
const logger = new Logger("AccountPage");
|
||||||
|
|
||||||
@ -52,25 +53,20 @@ const logger = new Logger("AccountPage");
|
|||||||
export function HomePage({
|
export function HomePage({
|
||||||
onRegister,
|
onRegister,
|
||||||
account,
|
account,
|
||||||
// onPendingOperationFound,
|
goToConfirmOperation,
|
||||||
goToBusinessAccount,
|
goToBusinessAccount,
|
||||||
}: {
|
}: {
|
||||||
account: string,
|
account: string,
|
||||||
// onPendingOperationFound: (id: string) => void;
|
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
goToBusinessAccount: () => void;
|
goToBusinessAccount: () => void;
|
||||||
|
goToConfirmOperation: (id:string) => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const [settings] = useSettings();
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
// if (settings.currentWithdrawalOperationId) {
|
|
||||||
// onPendingOperationFound(settings.currentWithdrawalOperationId);
|
|
||||||
// return <Loading />;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountPage
|
<AccountPage
|
||||||
account={account}
|
account={account}
|
||||||
|
goToConfirmOperation={goToConfirmOperation}
|
||||||
goToBusinessAccount={goToBusinessAccount}
|
goToBusinessAccount={goToBusinessAccount}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
/>
|
/>
|
||||||
@ -108,6 +104,7 @@ export function WithdrawalOperationPage({
|
|||||||
withdrawUri={parsedUri}
|
withdrawUri={parsedUri}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
updateSettings("currentWithdrawalOperationId", undefined)
|
updateSettings("currentWithdrawalOperationId", undefined)
|
||||||
|
onContinue()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -178,7 +175,7 @@ export function handleNotOkResult(
|
|||||||
assertUnreachable(result);
|
assertUnreachable(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
route("/")
|
||||||
return <div>error</div>;
|
return <div>error</div>;
|
||||||
}
|
}
|
||||||
return <div />;
|
return <div />;
|
||||||
|
@ -26,6 +26,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js";
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
currency: string;
|
currency: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
goToConfirmOperation: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading |
|
export type State = State.Loading |
|
||||||
@ -57,26 +58,33 @@ export namespace State {
|
|||||||
error: undefined;
|
error: undefined;
|
||||||
uri: WithdrawUriResult,
|
uri: WithdrawUriResult,
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onAbort: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvalidPayto {
|
export interface InvalidPayto {
|
||||||
status: "invalid-payto",
|
status: "invalid-payto",
|
||||||
error: undefined;
|
error: undefined;
|
||||||
payto: string | null;
|
payto: string | null;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export interface InvalidWithdrawal {
|
export interface InvalidWithdrawal {
|
||||||
status: "invalid-withdrawal",
|
status: "invalid-withdrawal",
|
||||||
error: undefined;
|
error: undefined;
|
||||||
|
onClose: () => void;
|
||||||
uri: string,
|
uri: string,
|
||||||
}
|
}
|
||||||
export interface InvalidReserve {
|
export interface InvalidReserve {
|
||||||
status: "invalid-reserve",
|
status: "invalid-reserve",
|
||||||
error: undefined;
|
error: undefined;
|
||||||
|
onClose: () => void;
|
||||||
reserve: string | null;
|
reserve: string | null;
|
||||||
}
|
}
|
||||||
export interface NeedConfirmation {
|
export interface NeedConfirmation {
|
||||||
status: "need-confirmation",
|
status: "need-confirmation",
|
||||||
|
onAbort: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
error: undefined;
|
error: undefined;
|
||||||
|
busy: boolean,
|
||||||
}
|
}
|
||||||
export interface Aborted {
|
export interface Aborted {
|
||||||
status: "aborted",
|
status: "aborted",
|
||||||
@ -111,7 +119,7 @@ const viewMapping: utils.StateViewMap<State> = {
|
|||||||
ready: ReadyView,
|
ready: ReadyView,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AccountPage = utils.compose(
|
export const OperationState = utils.compose(
|
||||||
(p: Props) => useComponentState(p),
|
(p: Props) => useComponentState(p),
|
||||||
viewMapping,
|
viewMapping,
|
||||||
);
|
);
|
||||||
|
@ -15,21 +15,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
|
import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, RequestError, notify, notifyError, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
|
import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
|
||||||
import { useBackendContext } from "../../context/backend.js";
|
import { useBackendContext } from "../../context/backend.js";
|
||||||
import { useAccessAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js";
|
import { useAccessAPI, useAccessAnonAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js";
|
||||||
import { Props, State } from "./index.js";
|
import { Props, State } from "./index.js";
|
||||||
import { useSettings } from "../../hooks/settings.js";
|
import { useSettings } from "../../hooks/settings.js";
|
||||||
import { buildRequestErrorMessage } from "../../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { getInitialBackendBaseURL } from "../../hooks/backend.js";
|
import { getInitialBackendBaseURL } from "../../hooks/backend.js";
|
||||||
|
|
||||||
export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
|
export function useComponentState({ currency, onClose,goToConfirmOperation }: Props): utils.RecursiveState<State> {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const [settings, updateSettings] = useSettings()
|
const [settings, updateSettings] = useSettings()
|
||||||
const { createWithdrawal } = useAccessAPI();
|
const { createWithdrawal } = useAccessAPI();
|
||||||
|
const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
|
||||||
|
const [busy, setBusy] = useState<Record<string, undefined>>()
|
||||||
|
|
||||||
const amount = settings.maxWithdrawalAmount
|
const amount = settings.maxWithdrawalAmount
|
||||||
|
|
||||||
async function doSilentStart() {
|
async function doSilentStart() {
|
||||||
//FIXME: if amount is not enough use balance
|
//FIXME: if amount is not enough use balance
|
||||||
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
|
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
|
||||||
@ -67,12 +70,14 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const withdrawalOperationId = settings.currentWithdrawalOperationId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
doSilentStart()
|
if (withdrawalOperationId === undefined) {
|
||||||
|
doSilentStart()
|
||||||
|
}
|
||||||
}, [settings.fastWithdrawal, amount])
|
}, [settings.fastWithdrawal, amount])
|
||||||
|
|
||||||
const baseUrl = getInitialBackendBaseURL()
|
const baseUrl = getInitialBackendBaseURL()
|
||||||
const withdrawalOperationId = settings.currentWithdrawalOperationId
|
|
||||||
|
|
||||||
if (!withdrawalOperationId) {
|
if (!withdrawalOperationId) {
|
||||||
return {
|
return {
|
||||||
@ -81,6 +86,63 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wid = withdrawalOperationId
|
||||||
|
|
||||||
|
async function doAbort() {
|
||||||
|
try {
|
||||||
|
setBusy({})
|
||||||
|
await abortWithdrawal(wid);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBusy(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doConfirm() {
|
||||||
|
try {
|
||||||
|
setBusy({})
|
||||||
|
await confirmWithdrawal(wid);
|
||||||
|
notifyInfo(i18n.str`Wire transfer completed!`)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
|
||||||
|
: status === HttpStatusCode.UnprocessableEntity
|
||||||
|
? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBusy(undefined)
|
||||||
|
}
|
||||||
const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api`
|
const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api`
|
||||||
const uri = stringifyWithdrawUri({
|
const uri = stringifyWithdrawUri({
|
||||||
bankIntegrationApiBaseUrl,
|
bankIntegrationApiBaseUrl,
|
||||||
@ -92,11 +154,13 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
status: "invalid-withdrawal",
|
status: "invalid-withdrawal",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
uri,
|
uri,
|
||||||
|
onClose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (): utils.RecursiveState<State> => {
|
return (): utils.RecursiveState<State> => {
|
||||||
const result = useWithdrawalDetails(withdrawalOperationId);
|
const result = useWithdrawalDetails(withdrawalOperationId);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading) {
|
if (result.loading) {
|
||||||
return {
|
return {
|
||||||
@ -119,10 +183,17 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.confirmation_done) {
|
if (data.confirmation_done) {
|
||||||
|
if (!settings.showWithdrawalSuccess) {
|
||||||
|
updateSettings("currentWithdrawalOperationId", undefined)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "confirmed",
|
status: "confirmed",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
onClose,
|
onClose: async () => {
|
||||||
|
updateSettings("currentWithdrawalOperationId", undefined)
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +202,12 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
status: "ready",
|
status: "ready",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
uri: parsedUri,
|
uri: parsedUri,
|
||||||
onClose
|
onClose: async () => {
|
||||||
|
await doAbort()
|
||||||
|
updateSettings("currentWithdrawalOperationId", undefined)
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onAbort: doAbort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +215,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
return {
|
return {
|
||||||
status: "invalid-reserve",
|
status: "invalid-reserve",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
reserve: data.selected_reserve_pub
|
reserve: data.selected_reserve_pub,
|
||||||
|
onClose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,13 +226,23 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
|
|||||||
return {
|
return {
|
||||||
status: "invalid-payto",
|
status: "invalid-payto",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
payto: data.selected_exchange_account
|
payto: data.selected_exchange_account,
|
||||||
|
onClose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// goToConfirmOperation(withdrawalOperationId)
|
||||||
return {
|
return {
|
||||||
status: "need-confirmation",
|
status: "need-confirmation",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
onAbort: async () => {
|
||||||
|
await doAbort()
|
||||||
|
updateSettings("currentWithdrawalOperationId", undefined)
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
busy: !!busy,
|
||||||
|
onConfirm: doConfirm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { Transactions } from "../../components/Transactions/index.js";
|
import { Transactions } from "../../components/Transactions/index.js";
|
||||||
@ -24,42 +24,375 @@ import { CopyButton } from "../../components/CopyButton.js";
|
|||||||
import { bankUiSettings } from "../../settings.js";
|
import { bankUiSettings } from "../../settings.js";
|
||||||
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
|
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
|
||||||
import { useSettings } from "../../hooks/settings.js";
|
import { useSettings } from "../../hooks/settings.js";
|
||||||
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
import { undefinedIfEmpty } from "../../utils.js";
|
||||||
|
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||||
|
import { QR } from "../../components/QR.js";
|
||||||
|
|
||||||
export function InvalidPaytoView({ error }: State.InvalidPayto) {
|
export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<div>Payto from server is not valid "{payto}"</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) {
|
export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<div>Withdrawal uri from server is not valid "{uri}"</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function InvalidReserveView({ error }: State.InvalidReserve) {
|
export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<div>Reserve from server is not valid "{reserve}"</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NeedConfirmationView({ error }: State.NeedConfirmation) {
|
export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) {
|
||||||
|
const { i18n } = useTranslationContext()
|
||||||
|
|
||||||
|
const captchaNumbers = useMemo(() => {
|
||||||
|
return {
|
||||||
|
a: Math.floor(Math.random() * 10),
|
||||||
|
b: Math.floor(Math.random() * 10),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
|
||||||
|
const answer = parseInt(captchaAnswer ?? "", 10);
|
||||||
|
const errors = undefinedIfEmpty({
|
||||||
|
answer: !captchaAnswer
|
||||||
|
? i18n.str`Answer the question before continue`
|
||||||
|
: Number.isNaN(answer)
|
||||||
|
? i18n.str`The answer should be a number`
|
||||||
|
: answer !== captchaNumbers.a + captchaNumbers.b
|
||||||
|
? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
|
||||||
|
: undefined,
|
||||||
|
}) ?? (busy ? {} as Record<string, undefined> : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<div class="bg-white shadow sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-base font-semibold text-gray-900">
|
||||||
|
<i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-3">
|
||||||
|
|
||||||
|
<label class={"relative sm:col-span-2 flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}>
|
||||||
|
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" />
|
||||||
|
<span class="flex flex-1">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
|
||||||
|
<i18n.Translate>challenge response test</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-5 w-5 text-indigo-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
<label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300">
|
||||||
|
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" />
|
||||||
|
<span class="flex flex-1">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
|
<i18n.Translate>using SMS</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
|
<i18n.Translate>not available</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300">
|
||||||
|
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" />
|
||||||
|
<span class="flex flex-1">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
|
<i18n.Translate>one time password</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
|
<i18n.Translate>not available</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-sm leading-6">
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6 sm:p-8">
|
||||||
|
<label for="withdraw-amount">{i18n.str`What is`}
|
||||||
|
<em>
|
||||||
|
{captchaNumbers.a} + {captchaNumbers.b}
|
||||||
|
</em>
|
||||||
|
?
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
// class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
aria-describedby="answer"
|
||||||
|
autoFocus
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
value={captchaAnswer ?? ""}
|
||||||
|
required
|
||||||
|
|
||||||
|
name="answer"
|
||||||
|
id="answer"
|
||||||
|
autocomplete="off"
|
||||||
|
onChange={(e): void => {
|
||||||
|
setCaptchaAnswer(e.currentTarget.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
onClick={onAbort}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate></button>
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={!!errors}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onConfirm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Transfer</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 mt-4 ">
|
||||||
|
{/* <div class="w-full">
|
||||||
|
<div class="px-4 sm:px-0 text-sm">
|
||||||
|
<p><i18n.Translate>Wire transfer details</i18n.Translate></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 border-t border-gray-100">
|
||||||
|
<dl class="divide-y divide-gray-100">
|
||||||
|
{((): VNode => {
|
||||||
|
switch (details.account.targetType) {
|
||||||
|
case "iban": {
|
||||||
|
const p = details.account as PaytoUriIBAN
|
||||||
|
const name = p.params["receiver-name"]
|
||||||
|
return <Fragment>
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
|
||||||
|
</div>
|
||||||
|
{name &&
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
case "x-taler-bank": {
|
||||||
|
const p = details.account as PaytoUriTalerBank
|
||||||
|
const name = p.params["receiver-name"]
|
||||||
|
return <Fragment>
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
|
||||||
|
</div>
|
||||||
|
{name &&
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0 break-words">{details.reserve}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd>
|
||||||
|
// {/* Amounts.stringifyValue(details.amount)
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function AbortedView({ error }: State.Aborted) {
|
export function AbortedView({ error, onClose }: State.Aborted) {
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<div>aborted</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function ConfirmedView({ error }: State.Confirmed) {
|
export function ConfirmedView({ error, onClose }: State.Confirmed) {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings, updateSettings] = useSettings()
|
||||||
return (
|
return (
|
||||||
<div>Payto from server is not valid "{error.data.paytoUri}"</div>
|
<Fragment>
|
||||||
|
|
||||||
|
<div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all ">
|
||||||
|
|
||||||
|
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||||
|
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-5">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
|
||||||
|
<i18n.Translate>Withdrawal OK</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
<i18n.Translate>
|
||||||
|
The wire transfer to the Taler exchange bank's account is completed, now the
|
||||||
|
exchange will send the requested amount into your GNU Taler wallet.
|
||||||
|
</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="flex flex-grow flex-col">
|
||||||
|
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
|
||||||
|
<i18n.Translate>Do not show this again</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" data-enabled={!settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
|
||||||
|
onClick={() => {
|
||||||
|
updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
|
||||||
|
}}>
|
||||||
|
<span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 sm:mt-6">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose()
|
||||||
|
}}>
|
||||||
|
<i18n.Translate>Close</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> {
|
export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
return <div />
|
useEffect(() => {
|
||||||
|
//Taler Wallet WebExtension is listening to headers response and tab updates.
|
||||||
|
//In the SPA there is no header response with the Taler URI so
|
||||||
|
//this hack manually triggers the tab update after the QR is in the DOM.
|
||||||
|
// WebExtension will be using
|
||||||
|
// https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
|
||||||
|
document.title = `${document.title} ${uri.withdrawalOperationId}`;
|
||||||
|
}, []);
|
||||||
|
const talerWithdrawUri = stringifyWithdrawUri(uri);
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
return <Fragment>
|
||||||
|
|
||||||
|
<div class="bg-white shadow sm:rounded-lg mt-4">
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
<i18n.Translate>On this device</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 sm:flex sm:items-start sm:justify-between">
|
||||||
|
<div class="max-w-xl text-sm text-gray-500">
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled.</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
|
||||||
|
<a href={talerWithdrawUri}
|
||||||
|
class="inline-flex items-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
>
|
||||||
|
<i18n.Translate>Start</i18n.Translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white shadow sm:rounded-lg mt-2">
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
<i18n.Translate>On a mobile phone</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 sm:flex sm:items-start sm:justify-between">
|
||||||
|
<div class="max-w-xl text-sm text-gray-500">
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>Scan the QR code with your mobile device.</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||||
|
onClick={() => {
|
||||||
|
setShow(!show)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!show ?
|
||||||
|
<i18n.Translate>Show QR</i18n.Translate>
|
||||||
|
:
|
||||||
|
<i18n.Translate>Hide QR</i18n.Translate>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{show &&
|
||||||
|
<div class="mt-2 max-w-md ml-auto mr-auto">
|
||||||
|
<QR text={talerWithdrawUri} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,11 @@ import { useSettings } from "../hooks/settings.js";
|
|||||||
* Let the user choose a payment option,
|
* Let the user choose a payment option,
|
||||||
* then specify the details trigger the action.
|
* then specify the details trigger the action.
|
||||||
*/
|
*/
|
||||||
export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
||||||
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
|
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
|
||||||
// const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
@ -56,6 +55,14 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
|
{!!settings.currentWithdrawalOperationId &&
|
||||||
|
<span class="inline-flex items-center gap-x-1.5 rounded-full bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-700">
|
||||||
|
<svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
|
||||||
|
<circle cx="3" cy="3" r="3" />
|
||||||
|
</svg>
|
||||||
|
<i18n.Translate>Operation in progress</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
@ -88,9 +95,7 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
<WalletWithdrawForm
|
<WalletWithdrawForm
|
||||||
focus
|
focus
|
||||||
limit={limit}
|
limit={limit}
|
||||||
onSuccess={(id) => {
|
goToConfirmOperation={goToConfirmOperation}
|
||||||
updateSettings("currentWithdrawalOperationId", id);
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setTab(undefined)
|
setTab(undefined)
|
||||||
}}
|
}}
|
||||||
|
@ -137,107 +137,3 @@ export function QrCodeSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function QrCodeSectionSimpler({
|
|
||||||
withdrawUri,
|
|
||||||
onAborted,
|
|
||||||
}: {
|
|
||||||
withdrawUri: WithdrawUriResult;
|
|
||||||
onAborted: () => void;
|
|
||||||
}): VNode {
|
|
||||||
const { i18n } = useTranslationContext();
|
|
||||||
useEffect(() => {
|
|
||||||
//Taler Wallet WebExtension is listening to headers response and tab updates.
|
|
||||||
//In the SPA there is no header response with the Taler URI so
|
|
||||||
//this hack manually triggers the tab update after the QR is in the DOM.
|
|
||||||
// WebExtension will be using
|
|
||||||
// https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
|
|
||||||
document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`;
|
|
||||||
}, []);
|
|
||||||
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
|
|
||||||
|
|
||||||
const { abortWithdrawal } = useAccessAnonAPI();
|
|
||||||
|
|
||||||
async function doAbort() {
|
|
||||||
try {
|
|
||||||
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
|
||||||
onAborted();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
notify(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
onClientError: (status) =>
|
|
||||||
status === HttpStatusCode.Conflict
|
|
||||||
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notifyError(
|
|
||||||
i18n.str`Operation failed, please report`,
|
|
||||||
(error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error)) as TranslatedString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<div class="bg-white shadow-xl sm:rounded-lg">
|
|
||||||
<div class="p2 ">
|
|
||||||
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
|
||||||
<i18n.Translate>If you have a Taler wallet installed in this device</i18n.Translate>
|
|
||||||
</h3>
|
|
||||||
<div class="mt-4">
|
|
||||||
<a href={talerWithdrawUri}
|
|
||||||
// class="text-sm font-semibold leading-6 text-gray-900 btn "
|
|
||||||
class="inline-flex items-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
||||||
>
|
|
||||||
<i18n.Translate>Click here to start</i18n.Translate>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 max-w-xl text-sm text-gray-500">
|
|
||||||
<p><i18n.Translate>
|
|
||||||
You will see the details of the operation in your wallet including the fees (if applies).
|
|
||||||
If you still one you can install it from <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">here</a>.
|
|
||||||
</i18n.Translate></p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 ">
|
|
||||||
<div />
|
|
||||||
<button type="button"
|
|
||||||
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
|
||||||
onClick={doAbort}
|
|
||||||
>
|
|
||||||
Cancel withdrawal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white shadow-xl sm:rounded-lg mt-8">
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
|
||||||
<i18n.Translate>Or if you have the wallet in another device</i18n.Translate>
|
|
||||||
</h3>
|
|
||||||
<div class="mt-4 max-w-xl text-sm text-gray-500">
|
|
||||||
<i18n.Translate>Scan the QR below to start the withdrawal</i18n.Translate>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 max-w-md ml-auto mr-auto">
|
|
||||||
<QR text={talerWithdrawUri} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
|
||||||
<button type="button"
|
|
||||||
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
|
||||||
onClick={doAbort}
|
|
||||||
>
|
|
||||||
Cancel withdrawal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -36,33 +36,52 @@ import { useAccessAPI } from "../hooks/access.js";
|
|||||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { Amount } from "./PaytoWireTransferForm.js";
|
import { Amount } from "./PaytoWireTransferForm.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { WithdrawalOperationState } from "./WithdrawalQRCode.js";
|
import { OperationState } from "./OperationState/index.js";
|
||||||
import { Loading } from "../components/Loading.js";
|
|
||||||
|
|
||||||
const logger = new Logger("WalletWithdrawForm");
|
const logger = new Logger("WalletWithdrawForm");
|
||||||
const RefAmount = forwardRef(Amount);
|
const RefAmount = forwardRef(Amount);
|
||||||
|
|
||||||
export function WalletWithdrawForm({
|
|
||||||
focus,
|
function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
|
||||||
limit,
|
|
||||||
onSuccess,
|
|
||||||
onCancel,
|
|
||||||
}: {
|
|
||||||
limit: AmountJson;
|
limit: AmountJson;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSuccess: (operationId: string) => void;
|
goToConfirmOperation: (operationId: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { createWithdrawal } = useAccessAPI();
|
|
||||||
const [settings, updateSettings] = useSettings()
|
const [settings, updateSettings] = useSettings()
|
||||||
|
|
||||||
|
const { createWithdrawal } = useAccessAPI();
|
||||||
const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
|
const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focus) ref.current?.focus();
|
if (focus) ref.current?.focus();
|
||||||
}, [focus]);
|
}, [focus]);
|
||||||
|
|
||||||
|
if (!!settings.currentWithdrawalOperationId) {
|
||||||
|
return <div class="rounded-md bg-yellow-50 ring-yellow-2 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-bold text-yellow-800">
|
||||||
|
<i18n.Translate>There is an operation already</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-yellow-700">
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>
|
||||||
|
To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
|
||||||
|
</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
}
|
||||||
|
|
||||||
const trimmedAmountStr = amountStr?.trim();
|
const trimmedAmountStr = amountStr?.trim();
|
||||||
|
|
||||||
const parsedAmount = trimmedAmountStr
|
const parsedAmount = trimmedAmountStr
|
||||||
@ -92,7 +111,8 @@ export function WalletWithdrawForm({
|
|||||||
i18n.str`Server responded with an invalid withdraw URI`,
|
i18n.str`Server responded with an invalid withdraw URI`,
|
||||||
i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
|
i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
|
||||||
} else {
|
} else {
|
||||||
onSuccess(uri.withdrawalOperationId);
|
updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
|
||||||
|
goToConfirmOperation(uri.withdrawalOperationId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
@ -115,113 +135,168 @@ export function WalletWithdrawForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <form
|
||||||
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6 sm:p-8">
|
||||||
|
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label for="withdraw-amount">{i18n.str`Amount`}</label>
|
||||||
|
<RefAmount
|
||||||
|
currency={limit.currency}
|
||||||
|
value={amountStr}
|
||||||
|
name="withdraw-amount"
|
||||||
|
onChange={(v) => {
|
||||||
|
setAmountStr(v);
|
||||||
|
}}
|
||||||
|
error={errors?.amount}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<span class="isolate inline-flex rounded-md shadow-sm">
|
||||||
|
<button type="button"
|
||||||
|
class="relative inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAmountStr("50.00")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
50.00
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAmountStr("25.00")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
25.00
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAmountStr("10.00")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
10.00
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="relative inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAmountStr("5.00")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
5.00
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate></button>
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
// disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doStart()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Continue</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function WalletWithdrawForm({
|
||||||
|
focus,
|
||||||
|
limit,
|
||||||
|
onCancel,
|
||||||
|
goToConfirmOperation,
|
||||||
|
}: {
|
||||||
|
limit: AmountJson;
|
||||||
|
focus?: boolean;
|
||||||
|
goToConfirmOperation: (operationId: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings, updateSettings] = useSettings()
|
||||||
|
|
||||||
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
<div class="px-4 sm:px-0">
|
<div class="px-4 sm:px-0">
|
||||||
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2>
|
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2>
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
<i18n.Translate>Upon starting you will receive the money in your digital wallet, if you don't have one please <a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">install one from here</a></i18n.Translate>.
|
<i18n.Translate>After using your wallet you will confirm or cancel the operation.</i18n.Translate>
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
<i18n.Translate>After using your wallet you will be redirected here to confirm or cancel the operation.</i18n.Translate>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!settings.fastWithdrawal ?
|
<div class="col-span-2">
|
||||||
<form
|
{settings.showInstallWallet && <div class="rounded-md bg-blue-50 ring-blue-2 ring-2 p-4">
|
||||||
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
<div class="flex">
|
||||||
autoCapitalize="none"
|
<div class="flex-shrink-0">
|
||||||
autoCorrect="off"
|
<svg class="h-5 w-5 text-blue-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
onSubmit={e => {
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
||||||
e.preventDefault()
|
</svg>
|
||||||
}}
|
</div>
|
||||||
>
|
<div class="ml-3">
|
||||||
<div class="px-4 py-6 sm:p-8">
|
<h3 class="text-sm font-bold text-blue-800">
|
||||||
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
<i18n.Translate>You need a GNU Taler Wallet</i18n.Translate>
|
||||||
<div class="sm:col-span-5">
|
</h3>
|
||||||
<label for="withdraw-amount">{i18n.str`Amount`}</label>
|
<div class="mt-2 text-sm text-blue-700">
|
||||||
<RefAmount
|
<p>
|
||||||
currency={limit.currency}
|
<i18n.Translate>
|
||||||
value={amountStr}
|
If you dont have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a>
|
||||||
name="withdraw-amount"
|
</i18n.Translate>
|
||||||
onChange={(v) => {
|
</p>
|
||||||
setAmountStr(v);
|
<p class="mt-3 text-sm flex justify-end">
|
||||||
}}
|
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||||
error={errors?.amount}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-5">
|
|
||||||
<span class="isolate inline-flex rounded-md shadow-sm">
|
|
||||||
<button type="button"
|
|
||||||
class="relative inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setAmountStr("50.00")
|
updateSettings("showInstallWallet", false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
50.00
|
I know
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
</p>
|
||||||
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setAmountStr("25.00")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
25.00
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setAmountStr("10.00")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
10.00
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="relative inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setAmountStr("5.00")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
5.00
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
</div>}
|
||||||
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Cancel</i18n.Translate></button>
|
|
||||||
<button type="submit"
|
|
||||||
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
||||||
// disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
doStart()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Continue</i18n.Translate>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
{!settings.fastWithdrawal ?
|
||||||
: settings.currentWithdrawalOperationId === undefined ?
|
<OldWithdrawalForm
|
||||||
<Loading /> :
|
focus={focus}
|
||||||
<WithdrawalOperationState
|
limit={limit}
|
||||||
currentOperation={settings.currentWithdrawalOperationId}
|
onCancel={onCancel}
|
||||||
currency={limit.currency}
|
goToConfirmOperation={goToConfirmOperation}
|
||||||
onClose={() => {
|
|
||||||
onCancel()
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
:
|
||||||
|
<OperationState
|
||||||
|
currency={limit.currency}
|
||||||
|
onClose={onCancel}
|
||||||
|
goToConfirmOperation={goToConfirmOperation}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,23 +18,17 @@ import {
|
|||||||
Amounts,
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
TranslatedString,
|
|
||||||
WithdrawUriResult,
|
WithdrawUriResult,
|
||||||
parsePaytoUri,
|
parsePaytoUri
|
||||||
parseWithdrawUri,
|
|
||||||
stringifyWithdrawUri,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js";
|
import { useWithdrawalDetails } from "../hooks/access.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { handleNotOkResult } from "./HomePage.js";
|
import { handleNotOkResult } from "./HomePage.js";
|
||||||
import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js";
|
import { QrCodeSection } from "./QrCodeSection.js";
|
||||||
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
|
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
|
||||||
import { buildRequestErrorMessage } from "../utils.js";
|
|
||||||
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
|
||||||
|
|
||||||
const logger = new Logger("WithdrawalQRCode");
|
const logger = new Logger("WithdrawalQRCode");
|
||||||
|
|
||||||
@ -54,18 +48,11 @@ export function WithdrawalQRCode({
|
|||||||
const [settings, updateSettings] = useSettings();
|
const [settings, updateSettings] = useSettings();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
|
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading) {
|
if (result.loading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
result.type === ErrorType.CLIENT &&
|
|
||||||
result.status === HttpStatusCode.NotFound
|
|
||||||
) {
|
|
||||||
onClose()
|
|
||||||
return <div>operation not found</div>;
|
|
||||||
}
|
|
||||||
// onLoadNotOk();
|
|
||||||
return handleNotOkResult(i18n)(result);
|
return handleNotOkResult(i18n)(result);
|
||||||
}
|
}
|
||||||
const { data } = result;
|
const { data } = result;
|
||||||
@ -127,22 +114,6 @@ export function WithdrawalQRCode({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="flex flex-grow flex-col">
|
|
||||||
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
|
|
||||||
<i18n.Translate>Do not show this again</i18n.Translate>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<button type="button" data-enabled={!settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
|
|
||||||
|
|
||||||
onClick={() => {
|
|
||||||
updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
|
|
||||||
}}>
|
|
||||||
<span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 sm:mt-6">
|
<div class="mt-5 sm:mt-6">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
@ -182,7 +153,6 @@ export function WithdrawalQRCode({
|
|||||||
the exchange is selcted but no account
|
the exchange is selcted but no account
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithdrawalConfirmationQuestion
|
<WithdrawalConfirmationQuestion
|
||||||
withdrawUri={withdrawUri}
|
withdrawUri={withdrawUri}
|
||||||
@ -198,126 +168,3 @@ export function WithdrawalQRCode({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function WithdrawalOperationState({
|
|
||||||
currency,
|
|
||||||
currentOperation,
|
|
||||||
onClose,
|
|
||||||
}: {currency:string, currentOperation: string, onClose: () => void}): VNode {
|
|
||||||
const { i18n } = useTranslationContext();
|
|
||||||
const [settings, updateSettings] = useSettings()
|
|
||||||
const { createWithdrawal } = useAccessAPI();
|
|
||||||
|
|
||||||
const amount = settings.maxWithdrawalAmount
|
|
||||||
async function doSilentStart() {
|
|
||||||
//FIXME: if amount is not enough use balance
|
|
||||||
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await createWithdrawal({
|
|
||||||
amount: Amounts.stringify(parsedAmount),
|
|
||||||
});
|
|
||||||
const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
|
|
||||||
if (!uri) {
|
|
||||||
return notifyError(
|
|
||||||
i18n.str`Server responded with an invalid withdraw URI`,
|
|
||||||
i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
|
|
||||||
} else {
|
|
||||||
updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
notify(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
onClientError: (status) =>
|
|
||||||
status === HttpStatusCode.Forbidden
|
|
||||||
? i18n.str`The operation was rejected due to insufficient funds`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notifyError(
|
|
||||||
i18n.str`Operation failed, please report`,
|
|
||||||
(error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error)) as TranslatedString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
doSilentStart()
|
|
||||||
}, [settings.fastWithdrawal, amount])
|
|
||||||
|
|
||||||
const result = useWithdrawalDetails(currentOperation);
|
|
||||||
if (!result.ok) {
|
|
||||||
if (result.loading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
result.type === ErrorType.CLIENT &&
|
|
||||||
result.status === HttpStatusCode.NotFound
|
|
||||||
) {
|
|
||||||
onClose()
|
|
||||||
return <div>operation not found</div>;
|
|
||||||
}
|
|
||||||
// onLoadNotOk();
|
|
||||||
return handleNotOkResult(i18n)(result);
|
|
||||||
}
|
|
||||||
const { data } = result;
|
|
||||||
|
|
||||||
const baseUrl = getInitialBackendBaseURL()
|
|
||||||
const uri = stringifyWithdrawUri({
|
|
||||||
bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`,
|
|
||||||
withdrawalOperationId: currentOperation,
|
|
||||||
});
|
|
||||||
const parsedUri = parseWithdrawUri(uri);
|
|
||||||
|
|
||||||
if (data.aborted) {
|
|
||||||
return <div>
|
|
||||||
the operation was aborted, you can create another one
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.confirmation_done) {
|
|
||||||
return <div>
|
|
||||||
the wire transfer is made, you coin should arrive shortly
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
if (!parsedUri) {
|
|
||||||
return <div>
|
|
||||||
the operation is not valid, create another one
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
if (!data.selection_done) {
|
|
||||||
return (
|
|
||||||
<QrCodeSectionSimpler
|
|
||||||
withdrawUri={parsedUri}
|
|
||||||
onAborted={() => {
|
|
||||||
notifyInfo(i18n.str`Operation canceled`);
|
|
||||||
onClose()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.selected_reserve_pub) {
|
|
||||||
return <div>
|
|
||||||
the exchange is selcted but no reserve pub
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return <div>
|
|
||||||
the exchange is selected but no account
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
the operation is wating for the question to be answered
|
|
||||||
</div>;
|
|
||||||
}
|
|
@ -360,7 +360,6 @@ function CreateCashout({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="asd"
|
name="asd"
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
console.log("asdasd", form.isDebit);
|
|
||||||
form.isDebit = !form.isDebit;
|
form.isDebit = !form.isDebit;
|
||||||
updateForm(structuredClone(form));
|
updateForm(structuredClone(form));
|
||||||
}}
|
}}
|
||||||
|
Loading…
Reference in New Issue
Block a user