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 * @author sebasjm
*/ */
export enum Pages { type PageLocation<DynamicPart extends object> = {
welcome = "/welcome", pattern: string;
(params: DynamicPart): string;
};
balance = "/balance", function replaceAll(
balance_history = "/balance/history/:currency?", pattern: string,
balance_manual_withdraw = "/balance/manual-withdraw/:currency?", vars: Record<string, string>,
balance_deposit = "/balance/deposit/:currency", values: Record<string, any>,
balance_transaction = "/balance/transaction/:tid", ): string {
let result = pattern;
dev = "/dev", for (const v in vars) {
result = result.replace(vars[v], values[v]);
backup = "/backup", }
backup_provider_detail = "/backup/provider/:pid", return result;
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 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 { export function PopupNavBar({ path = "" }: { path?: string }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<NavigationHeader> <NavigationHeader>
<a href="/balance" class={path.startsWith("/balance") ? "active" : ""}> <a
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<a href="/backup" class={path.startsWith("/backup") ? "active" : ""}> <a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate> <i18n.Translate>Backup</i18n.Translate>
</a> </a>
<a href="/settings"> <a href={Pages.settings}>
<SvgIcon <SvgIcon
title={i18n.str`Settings`} title={i18n.str`Settings`}
dangerouslySetInnerHTML={{ __html: settingsIcon }} dangerouslySetInnerHTML={{ __html: settingsIcon }}
@ -92,21 +143,27 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
return ( return (
<NavigationHeaderHolder> <NavigationHeaderHolder>
<NavigationHeader> <NavigationHeader>
<a href="/balance" class={path.startsWith("/balance") ? "active" : ""}> <a
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<a href="/backup" class={path.startsWith("/backup") ? "active" : ""}> <a
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate> <i18n.Translate>Backup</i18n.Translate>
</a> </a>
<JustInDevMode> <JustInDevMode>
<a href="/dev" class={path.startsWith("/dev") ? "active" : ""}> <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate> <i18n.Translate>Dev</i18n.Translate>
</a> </a>
</JustInDevMode> </JustInDevMode>
<a <a
href="/settings" href={Pages.settings}
class={path.startsWith("/settings") ? "active" : ""} class={path.startsWith("/settings") ? "active" : ""}
> >
<i18n.Translate>Settings</i18n.Translate> <i18n.Translate>Settings</i18n.Translate>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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