typechecked dynamic path

This commit is contained in:
Sebastian 2022-06-02 12:20:36 -03:00
parent af7b107f45
commit 5d9390bb34
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
10 changed files with 145 additions and 122 deletions

View File

@ -40,43 +40,94 @@ import settingsIcon from "./svg/settings_black_24dp.svg";
* @author sebasjm
*/
export enum Pages {
welcome = "/welcome",
type PageLocation<DynamicPart extends object> = {
pattern: string;
(params: DynamicPart): string;
};
balance = "/balance",
balance_history = "/balance/history/:currency?",
balance_manual_withdraw = "/balance/manual-withdraw/:currency?",
balance_deposit = "/balance/deposit/:currency",
balance_transaction = "/balance/transaction/:tid",
dev = "/dev",
backup = "/backup",
backup_provider_detail = "/backup/provider/:pid",
backup_provider_add = "/backup/provider/add",
settings = "/settings",
settings_exchange_add = "/settings/exchange/add/:currency?",
cta = "/cta/:action",
cta_pay = "/cta/pay",
cta_refund = "/cta/refund",
cta_tips = "/cta/tip",
cta_withdraw = "/cta/withdraw",
cta_deposit = "/cta/deposit",
function replaceAll(
pattern: string,
vars: Record<string, string>,
values: Record<string, any>,
): string {
let result = pattern;
for (const v in vars) {
result = result.replace(vars[v], values[v]);
}
return result;
}
function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
const patternParams = pattern.match(/(:[\w?]*)/g);
if (!patternParams)
throw Error(
`page definition pattern ${pattern} doesn't have any parameter`,
);
const vars = patternParams.reduce((prev, cur) => {
const pName = cur.match(/(\w+)/g);
//skip things like :? in the path pattern
if (!pName || !pName[0]) return prev;
const name = pName[0];
return { ...prev, [name]: cur };
}, {} as Record<string, string>);
const f = (values: T): string => replaceAll(pattern, vars, values);
f.pattern = pattern;
return f;
}
export const Pages = {
welcome: "/welcome",
balance: "/balance",
balanceHistory: pageDefinition<{ currency?: string }>(
"/balance/history/:currency?",
),
balanceManualWithdraw: pageDefinition<{ currency?: string }>(
"/balance/manual-withdraw/:currency?",
),
balanceDeposit: pageDefinition<{ currency: string }>(
"/balance/deposit/:currency",
),
balanceTransaction: pageDefinition<{ tid: string }>(
"/balance/transaction/:tid",
),
dev: "/dev",
backup: "/backup",
backupProviderDetail: pageDefinition<{ pid: string }>(
"/backup/provider/:pid",
),
backupProviderAdd: "/backup/provider/add",
settings: "/settings",
settingsExchangeAdd: pageDefinition<{ currency?: string }>(
"/settings/exchange/add/:currency?",
),
cta: pageDefinition<{ action: string }>("/cta/:action"),
ctaPay: "/cta/pay",
ctaRefund: "/cta/refund",
ctaTips: "/cta/tip",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
};
export function PopupNavBar({ path = "" }: { path?: string }): VNode {
const { i18n } = useTranslationContext();
return (
<NavigationHeader>
<a href="/balance" class={path.startsWith("/balance") ? "active" : ""}>
<a
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate>
</a>
<a href="/backup" class={path.startsWith("/backup") ? "active" : ""}>
<a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate>
</a>
<a href="/settings">
<a href={Pages.settings}>
<SvgIcon
title={i18n.str`Settings`}
dangerouslySetInnerHTML={{ __html: settingsIcon }}
@ -92,21 +143,27 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
return (
<NavigationHeaderHolder>
<NavigationHeader>
<a href="/balance" class={path.startsWith("/balance") ? "active" : ""}>
<a
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate>
</a>
<a href="/backup" class={path.startsWith("/backup") ? "active" : ""}>
<a
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate>
</a>
<JustInDevMode>
<a href="/dev" class={path.startsWith("/dev") ? "active" : ""}>
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate>
</a>
</JustInDevMode>
<a
href="/settings"
href={Pages.settings}
class={path.startsWith("/settings") ? "active" : ""}
>
<i18n.Translate>Settings</i18n.Translate>

View File

@ -14,7 +14,7 @@ import Banner from "./Banner.js";
import { Time } from "./Time.js";
interface Props extends JSX.HTMLAttributes {
goToTransaction: (id: string) => void;
goToTransaction: (id: string) => Promise<void>;
}
export function PendingTransactions({ goToTransaction }: Props): VNode {
@ -46,7 +46,7 @@ export function PendingTransactionsView({
transactions,
goToTransaction,
}: {
goToTransaction: (id: string) => void;
goToTransaction: (id: string) => Promise<void>;
transactions: Transaction[];
}): VNode {
return (

View File

@ -120,7 +120,7 @@ function TransactionLayout(props: TransactionLayoutProps): VNode {
const { i18n } = useTranslationContext();
return (
<HistoryRow
href={Pages.balance_transaction.replace(":tid", props.id)}
href={Pages.balanceTransaction({ tid: props.id })}
style={{
backgroundColor: props.pending ? "lightcyan" : "inherit",
alignItems: "center",

View File

@ -45,8 +45,9 @@ function CheckTalerActionComponent(): VNode {
const [talerActionUrl] = useTalerActionURL();
useEffect(() => {
if (talerActionUrl)
route(Pages.cta.replace(":action", encodeURIComponent(talerActionUrl)));
if (talerActionUrl) {
route(Pages.cta({ action: encodeURIComponent(talerActionUrl) }));
}
}, [talerActionUrl]);
return <Fragment />;
@ -60,8 +61,8 @@ export function Application(): VNode {
{({ devMode }: { devMode: boolean }) => (
<IoCProviderForRuntime>
<PendingTransactions
goToTransaction={(txId: string) =>
redirectTo(Pages.balance_transaction.replace(":tid", txId))
goToTransaction={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
<Match>
@ -74,24 +75,18 @@ export function Application(): VNode {
path={Pages.balance}
component={BalancePage}
goToWalletManualWithdraw={() =>
redirectTo(
Pages.balance_manual_withdraw.replace(":currency?", ""),
)
redirectTo(Pages.balanceManualWithdraw({}))
}
goToWalletDeposit={(currency: string) =>
redirectTo(
Pages.balance_deposit.replace(":currency", currency),
)
redirectTo(Pages.balanceDeposit({ currency }))
}
goToWalletHistory={(currency: string) =>
redirectTo(
Pages.balance_history.replace(":currency?", currency),
)
redirectTo(Pages.balanceHistory({ currency }))
}
/>
<Route
path={Pages.cta}
path={Pages.cta.pattern}
component={function Action({ action }: { action: string }) {
const [, setDismissed] = useTalerActionURL();
@ -110,37 +105,37 @@ export function Application(): VNode {
<Route
path={Pages.backup}
component={BackupPage}
onAddProvider={() => redirectTo(Pages.backup_provider_add)}
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
<Route
path={Pages.backup_provider_detail}
path={Pages.backupProviderDetail.pattern}
component={ProviderDetailPage}
onBack={() => redirectTo(Pages.backup)}
/>
<Route
path={Pages.balance_transaction}
path={Pages.balanceTransaction.pattern}
component={RedirectToWalletPage}
/>
<Route
path={Pages.balance_manual_withdraw}
path={Pages.balanceManualWithdraw.pattern}
component={RedirectToWalletPage}
/>
<Route
path={Pages.balance_deposit}
path={Pages.balanceDeposit.pattern}
component={RedirectToWalletPage}
/>
<Route
path={Pages.balance_history}
path={Pages.balanceHistory.pattern}
component={RedirectToWalletPage}
/>
<Route
path={Pages.backup_provider_add}
path={Pages.backupProviderAdd}
component={RedirectToWalletPage}
/>
<Route path={Pages.settings} component={RedirectToWalletPage} />
<Route
path={Pages.settings_exchange_add}
path={Pages.settingsExchangeAdd.pattern}
component={RedirectToWalletPage}
/>
<Route path={Pages.dev} component={RedirectToWalletPage} />

View File

@ -93,10 +93,8 @@ export function Application(): VNode {
}}
>
<PendingTransactions
goToTransaction={(txId: string) =>
redirectTo(
Pages.balance_transaction.replace(":tid", txId),
)
goToTransaction={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</div>
@ -122,50 +120,37 @@ export function Application(): VNode {
*/}
<Route
path={Pages.balance_history}
path={Pages.balanceHistory.pattern}
component={HistoryPage}
goToWalletDeposit={(currency: string) =>
redirectTo(
Pages.balance_deposit.replace(":currency", currency),
)
redirectTo(Pages.balanceDeposit({ currency }))
}
goToWalletManualWithdraw={(currency?: string) =>
redirectTo(
Pages.balance_manual_withdraw.replace(
":currency?",
currency || "",
),
)
redirectTo(Pages.balanceManualWithdraw({ currency }))
}
/>
<Route
path={Pages.balance_transaction}
path={Pages.balanceTransaction.pattern}
component={TransactionPage}
goToWalletHistory={(currency?: string) =>
redirectTo(
Pages.balance_history.replace(":currency?", currency || ""),
)
redirectTo(Pages.balanceHistory({ currency }))
}
/>
<Route
path={Pages.balance_manual_withdraw}
path={Pages.balanceManualWithdraw.pattern}
component={ManualWithdrawPage}
onCancel={() => redirectTo(Pages.balance)}
/>
<Route
path={Pages.balance_deposit}
path={Pages.balanceDeposit.pattern}
component={DepositPage}
onCancel={(currency: string) => {
redirectTo(
Pages.balance_history.replace(":currency?", currency),
);
redirectTo(Pages.balanceHistory({ currency }));
}}
onSuccess={(currency: string) => {
redirectTo(
Pages.balance_history.replace(":currency?", currency),
);
redirectTo(Pages.balanceHistory({ currency }));
setGlobalNotification(
<i18n.Translate>
All done, your transaction is in progress
@ -184,15 +169,15 @@ export function Application(): VNode {
<Route
path={Pages.backup}
component={BackupPage}
onAddProvider={() => redirectTo(Pages.backup_provider_add)}
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
<Route
path={Pages.backup_provider_detail}
path={Pages.backupProviderDetail.pattern}
component={ProviderDetailPage}
onBack={() => redirectTo(Pages.backup)}
/>
<Route
path={Pages.backup_provider_add}
path={Pages.backupProviderAdd}
component={ProviderAddPage}
onBack={() => redirectTo(Pages.backup)}
/>
@ -201,7 +186,7 @@ export function Application(): VNode {
* SETTINGS
*/}
<Route
path={Pages.settings_exchange_add}
path={Pages.settingsExchangeAdd.pattern}
component={ExchangeAddPage}
onBack={() => redirectTo(Pages.balance)}
/>
@ -216,22 +201,17 @@ export function Application(): VNode {
* CALL TO ACTION
*/}
<Route
path={Pages.cta_pay}
path={Pages.ctaPay}
component={PayPage}
goToWalletManualWithdraw={(currency?: string) =>
redirectTo(
Pages.balance_manual_withdraw.replace(
":currency?",
currency || "",
),
)
redirectTo(Pages.balanceManualWithdraw({ currency }))
}
goBack={() => redirectTo(Pages.balance)}
/>
<Route path={Pages.cta_refund} component={RefundPage} />
<Route path={Pages.cta_tips} component={TipPage} />
<Route path={Pages.cta_withdraw} component={WithdrawPage} />
<Route path={Pages.cta_deposit} component={DepositPageCTA} />
<Route path={Pages.ctaRefund} component={RefundPage} />
<Route path={Pages.ctaTips} component={TipPage} />
<Route path={Pages.ctaWithdraw} component={WithdrawPage} />
<Route path={Pages.ctaDeposit} component={DepositPageCTA} />
{/**
* NOT FOUND
@ -240,13 +220,13 @@ export function Application(): VNode {
<Route
path={Pages.balance}
component={Redirect}
to={Pages.balance_history.replace(":currency?", "")}
to={Pages.balanceHistory({})}
/>
<Route
default
component={Redirect}
to={Pages.balance_history.replace(":currency?", "")}
to={Pages.balanceHistory({})}
/>
</Router>
</WalletBox>
@ -268,5 +248,7 @@ function Redirect({ to }: { to: string }): null {
}
function shouldShowPendingOperations(path: string): boolean {
// FIXME: replace includes with a match API like preact router does
// [Pages.balanceHistory, Pages.dev, Pages.settings, Pages.backup]
return ["/balance/history/", "/dev", "/settings", "/backup"].includes(path);
}

View File

@ -167,10 +167,9 @@ function BackupLayout(props: TransactionLayoutProps): VNode {
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
<a
href={Pages.backup_provider_detail.replace(
":pid",
encodeURIComponent(props.id),
)}
href={Pages.backupProviderDetail({
pid: encodeURIComponent(props.id),
})}
>
<span>{props.title}</span>
</a>

View File

@ -165,10 +165,7 @@ export function CreateManualWithdraw({
</i18n.Translate>
</BoldLight>
<LinkPrimary
href={Pages.settings_exchange_add.replace(
":currency?",
initialCurrency,
)}
href={Pages.settingsExchangeAdd({ currency: initialCurrency })}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>
@ -194,7 +191,7 @@ export function CreateManualWithdraw({
<i18n.Translate>No exchange configured</i18n.Translate>
</BoldLight>
<LinkPrimary
href={Pages.settings_exchange_add.replace(":currency?", "")}
href={Pages.settingsExchangeAdd({})}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>
@ -242,7 +239,7 @@ export function CreateManualWithdraw({
</Input>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<LinkPrimary
href={Pages.settings_exchange_add.replace(":currency?", "")}
href={Pages.settingsExchangeAdd({})}
style={{ marginLeft: "auto" }}
>
<i18n.Translate>Add Exchange</i18n.Translate>

View File

@ -156,9 +156,6 @@ export function View({
[exchange_name: string]: CalculatedCoinfInfo[];
},
);
function Item({ children }: any) {
return <div>{children}</div>;
}
return (
<div>
<p>

View File

@ -178,9 +178,7 @@ export function SettingsView({
)}
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div />
<LinkPrimary
href={Pages.settings_exchange_add.replace(":currency?", "")}
>
<LinkPrimary href={Pages.settingsExchangeAdd({})}>
<i18n.Translate>Add an exchange</i18n.Translate>
</LinkPrimary>
</div>

View File

@ -352,10 +352,9 @@ export function TransactionView({
<td>
{<Amount value={r.amountEffective} />}{" "}
<a
href={Pages.balance_transaction.replace(
":tid",
r.transactionId,
)}
href={Pages.balanceTransaction({
tid: r.transactionId,
})}
>
was refunded
</a>{" "}
@ -556,10 +555,9 @@ export function TransactionView({
title={<i18n.Translate>Original order ID</i18n.Translate>}
text={
<a
href={Pages.balance_transaction.replace(
":tid",
transaction.refundedTransactionId,
)}
href={Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
})}
>
{transaction.info.orderId}
</a>