diff options
author | Marcos Gutierrez <gmarcos87@gmail.com> | 2020-01-27 10:11:39 -0300 |
---|---|---|
committer | Marcos Gutierrez <gmarcos87@gmail.com> | 2020-01-27 10:11:39 -0300 |
commit | d6d56479469f0e7fc050976908c7abcacd61bc27 (patch) | |
tree | 3ab7136e544b41edf0b5d1a936d23206be38f695 /src/webex/pages | |
parent | b961ca2c39d813443581b8651e6b144ee8bec6e9 (diff) | |
download | wallet-core-d6d56479469f0e7fc050976908c7abcacd61bc27.tar.xz |
add new history view and some global style changes
Diffstat (limited to 'src/webex/pages')
-rw-r--r-- | src/webex/pages/popup.css | 129 | ||||
-rw-r--r-- | src/webex/pages/popup.tsx | 432 |
2 files changed, 458 insertions, 103 deletions
diff --git a/src/webex/pages/popup.css b/src/webex/pages/popup.css index 675412c11..4cae66177 100644 --- a/src/webex/pages/popup.css +++ b/src/webex/pages/popup.css @@ -12,21 +12,24 @@ body { padding: 0; max-height: 800px; overflow: hidden; + background-color: #f8faf7; + font-family: Arial, Helvetica, sans-serif; } .nav { - background-color: #ddd; + background-color: #033; padding: 0.5em 0; } .nav a { - color: black; - padding: 0.5em; + color: #f8faf7; + padding: 0.7em 1.4em; text-decoration: none; } .nav a.active { - background-color: white; + background-color: #f8faf7; + color: #000; font-weight: bold; } @@ -71,14 +74,114 @@ body { } .historyItem { - border: 1px solid black; - border-radius: 10px; - padding-left: 0.5em; - margin: 0.5em; -} - -.historyDate { - font-size: 90%; + min-width: 380px; + display: flex; + flex-direction: row; + border-bottom: 1px solid #d9dbd8; + padding: 0.5em; + align-items: center; + } + + .historyItem .amount { + font-size: 110%; + font-weight: bold; + text-align: right; + } + + .historyDate, + .historyTitle, + .historyText, + .historySmall { margin: 0.3em; + } + + .historyDate { + font-size: 90%; color: slategray; -} + text-align: right; + } + + .historyLeft { + display: flex; + flex-direction: column; + text-align: right; + } + + .historyContent { + flex: 1; + } + + .historyTitle { + font-weight: 400; + } + + .historyText { + font-size: 90%; + } + + .historySmall { + font-size: 70%; + text-transform: uppercase; + } + + .historyAmount { + flex-grow: 1; + } + + .historyAmount .primary { + font-size: 100%; + } + + .historyAmount .secondary { + font-size: 80%; + } + + .historyAmount .positive { + color: #088; + } + + .historyAmount .positive:before { + content: "+"; + } + + .historyAmount .negative { + color: #800 + } + + .historyAmount .negative:before { + color: #800; + content: "-"; + } + .icon { + margin: 0 10px; + text-align: center; + width: 35px; + font-size: 20px; + border-radius: 50%; + background: #ccc; + color: #fff; + padding-top: 4px; + height: 30px; + } + + .option { + text-transform: uppercase; + text-align: right; + padding: 0.4em; + font-size: 0.9em; + } + + input[type=checkbox], input[type=radio] { + vertical-align: middle; + position: relative; + bottom: 1px; + } + + input[type=radio] { + bottom: 2px; + } + + .balance { + text-align: center; + padding-top: 2em; + }
\ No newline at end of file diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index b26e86e65..f873414a1 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -42,9 +42,12 @@ import { } from "../renderHtml"; import * as wxApi from "../wxApi"; -import * as React from "react"; +import React, { Fragment } from "react"; import { HistoryEvent } from "../../types/history"; +import moment from "moment"; +import { Timestamp } from "../../util/time"; + function onUpdateNotification(f: () => void): () => void { const port = chrome.runtime.connect({ name: "notifications" }); const listener = () => { @@ -185,7 +188,7 @@ function bigAmount(amount: AmountJson): JSX.Element { const v = amount.value + amount.fraction / Amounts.fractionalBase; return ( <span> - <span style={{ fontSize: "300%" }}>{v}</span>{" "} + <span style={{ fontSize: "5em", display: 'block'}}>{v}</span>{" "} <span>{amount.currency}</span> </span> ); @@ -193,12 +196,10 @@ function bigAmount(amount: AmountJson): JSX.Element { function EmptyBalanceView() { return ( - <div> - <i18n.Translate wrap="p"> - You have no balance to show. Need some{" "} - <PageLink pageName="welcome.html">help</PageLink> getting started? - </i18n.Translate> - </div> + <i18n.Translate wrap="p"> + You have no balance to show. Need some{" "} + <PageLink pageName="welcome.html">help</PageLink> getting started? + </i18n.Translate> ); } @@ -299,7 +300,7 @@ class WalletBalanceView extends React.Component<any, any> { const wallet = this.balance; if (this.gotError) { return ( - <div> + <div className='balance'> <p>{i18n.str`Error: could not retrieve balance information.`}</p> <p> Click <PageLink pageName="welcome.html">here</PageLink> for help and @@ -320,114 +321,354 @@ class WalletBalanceView extends React.Component<any, any> { </p> ); }); - return <div>{listing.length > 0 ? listing : <EmptyBalanceView />}</div>; + return listing.length > 0 ? <div className='balance'>{listing}</div> : <EmptyBalanceView />; } } +function Icon({ l }: { l: string }) { + return <div className={"icon"}>{l}</div>; +} + +function formatAndCapitalize(text: string) { + text = text.replace("-", " "); + text = text.replace(/^./, text[0].toUpperCase()); + return text; +} + +type HistoryItemProps = { + title?: string | JSX.Element; + text?: string | JSX.Element; + small?: string | JSX.Element; + amount?: string | AmountJson; + fees?: string | AmountJson; + invalid?: string | AmountJson; + icon?: string; + timestamp: Timestamp; + negative?: boolean; +}; + +function HistoryItem({ + title, + text, + small, + amount, + fees, + invalid, + timestamp, + icon, + negative = false +}: HistoryItemProps) { + function formatDate(timestamp: number | "never") { + if (timestamp !== "never") { + const itemDate = moment(timestamp); + if (itemDate.isBetween(moment().subtract(2, "days"), moment())) { + return itemDate.fromNow(); + } + return itemDate.format("lll"); + } + return null; + } + + let invalidElement, amountElement, feesElement; + + if (amount) { + amountElement = renderAmount(amount); + } + + if (fees) { + fees = typeof fees === "string" ? Amounts.parse(fees) : fees; + if (fees && Amounts.isNonZero(fees)) { + feesElement = renderAmount(fees); + } + } + + if (invalid) { + invalid = typeof invalid === "string" ? Amounts.parse(invalid) : invalid; + if (invalid && Amounts.isNonZero(invalid)) { + invalidElement = renderAmount(invalid); + } + } + + return ( + <div className="historyItem"> + {icon ? <Icon l={icon} /> : null} + <div className="historyContent"> + {title ? <div className={"historyTitle"}>{title}</div> : null} + {text ? <div className={"historyText"}>{text}</div> : null} + {small ? <div className={"historySmall"}>{small}</div> : null} + </div> + <div className={"historyLeft"}> + <div className={"historyAmount"}> + {amountElement ? ( + <div className={`${negative ? "negative" : "positive"}`}> + {amountElement} + </div> + ) : null} + {invalidElement ? ( + <div className={"secondary"}> + {i18n.str`Invalid `}{" "} + <span className={"negative"}>{invalidElement}</span> + </div> + ) : null} + {feesElement ? ( + <div className={"secondary"}> + {i18n.str`Fees `}{" "} + <span className={"negative"}>{feesElement}</span> + </div> + ) : null} + </div> + <div className="historyDate">{formatDate(timestamp.t_ms)}</div> + </div> + </div> + ); +} + +function amountDiff( + total: string | Amounts.AmountJson, + partial: string | Amounts.AmountJson +): Amounts.AmountJson | string { + let a = typeof total === "string" ? Amounts.parse(total) : total; + let b = typeof partial === "string" ? Amounts.parse(partial) : partial; + if (a && b) { + return Amounts.sub(a, b).amount; + } else { + return total; + } +} + + +function parseSummary(summary: string) { + let parsed = summary.split(/: (.+)/); + return { + merchant: parsed[0], + item: parsed[1] + }; +} + function formatHistoryItem(historyItem: HistoryEvent) { - const d = historyItem; - console.log("hist item", historyItem); switch (historyItem.type) { - /* - case "create-reserve": + case "refreshed": { return ( - <i18n.Translate wrap="p"> - Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for{" "} - <span>{renderAmount(d.requestedAmount)}</span>. - </i18n.Translate> + <HistoryItem + timestamp={historyItem.timestamp} + small={i18n.str`Refresh sessions has completed`} + fees={amountDiff( + historyItem.amountRefreshedRaw, + historyItem.amountRefreshedEffective + )} + /> + ); + } + + case "order-refused": { + const { merchant, item } = parseSummary( + historyItem.orderShortInfo.summary ); - case "confirm-reserve": { - const exchange = new URL(d.exchangeBaseUrl).host; - const pub = abbrev(d.reservePub); return ( - <i18n.Translate wrap="p"> - Started to withdraw - <span>{renderAmount(d.requestedAmount)}</span> - from <span>{exchange}</span> (<span>{pub}</span>). - </i18n.Translate> + <HistoryItem + icon={"X"} + timestamp={historyItem.timestamp} + small={i18n.str`Order Refused`} + title={merchant} + text={abbrev(item, 30)} + /> ); } - case "offer-contract": { + + case "order-redirected": { + const { merchant, item } = parseSummary( + historyItem.newOrderShortInfo.summary + ); return ( - <i18n.Translate wrap="p"> - Merchant <em>{abbrev(d.merchantName, 15)}</em> offered contract{" "} - <span>{abbrev(d.contractTermsHash)}</span>. - </i18n.Translate> + <HistoryItem + icon={"⟲"} + small={i18n.str`Order redirected`} + text={abbrev(item, 40)} + timestamp={historyItem.timestamp} + title={merchant} + /> ); } - case "depleted-reserve": { - const exchange = d.exchangeBaseUrl - ? new URL(d.exchangeBaseUrl).host - : "??"; - const amount = renderAmount(d.requestedAmount); - const pub = abbrev(d.reservePub); + + case "payment-aborted": { + const { merchant, item } = parseSummary( + historyItem.orderShortInfo.summary + ); return ( - <i18n.Translate wrap="p"> - Withdrew <span>{amount}</span> from <span>{exchange}</span> ( - <span>{pub}</span>). - </i18n.Translate> + <HistoryItem + amount={historyItem.orderShortInfo.amount} + fees={historyItem.amountLost} + icon={"P"} + small={i18n.str`Payment aborted`} + text={abbrev(item, 40)} + timestamp={historyItem.timestamp} + title={merchant} + /> ); } - case "pay": { - const url = d.fulfillmentUrl; - const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>; + + case "payment-sent": { + const url = historyItem.orderShortInfo.merchantBaseUrl; + const { merchant, item } = parseSummary( + historyItem.orderShortInfo.summary + ); + const fees = amountDiff( + historyItem.amountPaidWithFees, + historyItem.orderShortInfo.amount + ); const fulfillmentLinkElem = ( - <a href={url} onClick={openTab(url)}> - view product - </a> + <Fragment> + <a + href={historyItem.orderShortInfo.merchantBaseUrl} + onClick={openTab(url)} + > + {item ? abbrev(item, 30) : null} + </a> + </Fragment> ); return ( - <i18n.Translate wrap="p"> - Paid <span>{renderAmount(d.amount)}</span> to merchant{" "} - <span>{merchantElem}</span>.<span> </span>( - <span>{fulfillmentLinkElem}</span>) - </i18n.Translate> + <HistoryItem + amount={historyItem.orderShortInfo.amount} + fees={fees} + icon={"P"} + negative={true} + small={i18n.str`Payment Sent`} + text={fulfillmentLinkElem} + timestamp={historyItem.timestamp} + title={merchant} + /> + ); + } + case "order-accepted": { + const url = historyItem.orderShortInfo.merchantBaseUrl; + const { merchant, item } = parseSummary( + historyItem.orderShortInfo.summary + ); + const fulfillmentLinkElem = ( + <Fragment> + <a + href={historyItem.orderShortInfo.merchantBaseUrl} + onClick={openTab(url)} + > + {item ? abbrev(item, 40) : null} + </a> + </Fragment> + ); + return ( + <HistoryItem + negative={true} + amount={historyItem.orderShortInfo.amount} + icon={"P"} + small={i18n.str`Order accepted`} + text={fulfillmentLinkElem} + timestamp={historyItem.timestamp} + title={merchant} + /> + ); + } + case "reserve-balance-updated": { + return ( + <HistoryItem + timestamp={historyItem.timestamp} + small={i18n.str`Reserve balance updated`} + fees={amountDiff( + historyItem.amountExpected, + historyItem.amountReserveBalance + )} + /> ); } case "refund": { - const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>; + const merchantElem = ( + <em>{abbrev(historyItem.orderShortInfo.summary, 25)}</em> + ); return ( - <i18n.Translate wrap="p"> - Merchant <span>{merchantElem}</span> gave a refund over{" "} - <span>{renderAmount(d.refundAmount)}</span>. - </i18n.Translate> + <HistoryItem + icon={"R"} + timestamp={historyItem.timestamp} + small={i18n.str`Payment refund`} + text={merchantElem} + amount={historyItem.amountRefundedRaw} + invalid={historyItem.amountRefundedInvalid} + fees={amountDiff( + amountDiff( + historyItem.amountRefundedRaw, + historyItem.amountRefundedInvalid + ), + historyItem.amountRefundedEffective + )} + /> ); } - case "tip": { - const tipPageUrl = new URL(chrome.extension.getURL("/src/webex/pages/tip.html")); - tipPageUrl.searchParams.set("tip_id", d.tipId); - tipPageUrl.searchParams.set("merchant_domain", d.merchantDomain); - const url = tipPageUrl.href; - const tipLink = <a href={url} onClick={openTab(url)}>{i18n.str`tip`}</a>; - // i18n: Tip + case "withdrawn": { + const exchange = new URL(historyItem.exchangeBaseUrl).host; + const fees = amountDiff( + historyItem.amountWithdrawnRaw, + historyItem.amountWithdrawnEffective + ); + return ( + <HistoryItem + amount={historyItem.amountWithdrawnRaw} + fees={fees} + icon={"w"} + small={i18n.str`Withdrawn`} + title={exchange} + timestamp={historyItem.timestamp} + /> + ); + } + case "tip-accepted": { + return ( + <HistoryItem + icon={"T"} + negative={true} + timestamp={historyItem.timestamp} + title={<i18n.Translate wrap={Fragment}>Tip Accepted</i18n.Translate>} + amount={historyItem.tipAmountRaw} + /> + ); + } + case "tip-declined": { return ( - <> - <i18n.Translate wrap="p"> - Merchant <span>{d.merchantDomain}</span> gave a{" "} - <span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>. - </i18n.Translate> - <span> - {" "} - {d.accepted ? null : ( - <i18n.Translate>You did not accept the tip yet.</i18n.Translate> - )} - </span> - </> + <HistoryItem + icon={"T"} + timestamp={historyItem.timestamp} + title={<i18n.Translate wrap={Fragment}>Tip Declined</i18n.Translate>} + amount={historyItem.tipAmountRaw} + /> ); } - */ default: - return <p>{i18n.str`Unknown event (${historyItem.type})`}</p>; + return ( + <HistoryItem + timestamp={historyItem.timestamp} + small={i18n.str`${formatAndCapitalize(historyItem.type)}`} + /> + ); } } +const HistoryComponent = (props: any) => { + const record = props.record; + return formatHistoryItem(record); +}; + class WalletHistory extends React.Component<any, any> { private myHistory: any[]; private gotError = false; private unmounted = false; - + private hidenTypes: string[] = [ + "order-accepted", + "order-redirected", + "refreshed", + "reserve-balance-updated", + "exchange-updated", + "exchange-added" + ]; + componentWillMount() { this.update(); + this.setState({ filter: true }); onUpdateNotification(() => this.update()); } @@ -468,21 +709,32 @@ class WalletHistory extends React.Component<any, any> { } const listing: any[] = []; - for (const record of history.reverse()) { - const item = ( - <div className="historyItem"> - <div className="historyDate"> - {new Date(record.timestamp.t_ms).toString()} - </div> - {formatHistoryItem(record)} - </div> - ); - + const messages = history + .reverse() + .filter(hEvent => { + if (!this.state.filter) return true; + return this.hidenTypes.indexOf(hEvent.type) === -1; + }); + + for (const record of messages) { + const item = (<HistoryComponent key={record.eventId} record={record} />); listing.push(item); } if (listing.length > 0) { - return <div className="container">{listing}</div>; + return ( + <div> + <div className="container">{listing}</div> + <div className="option"> + Filtered list{" "} + <input + type="checkbox" + checked={this.state.filter} + onChange={() => this.setState({ filter: !this.state.filter })} + /> + </div> + </div> + ); } return <p>{i18n.str`Your wallet has no events recorded.`}</p>; } |