625 lines
21 KiB
TypeScript
625 lines
21 KiB
TypeScript
/*
|
|
This file is part of GNU Taler
|
|
(C) 2022 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/>
|
|
*/
|
|
|
|
/**
|
|
* Main entry point for extension pages.
|
|
*
|
|
* @author sebasjm
|
|
*/
|
|
|
|
import {
|
|
TalerUri,
|
|
TranslatedString,
|
|
stringifyTalerUri,
|
|
} from "@gnu-taler/taler-util";
|
|
import {
|
|
TranslationProvider,
|
|
useTranslationContext,
|
|
} from "@gnu-taler/web-util/browser";
|
|
import { createHashHistory } from "history";
|
|
import { ComponentChildren, Fragment, VNode, h } from "preact";
|
|
import { Route, Router, route } from "preact-router";
|
|
import { useEffect } from "preact/hooks";
|
|
import {
|
|
Pages,
|
|
WalletNavBar,
|
|
WalletNavBarOptions,
|
|
getPathnameForTalerURI,
|
|
} from "../NavigationBar.js";
|
|
import { AlertView, CurrentAlerts } from "../components/CurrentAlerts.js";
|
|
import { LogoHeader } from "../components/LogoHeader.js";
|
|
import PendingTransactions from "../components/PendingTransactions.js";
|
|
import {
|
|
LinkPrimary,
|
|
RedBanner,
|
|
SubTitle,
|
|
WalletAction,
|
|
WalletBox,
|
|
} from "../components/styled/index.js";
|
|
import { AlertProvider } from "../context/alert.js";
|
|
import { IoCProviderForRuntime } from "../context/iocContext.js";
|
|
import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
|
|
import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
|
|
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
|
|
import { PaymentPage } from "../cta/Payment/index.js";
|
|
import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js";
|
|
import { RecoveryPage } from "../cta/Recovery/index.js";
|
|
import { RefundPage } from "../cta/Refund/index.js";
|
|
import { TipPage } from "../cta/Reward/index.js";
|
|
import { TransferCreatePage } from "../cta/TransferCreate/index.js";
|
|
import { TransferPickupPage } from "../cta/TransferPickup/index.js";
|
|
import {
|
|
WithdrawPageFromParams,
|
|
WithdrawPageFromURI,
|
|
} from "../cta/Withdraw/index.js";
|
|
import { useIsOnline } from "../hooks/useIsOnline.js";
|
|
import { strings } from "../i18n/strings.js";
|
|
import CloseIcon from "../svg/close_24px.inline.svg";
|
|
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
|
|
import { AddExchange } from "./AddExchange/index.js";
|
|
import { BackupPage } from "./BackupPage.js";
|
|
import { DepositPage } from "./DepositPage/index.js";
|
|
import { DestinationSelectionPage } from "./DestinationSelection/index.js";
|
|
import { DeveloperPage } from "./DeveloperPage.js";
|
|
import { HistoryPage } from "./History.js";
|
|
import { NotificationsPage } from "./Notifications/index.js";
|
|
import { ProviderDetailPage } from "./ProviderDetailPage.js";
|
|
import { QrReaderPage } from "./QrReader.js";
|
|
import { SettingsPage } from "./Settings.js";
|
|
import { TransactionPage } from "./Transaction.js";
|
|
import { WelcomePage } from "./Welcome.js";
|
|
|
|
export function Application(): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
const hash_history = createHashHistory();
|
|
|
|
async function redirectToTxInfo(tid: string): Promise<void> {
|
|
redirectTo(Pages.balanceTransaction({ tid }));
|
|
}
|
|
return (
|
|
<TranslationProvider source={strings}>
|
|
<IoCProviderForRuntime>
|
|
<Router history={hash_history}>
|
|
<Route
|
|
path={Pages.welcome}
|
|
component={() => (
|
|
<WalletTemplate>
|
|
<WelcomePage />
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.qr}
|
|
component={() => (
|
|
<WalletTemplate goToTransaction={redirectToTxInfo}>
|
|
<QrReaderPage
|
|
onDetected={(talerActionUrl: TalerUri) => {
|
|
redirectTo(
|
|
Pages.defaultCta({
|
|
uri: stringifyTalerUri(talerActionUrl),
|
|
}),
|
|
);
|
|
}}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.settings}
|
|
component={() => (
|
|
<WalletTemplate goToTransaction={redirectToTxInfo}>
|
|
<SettingsPage />
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.notifications}
|
|
component={() => (
|
|
<WalletTemplate>
|
|
<NotificationsPage />
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
{/**
|
|
* SETTINGS
|
|
*/}
|
|
<Route
|
|
path={Pages.settingsExchangeAdd.pattern}
|
|
component={() => (
|
|
<WalletTemplate>
|
|
<AddExchange onBack={() => redirectTo(Pages.balance)} />
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.balanceHistory.pattern}
|
|
component={({ currency }: { currency?: string }) => (
|
|
<WalletTemplate path="balance" goToTransaction={redirectToTxInfo}>
|
|
<HistoryPage
|
|
currency={currency}
|
|
goToWalletDeposit={(currency: string) =>
|
|
redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
|
|
}
|
|
goToWalletManualWithdraw={(currency?: string) =>
|
|
redirectTo(
|
|
Pages.receiveCash({
|
|
amount: !currency ? undefined : `${currency}:0`,
|
|
}),
|
|
)
|
|
}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.sendCash.pattern}
|
|
component={({ amount }: { amount?: string }) => (
|
|
<WalletTemplate path="balance">
|
|
<DestinationSelectionPage
|
|
type="send"
|
|
amount={amount}
|
|
goToWalletBankDeposit={(amount: string) =>
|
|
redirectTo(Pages.balanceDeposit({ amount }))
|
|
}
|
|
goToWalletWalletSend={(amount: string) =>
|
|
redirectTo(Pages.ctaTransferCreate({ amount }))
|
|
}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.receiveCash.pattern}
|
|
component={({ amount }: { amount?: string }) => (
|
|
<WalletTemplate path="balance">
|
|
<DestinationSelectionPage
|
|
type="get"
|
|
amount={amount}
|
|
goToWalletManualWithdraw={(amount?: string) =>
|
|
redirectTo(Pages.ctaWithdrawManual({ amount }))
|
|
}
|
|
goToWalletWalletInvoice={(amount?: string) =>
|
|
redirectTo(Pages.ctaInvoiceCreate({ amount }))
|
|
}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.balanceTransaction.pattern}
|
|
component={({ tid }: { tid: string }) => (
|
|
<WalletTemplate path="balance">
|
|
<TransactionPage
|
|
tid={tid}
|
|
goToWalletHistory={(currency?: string) =>
|
|
redirectTo(Pages.balanceHistory({ currency }))
|
|
}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.balanceDeposit.pattern}
|
|
component={({ amount }: { amount: string }) => (
|
|
<WalletTemplate path="balance">
|
|
<DepositPage
|
|
amount={amount}
|
|
onCancel={(currency: string) => {
|
|
redirectTo(Pages.balanceHistory({ currency }));
|
|
}}
|
|
onSuccess={(currency: string) => {
|
|
redirectTo(Pages.balanceHistory({ currency }));
|
|
}}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
<Route
|
|
path={Pages.backup}
|
|
component={() => (
|
|
<WalletTemplate path="backup" goToTransaction={redirectToTxInfo}>
|
|
<BackupPage
|
|
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.backupProviderDetail.pattern}
|
|
component={({ pid }: { pid: string }) => (
|
|
<WalletTemplate>
|
|
<ProviderDetailPage
|
|
pid={pid}
|
|
onPayProvider={(uri: string) =>
|
|
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
|
|
}
|
|
onWithdraw={(amount: string) =>
|
|
redirectTo(Pages.receiveCash({ amount }))
|
|
}
|
|
onBack={() => redirectTo(Pages.backup)}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.backupProviderAdd}
|
|
component={() => (
|
|
<WalletTemplate>
|
|
<AddBackupProviderPage
|
|
onPaymentRequired={(uri: string) =>
|
|
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
|
|
}
|
|
onComplete={(pid: string) =>
|
|
redirectTo(Pages.backupProviderDetail({ pid }))
|
|
}
|
|
onBack={() => redirectTo(Pages.backup)}
|
|
/>
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
{/**
|
|
* DEV
|
|
*/}
|
|
<Route
|
|
path={Pages.dev}
|
|
component={() => (
|
|
<WalletTemplate path="dev" goToTransaction={redirectToTxInfo}>
|
|
<DeveloperPage />
|
|
</WalletTemplate>
|
|
)}
|
|
/>
|
|
|
|
{/**
|
|
* CALL TO ACTION
|
|
*/}
|
|
<Route
|
|
path={Pages.defaultCta.pattern}
|
|
component={({ uri }: { uri: string }) => {
|
|
const path = getPathnameForTalerURI(uri);
|
|
if (!path) {
|
|
return (
|
|
<CallToActionTemplate title={i18n.str`Taler URI handler`}>
|
|
<AlertView
|
|
alert={{
|
|
type: "warning",
|
|
message: i18n.str`Could not found a handler for the Taler URI`,
|
|
description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
|
|
}}
|
|
/>
|
|
</CallToActionTemplate>
|
|
);
|
|
}
|
|
return <Redirect to={path} />;
|
|
}}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaPay}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
|
|
<PaymentPage
|
|
talerPayUri={decodeURIComponent(talerUri)}
|
|
goToWalletManualWithdraw={(amount?: string) =>
|
|
redirectTo(Pages.receiveCash({ amount }))
|
|
}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaPayTemplate}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
|
|
<PaymentTemplatePage
|
|
talerTemplateUri={decodeURIComponent(talerUri)}
|
|
goToWalletManualWithdraw={(amount?: string) =>
|
|
redirectTo(Pages.receiveCash({ amount }))
|
|
}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaRefund}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash refund`}>
|
|
<RefundPage
|
|
talerRefundUri={decodeURIComponent(talerUri)}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaTips}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash tip`}>
|
|
<TipPage
|
|
talerTipUri={decodeURIComponent(talerUri)}
|
|
onCancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaWithdraw}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
|
|
<WithdrawPageFromURI
|
|
talerWithdrawUri={decodeURIComponent(talerUri)}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaWithdrawManual.pattern}
|
|
component={({
|
|
amount,
|
|
talerUri,
|
|
}: {
|
|
amount: string;
|
|
talerUri: string;
|
|
}) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
|
|
<WithdrawPageFromParams
|
|
onAmountChanged={async (e) => {
|
|
const page = `${Pages.ctaWithdrawManual({
|
|
amount,
|
|
})}?talerUri=${encodeURIComponent(talerUri)}`;
|
|
redirectTo(page);
|
|
}}
|
|
talerExchangeWithdrawUri={talerUri}
|
|
amount={amount}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaDeposit}
|
|
component={({
|
|
amount,
|
|
talerUri,
|
|
}: {
|
|
amount: string;
|
|
talerUri: string;
|
|
}) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash deposit`}>
|
|
<DepositPageCTA
|
|
amountStr={amount}
|
|
talerDepositUri={decodeURIComponent(talerUri)}
|
|
cancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaInvoiceCreate.pattern}
|
|
component={({ amount }: { amount: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
|
|
<InvoiceCreatePage
|
|
amount={amount}
|
|
onClose={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaTransferCreate.pattern}
|
|
component={({ amount }: { amount: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
|
|
<TransferCreatePage
|
|
amount={amount}
|
|
onClose={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaInvoicePay}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
|
|
<InvoicePayPage
|
|
talerPayPullUri={decodeURIComponent(talerUri)}
|
|
goToWalletManualWithdraw={(amount?: string) =>
|
|
redirectTo(Pages.receiveCash({ amount }))
|
|
}
|
|
onClose={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaTransferPickup}
|
|
component={({ talerUri }: { talerUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
|
|
<TransferPickupPage
|
|
talerPayPushUri={decodeURIComponent(talerUri)}
|
|
onClose={() => redirectTo(Pages.balance)}
|
|
onSuccess={(tid: string) =>
|
|
redirectTo(Pages.balanceTransaction({ tid }))
|
|
}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={Pages.ctaRecovery}
|
|
component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => (
|
|
<CallToActionTemplate title={i18n.str`Digital cash recovery`}>
|
|
<RecoveryPage
|
|
talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
|
|
onCancel={() => redirectTo(Pages.balance)}
|
|
onSuccess={() => redirectTo(Pages.backup)}
|
|
/>
|
|
</CallToActionTemplate>
|
|
)}
|
|
/>
|
|
|
|
{/**
|
|
* NOT FOUND
|
|
* all redirects should be at the end
|
|
*/}
|
|
<Route
|
|
path={Pages.balance}
|
|
component={() => <Redirect to={Pages.balanceHistory({})} />}
|
|
/>
|
|
|
|
<Route
|
|
default
|
|
component={() => <Redirect to={Pages.balanceHistory({})} />}
|
|
/>
|
|
</Router>
|
|
</IoCProviderForRuntime>
|
|
</TranslationProvider>
|
|
);
|
|
}
|
|
|
|
async function redirectTo(location: string): Promise<void> {
|
|
route(location);
|
|
}
|
|
|
|
function Redirect({ to }: { to: string }): null {
|
|
useEffect(() => {
|
|
route(to, true);
|
|
});
|
|
return null;
|
|
}
|
|
|
|
function matchesRoute(url: string, route: string): boolean {
|
|
type MatcherFunc = (
|
|
url: string,
|
|
route: string,
|
|
opts: any,
|
|
) => Record<string, string> | false;
|
|
|
|
const internalPreactMatcher: MatcherFunc = (Router as any).exec;
|
|
const result = internalPreactMatcher(url, route, {});
|
|
return !result ? false : true;
|
|
}
|
|
|
|
function CallToActionTemplate({
|
|
title,
|
|
children,
|
|
}: {
|
|
title: TranslatedString;
|
|
children: ComponentChildren;
|
|
}): VNode {
|
|
const { i18n } = useTranslationContext();
|
|
return (
|
|
<WalletAction>
|
|
<LogoHeader />
|
|
<section style={{ display: "flex", justifyContent: "right", margin: 0 }}>
|
|
<LinkPrimary href={Pages.balance}>
|
|
<div
|
|
style={{
|
|
height: 24,
|
|
width: 24,
|
|
marginLeft: 4,
|
|
marginRight: 4,
|
|
border: "1px solid black",
|
|
borderRadius: 12,
|
|
}}
|
|
dangerouslySetInnerHTML={{ __html: CloseIcon }}
|
|
/>
|
|
</LinkPrimary>
|
|
</section>
|
|
<SubTitle>{title}</SubTitle>
|
|
<AlertProvider>
|
|
<CurrentAlerts />
|
|
{children}
|
|
</AlertProvider>
|
|
<section style={{ display: "flex", justifyContent: "right" }}>
|
|
<LinkPrimary href={Pages.balance}>
|
|
<i18n.Translate>Return to wallet</i18n.Translate>
|
|
</LinkPrimary>
|
|
</section>
|
|
</WalletAction>
|
|
);
|
|
}
|
|
|
|
function WalletTemplate({
|
|
path,
|
|
children,
|
|
goToTransaction,
|
|
}: {
|
|
path?: WalletNavBarOptions;
|
|
children: ComponentChildren;
|
|
goToTransaction?: (id: string) => Promise<void>;
|
|
}): VNode {
|
|
const online = useIsOnline();
|
|
const { i18n } = useTranslationContext();
|
|
return (
|
|
<Fragment>
|
|
{!online && (
|
|
<div style={{ display: "flex", justifyContent: "center" }}>
|
|
<RedBanner>{i18n.str`Network is offline`}</RedBanner>
|
|
</div>
|
|
)}
|
|
<LogoHeader />
|
|
<WalletNavBar path={path} />
|
|
{goToTransaction ? (
|
|
<PendingTransactions goToTransaction={goToTransaction} />
|
|
) : undefined}
|
|
<WalletBox>
|
|
<AlertProvider>
|
|
<CurrentAlerts />
|
|
{children}
|
|
</AlertProvider>
|
|
</WalletBox>
|
|
</Fragment>
|
|
);
|
|
}
|