transaction details template

mayor change in the template of the transaction details for every
transaction
more work needs to be done in wallet core for tip and refund to show
more information about the merchant like logo and website
This commit is contained in:
Sebastian 2022-05-26 15:55:14 -03:00
parent 72d936eaf9
commit 24162c1086
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
14 changed files with 976 additions and 460 deletions

View File

@ -362,6 +362,9 @@ export interface MerchantInfo {
name: string; name: string;
jurisdiction?: Location; jurisdiction?: Location;
address?: Location; address?: Location;
logo?: string;
website?: string;
email?: string;
} }
export interface Tax { export interface Tax {

View File

@ -33,6 +33,7 @@ import {
codecForInternationalizedString, codecForInternationalizedString,
codecForMerchantInfo, codecForMerchantInfo,
codecForProduct, codecForProduct,
Location,
} from "./talerTypes.js"; } from "./talerTypes.js";
import { import {
Codec, Codec,
@ -276,6 +277,17 @@ export interface OrderShortInfo {
*/ */
products: Product[] | undefined; products: Product[] | undefined;
/**
* Time indicating when the order should be delivered.
* May be overwritten by individual products.
*/
delivery_date?: TalerProtocolTimestamp;
/**
* Delivery location for (all!) products.
*/
delivery_location?: Location;
/** /**
* URL of the fulfillment, given by the merchant * URL of the fulfillment, given by the merchant
*/ */

View File

@ -54,6 +54,7 @@ export const buildConfig = {
loader: { loader: {
'.svg': 'text', '.svg': 'text',
'.png': 'dataurl', '.png': 'dataurl',
'.jpeg': 'dataurl',
}, },
target: [ target: [
'es6' 'es6'

View File

@ -6,7 +6,7 @@ export function Amount({ value }: { value: AmountJson | AmountString }): VNode {
const amount = Amounts.stringifyValue(aj, 2); const amount = Amounts.stringifyValue(aj, 2);
return ( return (
<Fragment> <Fragment>
{amount} {aj.currency} {amount}&nbsp;{aj.currency}
</Fragment> </Fragment>
); );
} }

View File

@ -44,7 +44,7 @@ export function BalanceTable({
width: "100%", width: "100%",
}} }}
> >
{Amounts.stringifyValue(av)} {Amounts.stringifyValue(av, 2)}
</td> </td>
</tr> </tr>
); );

View File

@ -46,43 +46,47 @@ export function BankDetailsByPaytoType({
if (payto.isKnown && payto.targetType === "bitcoin") { if (payto.isKnown && payto.targetType === "bitcoin") {
const min = segwitMinAmount(amount.currency); const min = segwitMinAmount(amount.currency);
return ( return (
<section style={{ textAlign: "left" }}> <section
style={{
textAlign: "left",
border: "solid 1px black",
padding: 8,
borderRadius: 4,
}}
>
<p style={{ marginTop: 0 }}>Bitcoin transfer details</p>
<p> <p>
<i18n.Translate> <i18n.Translate>
Bitcoin exchange need a transaction with 3 output, one output is the The exchange need a transaction with 3 output, one output is the
exchange account and the other two are segwit fake address for exchange account and the other two are segwit fake address for
metadata with an minimum amount. Reserve pub : {subject} metadata with an minimum amount.
</i18n.Translate> </i18n.Translate>
</p> </p>
<Row
literal
name={<i18n.Translate>Reserve</i18n.Translate>}
value={subject}
/>
<p> <p>
<i18n.Translate> <i18n.Translate>
In bitcoincore wallet use &apos;Add Recipient&apos; button to add In bitcoincore wallet use &apos;Add Recipient&apos; button to add
two additional recipient and copy addresses and amounts two additional recipient and copy addresses and amounts
</i18n.Translate> </i18n.Translate>
<ul> </p>
<li> <table>
{payto.targetPath} {Amounts.stringifyValue(amount)} BTC <tr>
</li> <td>{payto.targetPath}</td>
{payto.segwitAddrs.map((addr, i) => ( <td>{Amounts.stringifyValue(amount)} BTC</td>
<li key={i}> </tr>
{addr} {Amounts.stringifyValue(min)} BTC {payto.segwitAddrs.map((addr, i) => (
</li> <tr key={i}>
))} <td>{addr}</td>
</ul> <td>{Amounts.stringifyValue(min)} BTC</td>
<i18n.Translate> </tr>
In Electrum wallet paste the following three lines in &apos;Pay ))}
to&apos; field : </table>
</i18n.Translate> <p>
<ul>
<li>
{payto.targetPath},{Amounts.stringifyValue(amount)}
</li>
{payto.segwitAddrs.map((addr, i) => (
<li key={i}>
{addr} {Amounts.stringifyValue(min)} BTC
</li>
))}
</ul>
<i18n.Translate> <i18n.Translate>
Make sure the amount show{" "} Make sure the amount show{" "}
{Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "} {Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "}
@ -93,7 +97,7 @@ export function BankDetailsByPaytoType({
); );
} }
const firstPart = !payto.isKnown ? ( const accountPart = !payto.isKnown ? (
<Row <Row
name={<i18n.Translate>Account</i18n.Translate>} name={<i18n.Translate>Account</i18n.Translate>}
value={payto.targetPath} value={payto.targetPath}
@ -113,10 +117,17 @@ export function BankDetailsByPaytoType({
<Row name={<i18n.Translate>IBAN</i18n.Translate>} value={payto.iban} /> <Row name={<i18n.Translate>IBAN</i18n.Translate>} value={payto.iban} />
) : undefined; ) : undefined;
return ( return (
<div style={{ textAlign: "left" }}> <div
<p>Bank transfer details</p> style={{
textAlign: "left",
border: "solid 1px black",
padding: 8,
borderRadius: 4,
}}
>
<p style={{ marginTop: 0 }}>Bank transfer details</p>
<table> <table>
{firstPart} {accountPart}
<Row <Row
name={<i18n.Translate>Exchange</i18n.Translate>} name={<i18n.Translate>Exchange</i18n.Translate>}
value={exchangeBaseUrl} value={exchangeBaseUrl}
@ -176,7 +187,7 @@ function Row({
</TooltipRight> </TooltipRight>
)} )}
</td> </td>
<td> <td style={{ paddingRight: 8 }}>
<b>{name}</b> <b>{name}</b>
</td> </td>
{literal ? ( {literal ? (

View File

@ -14,33 +14,122 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { ExtraLargeText, LargeText, SmallLightText } from "./styled/index.js"; import { useState } from "preact/hooks";
import {
ExtraLargeText,
LargeText,
SmallBoldText,
SmallLightText,
} from "./styled/index.js";
export type Kind = "positive" | "negative" | "neutral"; export type Kind = "positive" | "negative" | "neutral";
interface Props { interface Props {
title: VNode; title: VNode | string;
text: VNode | string; text: VNode | string;
kind: Kind; kind?: Kind;
big?: boolean; big?: boolean;
showSign?: boolean;
} }
export function Part({ text, title, kind, big }: Props): VNode { export function Part({
text,
title,
kind = "neutral",
big,
showSign,
}: Props): VNode {
const Text = big ? ExtraLargeText : LargeText; const Text = big ? ExtraLargeText : LargeText;
return ( return (
<div style={{ margin: "1em" }}> <div style={{ margin: "1em" }}>
<SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText> <SmallBoldText style={{ marginBottom: "1em" }}>{title}</SmallBoldText>
<Text <Text
style={{ style={{
color: color:
kind == "positive" ? "green" : kind == "negative" ? "red" : "black", kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
fontWeight: "lighten",
}} }}
> >
{!showSign || kind === "neutral"
? undefined
: kind === "positive"
? "+"
: "-"}
{text} {text}
</Text> </Text>
</div> </div>
); );
} }
const CollasibleBox = styled.div`
border: 1px solid black;
border-radius: 0.25em;
display: flex;
vertical-align: middle;
justify-content: space-between;
flex-direction: column;
/* margin: 0.5em; */
padding: 0.5em;
/* margin: 1em; */
/* width: 100%; */
/* color: #721c24; */
/* background: #f8d7da; */
& > div {
display: flex;
justify-content: space-between;
div {
margin-top: auto;
margin-bottom: auto;
}
& > button {
align-self: center;
font-size: 100%;
padding: 0;
height: 28px;
width: 28px;
}
}
`;
import arrowDown from "../svg/chevron-down.svg";
export function PartCollapsible({ text, title, big, showSign }: Props): VNode {
const Text = big ? ExtraLargeText : LargeText;
const [collapsed, setCollapsed] = useState(true);
return (
<CollasibleBox>
<div>
<SmallBoldText>{title}</SmallBoldText>
<button
onClick={() => {
setCollapsed((v) => !v);
}}
>
<div
style={{
transform: !collapsed ? "scaleY(-1)" : undefined,
height: 24,
}}
dangerouslySetInnerHTML={{ __html: arrowDown }}
/>
</button>
</div>
{/* <SmallBoldText
style={{
paddingBottom: "1em",
paddingTop: "1em",
paddingLeft: "1em",
border: "black solid 1px",
}}
>
</SmallBoldText> */}
{!collapsed && <div style={{ display: "block" }}>{text}</div>}
</CollasibleBox>
);
}
interface PropsPayto { interface PropsPayto {
payto: PaytoUri; payto: PaytoUri;
kind: Kind; kind: Kind;

View File

@ -87,7 +87,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
& > * { & > * {
width: 500px; width: 600px;
} }
& > section { & > section {
padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")}; padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
@ -660,6 +660,12 @@ export const WarningText = styled.div`
export const SmallText = styled.div` export const SmallText = styled.div`
font-size: small; font-size: small;
`; `;
export const SmallBoldText = styled.div`
font-size: small;
font-weight: bold;
`;
export const LargeText = styled.div` export const LargeText = styled.div`
font-size: large; font-size: large;
`; `;

View File

@ -13,7 +13,11 @@
You should have received a copy of the GNU General Public License along with 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/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
declare module "*.jpeg" { declare module "*.jpeg" {
const content: any;
export default content;
}
declare module "*.jpg" {
const content: any; const content: any;
export default content; export default content;
} }

View File

@ -330,9 +330,11 @@ function Application(): VNode {
const hash = location.hash.substring(1); const hash = location.hash.substring(1);
const found = document.getElementById(hash); const found = document.getElementById(hash);
if (found) { if (found) {
found.scrollIntoView({ setTimeout(() => {
block: "center", found.scrollIntoView({
}); block: "center",
});
}, 10);
} }
} }
}, []); }, []);

View File

@ -26,22 +26,27 @@ options.requestAnimationFrame = (fn: () => void) => {
export function createExample<Props>( export function createExample<Props>(
Component: FunctionalComponent<Props>, Component: FunctionalComponent<Props>,
props: Partial<Props>, props: Partial<Props> | (() => Partial<Props>),
): ComponentChildren { ): ComponentChildren {
//FIXME: props are evaluated on build time
// in some cases we want to evaluated the props on render time so we can get some relative timestamp
// check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props
const Render = (args: any): VNode => create(Component, args); const Render = (args: any): VNode => create(Component, args);
Render.args = props; Render.args = evaluatedProps;
return Render; return Render;
} }
export function createExampleWithCustomContext<Props, ContextProps>( export function createExampleWithCustomContext<Props, ContextProps>(
Component: FunctionalComponent<Props>, Component: FunctionalComponent<Props>,
props: Partial<Props>, props: Partial<Props> | (() => Partial<Props>),
ContextProvider: FunctionalComponent<ContextProps>, ContextProvider: FunctionalComponent<ContextProps>,
contextProps: Partial<ContextProps>, contextProps: Partial<ContextProps>,
): ComponentChildren { ): ComponentChildren {
const evaluatedProps = typeof props === "function" ? props() : props
const Render = (args: any): VNode => create(Component, args); const Render = (args: any): VNode => create(Component, args);
const WithContext = (args: any): VNode => create(ContextProvider, { ...contextProps, children: [Render(args)] } as any); const WithContext = (args: any): VNode => create(ContextProvider, { ...contextProps, children: [Render(args)] } as any);
WithContext.args = props WithContext.args = evaluatedProps
return WithContext return WithContext
} }

View File

@ -30,6 +30,7 @@ import {
TransactionTip, TransactionTip,
TransactionType, TransactionType,
TransactionWithdrawal, TransactionWithdrawal,
WithdrawalDetails,
WithdrawalType, WithdrawalType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { DevContextProviderForTesting } from "../context/devContext.js"; import { DevContextProviderForTesting } from "../context/devContext.js";
@ -57,6 +58,8 @@ const commonTransaction = {
transactionId: "12", transactionId: "12",
} as TransactionCommon; } as TransactionCommon;
import merchantIcon from "../../static-dev/merchant-icon-11.jpeg";
const exampleData = { const exampleData = {
withdraw: { withdraw: {
...commonTransaction, ...commonTransaction,
@ -65,27 +68,34 @@ const exampleData = {
withdrawalDetails: { withdrawalDetails: {
confirmed: false, confirmed: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
exchangePaytoUris: ["payto://x-taler-bank/bank/account"], exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"],
type: WithdrawalType.ManualTransfer, type: WithdrawalType.ManualTransfer,
}, },
} as TransactionWithdrawal, } as TransactionWithdrawal,
payment: { payment: {
...commonTransaction, ...commonTransaction,
amountEffective: "KUDOS:11", amountEffective: "KUDOS:12",
type: TransactionType.Payment, type: TransactionType.Payment,
info: { info: {
contractTermsHash: "ASDZXCASD", contractTermsHash: "ASDZXCASD",
merchant: { merchant: {
name: "the merchant", name: "the merchant",
logo: merchantIcon,
website: "https://www.themerchant.taler",
email: "contact@merchant.taler",
}, },
orderId: "2021.167-03NPY6MCYMVGT", orderId: "2021.167-03NPY6MCYMVGT",
products: [], products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth", summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "", fulfillmentMessage: "",
// delivery_date: { t_s: 1 },
// delivery_location: {
// address_lines: [""],
// },
}, },
refundPending: undefined, refundPending: undefined,
totalRefundEffective: "USD:0", totalRefundEffective: "KUDOS:0",
totalRefundRaw: "USD:0", totalRefundRaw: "KUDOS:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0", proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted, status: PaymentStatus.Accepted,
} as TransactionPayment, } as TransactionPayment,
@ -93,7 +103,7 @@ const exampleData = {
...commonTransaction, ...commonTransaction,
type: TransactionType.Deposit, type: TransactionType.Deposit,
depositGroupId: "#groupId", depositGroupId: "#groupId",
targetPaytoUri: "payto://x-taler-bank/bank/account", targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
} as TransactionDeposit, } as TransactionDeposit,
refresh: { refresh: {
...commonTransaction, ...commonTransaction,
@ -117,7 +127,7 @@ const exampleData = {
}, },
orderId: "2021.167-03NPY6MCYMVGT", orderId: "2021.167-03NPY6MCYMVGT",
products: [], products: [],
summary: "the summary", summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "", fulfillmentMessage: "",
}, },
refundPending: undefined, refundPending: undefined,
@ -143,20 +153,27 @@ export const Withdraw = createExample(TestedComponent, {
transaction: exampleData.withdraw, transaction: exampleData.withdraw,
}); });
export const WithdrawOneMinuteAgo = createExample(TestedComponent, { export const WithdrawFiveMinutesAgo = createExample(TestedComponent, () => ({
transaction: { transaction: {
...exampleData.withdraw, ...exampleData.withdraw,
timestamp: TalerProtocolTimestamp.fromSeconds(new Date().getTime() - 60), timestamp: TalerProtocolTimestamp.fromSeconds(
new Date().getTime() / 1000 - 60 * 5,
),
}, },
}); }));
export const WithdrawOneMinuteAgoAndPending = createExample(TestedComponent, { export const WithdrawFiveMinutesAgoAndPending = createExample(
transaction: { TestedComponent,
...exampleData.withdraw, () => ({
timestamp: TalerProtocolTimestamp.fromSeconds(new Date().getTime() - 60), transaction: {
pending: true, ...exampleData.withdraw,
}, timestamp: TalerProtocolTimestamp.fromSeconds(
}); new Date().getTime() / 1000 - 60 * 5,
),
pending: true,
},
}),
);
export const WithdrawError = createExample(TestedComponent, { export const WithdrawError = createExample(TestedComponent, {
transaction: { transaction: {
@ -177,17 +194,17 @@ export const WithdrawErrorInDevMode = createExampleInCustomContext(
{ value: true }, { value: true },
); );
export const WithdrawPendingManual = createExample(TestedComponent, { export const WithdrawPendingManual = createExample(TestedComponent, () => ({
transaction: { transaction: {
...exampleData.withdraw, ...exampleData.withdraw,
withdrawalDetails: { withdrawalDetails: {
type: WithdrawalType.ManualTransfer, type: WithdrawalType.ManualTransfer,
exchangePaytoUris: ["payto://iban/asdasdasd"], exchangePaytoUris: ["payto://iban/asdasdasd"],
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
}, } as WithdrawalDetails,
pending: true, pending: true,
}, },
}); }));
export const WithdrawPendingTalerBankUnconfirmed = createExample( export const WithdrawPendingTalerBankUnconfirmed = createExample(
TestedComponent, TestedComponent,
@ -231,10 +248,95 @@ export const PaymentError = createExample(TestedComponent, {
}, },
}); });
export const PaymentWithoutFee = createExample(TestedComponent, { export const PaymentWithRefund = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
totalRefundEffective: "KUDOS:1",
totalRefundRaw: "KUDOS:1",
},
});
export const PaymentWithDeliveryDate = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
info: {
...exampleData.payment.info,
delivery_date: {
t_s: new Date().getTime() / 1000,
},
},
},
});
export const PaymentWithDeliveryAddr = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
info: {
...exampleData.payment.info,
delivery_location: {
country: "Argentina",
street: "Elm Street",
district: "CABA",
post_code: "1101",
},
},
},
});
export const PaymentWithDeliveryFull = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
info: {
...exampleData.payment.info,
delivery_date: {
t_s: new Date().getTime() / 1000,
},
delivery_location: {
country: "Argentina",
street: "Elm Street",
district: "CABA",
post_code: "1101",
},
},
},
});
export const PaymentWithRefundPending = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
refundPending: "KUDOS:3",
totalRefundEffective: "KUDOS:1",
totalRefundRaw: "KUDOS:1",
},
});
export const PaymentWithFeeAndRefund = createExample(TestedComponent, {
transaction: { transaction: {
...exampleData.payment, ...exampleData.payment,
amountRaw: "KUDOS:11", amountRaw: "KUDOS:11",
totalRefundEffective: "KUDOS:1",
totalRefundRaw: "KUDOS:1",
},
});
export const PaymentWithFeeAndRefundFee = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:11",
totalRefundEffective: "KUDOS:1",
totalRefundRaw: "KUDOS:2",
},
});
export const PaymentWithoutFee = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:12",
}, },
}); });
@ -249,7 +351,7 @@ export const PaymentWithProducts = createExample(TestedComponent, {
...exampleData.payment, ...exampleData.payment,
info: { info: {
...exampleData.payment.info, ...exampleData.payment.info,
summary: "this order has 5 products", summary: "summary of 5 products",
products: [ products: [
{ {
description: "t-shirt", description: "t-shirt",
@ -360,20 +462,3 @@ export const RefundError = createExample(TestedComponent, {
export const RefundPending = createExample(TestedComponent, { export const RefundPending = createExample(TestedComponent, {
transaction: { ...exampleData.refund, pending: true }, transaction: { ...exampleData.refund, pending: true },
}); });
export const RefundWithProducts = createExample(TestedComponent, {
transaction: {
...exampleData.refund,
info: {
...exampleData.refund.info,
products: [
{
description: "t-shirt",
},
{
description: "beer",
},
],
},
} as TransactionRefund,
});

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB