diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-16 13:04:31 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-16 13:04:31 -0300 |
commit | 80fee4ee613ccfcbf951636eaac7ef61957d312d (patch) | |
tree | 6abb2a6af02f8d7a7b2c401c9bb90cc95681bb85 /packages/taler-wallet-webextension/src | |
parent | 6bb782677cd14e3ec6af249f5d8a2e93dc8e8b5f (diff) | |
download | wallet-core-80fee4ee613ccfcbf951636eaac7ef61957d312d.tar.xz |
fix #8735
Diffstat (limited to 'packages/taler-wallet-webextension/src')
5 files changed, 321 insertions, 167 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 527600c96..fe348f7fb 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -34,6 +34,7 @@ import { } from "./components/styled/index.js"; import { useBackendContext } from "./context/backend.js"; import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js"; +import searchIcon from "./svg/search_24px.inline.svg"; import qrIcon from "./svg/qr_code_24px.inline.svg"; import settingsIcon from "./svg/settings_black_24dp.inline.svg"; import warningIcon from "./svg/warning_24px.inline.svg"; @@ -55,7 +56,7 @@ type PageLocation<DynamicPart extends object> = { function replaceAll( pattern: string, vars: Record<string, string>, - values: Record<string, any>, + values: Record<string, string>, ): string { let result = pattern; for (const v in vars) { @@ -75,16 +76,20 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> { `page definition pattern ${pattern} doesn't have any parameter`, ); - const vars = patternParams.reduce((prev, cur) => { - const pName = cur.match(/(\w+)/g); + 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>); + //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 ?? {}); + const f = (values: T): string => + replaceAll(pattern, vars, (values ?? {}) as Record<string, string>); f.pattern = pattern; return f; } @@ -95,6 +100,9 @@ export const Pages = { balanceHistory: pageDefinition<{ currency?: string }>( "/balance/history/:currency?", ), + searchHistory: pageDefinition<{ currency?: string }>( + "/search/history/:currency?", + ), balanceDeposit: pageDefinition<{ amount: string }>( "/balance/deposit/:amount", ), @@ -268,6 +276,13 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode { <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }} > + <a href={Pages.searchHistory({})}> + <SvgIcon + title={i18n.str`Search transactions`} + dangerouslySetInnerHTML={{ __html: searchIcon }} + color="white" + /> + </a> <a href={Pages.qr}> <SvgIcon title={i18n.str`QR Reader and Taler URI`} diff --git a/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg new file mode 100644 index 000000000..d880cbf0f --- /dev/null +++ b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" height="24" width="24"> + <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" /> +</svg> + diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 5c31701e2..884c2eab7 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -157,7 +157,7 @@ export function Application(): VNode { )} /> - <Route +<Route path={Pages.balanceHistory.pattern} component={({ currency }: { currency?: string }) => ( <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> @@ -178,6 +178,27 @@ export function Application(): VNode { )} /> <Route + path={Pages.searchHistory.pattern} + component={({ currency }: { currency?: string }) => ( + <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + <HistoryPage + currency={currency} + search + 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" goToURL={redirectToURL}> @@ -568,17 +589,17 @@ function Redirect({ to }: { to: string }): null { return null; } -function matchesRoute(url: string, route: string): boolean { - type MatcherFunc = ( - url: string, - route: string, - opts: any, - ) => Record<string, string> | false; +// 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; -} +// const internalPreactMatcher: MatcherFunc = (Router as any).exec; +// const result = internalPreactMatcher(url, route, {}); +// return !result ? false : true; +// } function CallToActionTemplate({ title, diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index c28e4188f..482b8d698 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -49,17 +49,17 @@ export default { let count = 0; const commonTransaction = (): TransactionCommon => -({ - amountRaw: "USD:10", - amountEffective: "USD:9", - txState: { - major: TransactionMajorState.Done, - }, - timestamp: TalerProtocolTimestamp.fromSeconds( - new Date().getTime() / 1000 - count++ * 60 * 60 * 7, - ), - transactionId: String(count), -} as TransactionCommon); + ({ + amountRaw: "USD:10", + amountEffective: "USD:9", + txState: { + major: TransactionMajorState.Done, + }, + timestamp: TalerProtocolTimestamp.fromSeconds( + new Date().getTime() / 1000 - count++ * 60 * 60 * 7, + ), + transactionId: String(count), + }) as TransactionCommon; const exampleData = { withdraw: { @@ -165,7 +165,9 @@ const exampleData = { export const SomeBalanceWithNoTransactions = tests.createExample( TestedComponent, { - transactions: [], + transactionsByDate: { + "11/11/11": [], + }, balances: [ { available: "TESTKUDOS:10" as AmountString, @@ -186,7 +188,9 @@ export const SomeBalanceWithNoTransactions = tests.createExample( ); export const OneSimpleTransaction = tests.createExample(TestedComponent, { - transactions: [exampleData.withdraw], + transactionsByDate: { + "11/11/11": [exampleData.withdraw], + }, balances: [ { flags: [], @@ -203,13 +207,14 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, { }, ], balanceIndex: 0, - }); export const TwoTransactionsAndZeroBalance = tests.createExample( TestedComponent, { - transactions: [exampleData.withdraw, exampleData.deposit], + transactionsByDate: { + "11/11/11": [exampleData.withdraw, exampleData.deposit], + }, balances: [ { flags: [], @@ -230,14 +235,16 @@ export const TwoTransactionsAndZeroBalance = tests.createExample( ); export const OneTransactionPending = tests.createExample(TestedComponent, { - transactions: [ - { - ...exampleData.withdraw, - txState: { - major: TransactionMajorState.Pending, + transactionsByDate: { + "11/11/11": [ + { + ...exampleData.withdraw, + txState: { + major: TransactionMajorState.Pending, + }, }, - }, - ], + ], + }, balances: [ { flags: [], @@ -257,22 +264,24 @@ export const OneTransactionPending = tests.createExample(TestedComponent, { }); export const SomeTransactions = tests.createExample(TestedComponent, { - transactions: [ - exampleData.withdraw, - exampleData.payment, - exampleData.withdraw, - exampleData.payment, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: - "this is a long summary that may be cropped because its too long", + transactionsByDate: { + "11/11/11": [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: + "this is a long summary that may be cropped because its too long", + }, }, - }, - exampleData.refund, - exampleData.deposit, - ], + exampleData.refund, + exampleData.deposit, + ], + }, balances: [ { flags: [], @@ -294,79 +303,81 @@ export const SomeTransactions = tests.createExample(TestedComponent, { export const SomeTransactionsInDifferentStates = tests.createExample( TestedComponent, { - transactions: [ - exampleData.withdraw, - { - ...exampleData.withdraw, - exchangeBaseUrl: "https://aborted/withdrawal", - txState: { - major: TransactionMajorState.Aborted, + transactionsByDate: { + "11/11/11": [ + exampleData.withdraw, + { + ...exampleData.withdraw, + exchangeBaseUrl: "https://aborted/withdrawal", + txState: { + major: TransactionMajorState.Aborted, + }, }, - }, - { - ...exampleData.withdraw, - exchangeBaseUrl: "https://pending/withdrawal", - txState: { - major: TransactionMajorState.Pending, + { + ...exampleData.withdraw, + exchangeBaseUrl: "https://pending/withdrawal", + txState: { + major: TransactionMajorState.Pending, + }, }, - }, - { - ...exampleData.withdraw, - exchangeBaseUrl: "https://failed/withdrawal", - txState: { - major: TransactionMajorState.Failed, + { + ...exampleData.withdraw, + exchangeBaseUrl: "https://failed/withdrawal", + txState: { + major: TransactionMajorState.Failed, + }, }, - }, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: "normal payment", - }, - }, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: "aborting in progress", - }, - txState: { - major: TransactionMajorState.Aborting, - }, - }, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: "aborted payment", + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: "normal payment", + }, }, - txState: { - major: TransactionMajorState.Aborted, - }, - }, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: "pending payment", + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: "aborting in progress", + }, + txState: { + major: TransactionMajorState.Aborting, + }, }, - txState: { - major: TransactionMajorState.Pending, + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: "aborted payment", + }, + txState: { + major: TransactionMajorState.Aborted, + }, }, - }, - { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: "failed payment", + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: "pending payment", + }, + txState: { + major: TransactionMajorState.Pending, + }, }, - txState: { - major: TransactionMajorState.Failed, + { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: "failed payment", + }, + txState: { + major: TransactionMajorState.Failed, + }, }, - }, - exampleData.refund, - exampleData.deposit, - ], + exampleData.refund, + exampleData.deposit, + ], + }, balances: [ { flags: [], @@ -389,15 +400,17 @@ export const SomeTransactionsInDifferentStates = tests.createExample( export const SomeTransactionsWithTwoCurrencies = tests.createExample( TestedComponent, { - transactions: [ - exampleData.withdraw, - exampleData.payment, - exampleData.withdraw, - exampleData.payment, - exampleData.refresh, - exampleData.refund, - exampleData.deposit, - ], + transactionsByDate: { + "11/11/11": [ + exampleData.withdraw, + exampleData.payment, + exampleData.withdraw, + exampleData.payment, + exampleData.refresh, + exampleData.refund, + exampleData.deposit, + ], + }, balances: [ { flags: [], @@ -431,7 +444,9 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample( ); export const FiveOfficialCurrencies = tests.createExample(TestedComponent, { - transactions: [exampleData.withdraw], + transactionsByDate: { + "11/11/11": [exampleData.withdraw], + }, balances: [ { flags: [], @@ -505,7 +520,9 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, { export const FiveOfficialCurrenciesWithHighValue = tests.createExample( TestedComponent, { - transactions: [exampleData.withdraw], + transactionsByDate: { + "11/11/11": [exampleData.withdraw], + }, balances: [ { flags: [], @@ -578,12 +595,14 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample( ); export const PeerToPeer = tests.createExample(TestedComponent, { - transactions: [ - exampleData.pull_credit, - exampleData.pull_debit, - exampleData.push_credit, - exampleData.push_debit, - ], + transactionsByDate: { + "11/11/11": [ + exampleData.pull_credit, + exampleData.pull_debit, + exampleData.push_credit, + exampleData.push_debit, + ], + }, balances: [ { flags: [], diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index fcd21a5ee..6006ce5e7 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -35,7 +35,7 @@ import { CenteredBoldText, CenteredText, DateSeparator, - NiceSelect + NiceSelect, } from "../components/styled/index.js"; import { alertFromError, useAlertContext } from "../context/alert.js"; import { useBackendContext } from "../context/backend.js"; @@ -45,32 +45,39 @@ import { Button } from "../mui/Button.js"; import { NoBalanceHelp } from "../popup/NoBalanceHelp.js"; import DownloadIcon from "../svg/download_24px.inline.svg"; import UploadIcon from "../svg/upload_24px.inline.svg"; +import { TextField } from "../mui/TextField.js"; +import { TextFieldHandler } from "../mui/handlers.js"; interface Props { currency?: string; + search?: boolean; goToWalletDeposit: (currency: string) => Promise<void>; goToWalletManualWithdraw: (currency?: string) => Promise<void>; } export function HistoryPage({ currency: _c, + search: showSearch, goToWalletManualWithdraw, goToWalletDeposit, }: Props): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); const [balanceIndex, setBalanceIndex] = useState<number>(0); + const [search, setSearch] = useState<string>(); + const [settings] = useSettings(); const state = useAsyncAsHook(async () => { const b = await api.wallet.call(WalletApiOperation.GetBalances, {}); const balance = b.balances.length > 0 ? b.balances[balanceIndex] : undefined; const tx = await api.wallet.call(WalletApiOperation.GetTransactions, { - scopeInfo: balance?.scopeInfo, + scopeInfo: showSearch ? undefined : balance?.scopeInfo, sort: "descending", includeRefreshes: settings.showRefeshTransactions, + search, }); return { b, tx }; - }, [balanceIndex]); + }, [balanceIndex, search]); useEffect(() => { return api.listener.onUpdateNotification( @@ -105,6 +112,41 @@ export function HistoryPage({ /> ); } + + const byDate = state.response.tx.transactions.reduce( + (rv, x) => { + const startDay = + x.timestamp.t_s === "never" + ? 0 + : startOfDay(x.timestamp.t_s * 1000).getTime(); + if (startDay) { + if (!rv[startDay]) { + rv[startDay] = []; + // datesWithTransaction.push(String(startDay)); + } + rv[startDay].push(x); + } + + return rv; + }, + {} as { [x: string]: Transaction[] }, + ); + + if (showSearch) { + return ( + <FilteredHistoryView + balance={state.response.b.balances[balanceIndex]} + search={{ + value: search ?? "", + onInput: pushAlertOnError(async (d: string) => { + setSearch(d); + }), + }} + transactionsByDate={byDate} + /> + ); + } + return ( <HistoryView balanceIndex={balanceIndex} @@ -112,7 +154,7 @@ export function HistoryPage({ balances={state.response.b.balances} goToWalletManualWithdraw={goToWalletManualWithdraw} goToWalletDeposit={goToWalletDeposit} - transactions={state.response.tx.transactions} + transactionsByDate={byDate} /> ); } @@ -121,7 +163,7 @@ export function HistoryView({ balances, balanceIndex, changeBalanceIndex, - transactions, + transactionsByDate, goToWalletManualWithdraw, goToWalletDeposit, }: { @@ -129,7 +171,7 @@ export function HistoryView({ changeBalanceIndex: (s: number) => void; goToWalletDeposit: (currency: string) => Promise<void>; goToWalletManualWithdraw: (currency?: string) => Promise<void>; - transactions: Transaction[]; + transactionsByDate: Record<string, Transaction[]>; balances: WalletBalance[]; }): VNode { const { i18n } = useTranslationContext(); @@ -140,25 +182,7 @@ export function HistoryView({ ? Amounts.jsonifyAmount(balance.available) : undefined; - const datesWithTransaction: string[] = []; - const byDate = transactions.reduce( - (rv, x) => { - const startDay = - x.timestamp.t_s === "never" - ? 0 - : startOfDay(x.timestamp.t_s * 1000).getTime(); - if (startDay) { - if (!rv[startDay]) { - rv[startDay] = []; - datesWithTransaction.push(String(startDay)); - } - rv[startDay].push(x); - } - - return rv; - }, - {} as { [x: string]: Transaction[] }, - ); + const datesWithTransaction: string[] = Object.keys(transactionsByDate); return ( <Fragment> @@ -195,8 +219,8 @@ export function HistoryView({ </Button> )} </div> - <div style={{display:"flex", flexDirection:"column"}}> - <h3 style={{marginBottom: 0}}>Balance</h3> + <div style={{ display: "flex", flexDirection: "column" }}> + <h3 style={{ marginBottom: 0 }}>Balance</h3> <div style={{ width: "fit-content", @@ -270,7 +294,78 @@ export function HistoryView({ format="dd MMMM yyyy" /> </DateSeparator> - {byDate[d].map((tx, i) => ( + {transactionsByDate[d].map((tx, i) => ( + <HistoryItem key={i} tx={tx} /> + ))} + </Fragment> + ); + })} + </section> + )} + </Fragment> + ); +} + +export function FilteredHistoryView({ + balance, + search, + transactionsByDate, +}: { + balance: WalletBalance; + search: TextFieldHandler; + transactionsByDate: Record<string, Transaction[]>; +}): VNode { + const { i18n } = useTranslationContext(); + + const available = balance + ? Amounts.jsonifyAmount(balance.available) + : undefined; + + const datesWithTransaction: string[] = Object.keys(transactionsByDate); + + return ( + <Fragment> + <section> + <div + style={{ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + justifyContent: "space-between", + marginRight: 20, + }} + > + <TextField + label="Search" + variant="filled" + error={search.error} + required + fullWidth + value={search.value} + onChange={search.onInput} + /> + </div> + </section> + {datesWithTransaction.length === 0 ? ( + <section> + <i18n.Translate> + Your transaction history is empty for this currency. + </i18n.Translate> + </section> + ) : ( + <section> + {datesWithTransaction.map((d, i) => { + return ( + <Fragment key={i}> + <DateSeparator> + <Time + timestamp={AbsoluteTime.fromMilliseconds( + Number.parseInt(d, 10), + )} + format="dd MMMM yyyy" + /> + </DateSeparator> + {transactionsByDate[d].map((tx, i) => ( <HistoryItem key={i} tx={tx} /> ))} </Fragment> |