add age restriction option to withdraw cta

This commit is contained in:
Sebastian 2022-05-04 16:25:53 -03:00
parent 4491118494
commit 7a2fe8018f
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
11 changed files with 167 additions and 25 deletions

View File

@ -17,8 +17,8 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
interface Props { interface Props {
enabled: boolean; enabled?: boolean;
onToggle: () => void; onToggle?: () => void;
label: VNode; label: VNode;
name: string; name: string;
description?: VNode; description?: VNode;

View File

@ -20,7 +20,7 @@ import { NiceSelect } from "./styled/index.js";
interface Props { interface Props {
value?: string; value?: string;
onChange: (s: string) => void; onChange?: (s: string) => void;
label: VNode; label: VNode;
list: { list: {
[label: string]: string; [label: string]: string;
@ -28,6 +28,7 @@ interface Props {
name: string; name: string;
description?: string; description?: string;
canBeNull?: boolean; canBeNull?: boolean;
maxWidth?: boolean;
} }
export function SelectList({ export function SelectList({
@ -36,6 +37,7 @@ export function SelectList({
list, list,
onChange, onChange,
label, label,
maxWidth,
description, description,
canBeNull, canBeNull,
}: Props): VNode { }: Props): VNode {
@ -53,8 +55,9 @@ export function SelectList({
<select <select
name={name} name={name}
value={value} value={value}
style={maxWidth ? { width: "100%" } : undefined}
onChange={(e) => { onChange={(e) => {
onChange(e.currentTarget.value); if (onChange) onChange(e.currentTarget.value);
}} }}
> >
{value === undefined || {value === undefined ||

View File

@ -101,6 +101,42 @@ export const NoEnoughBalance = createExample(TestedComponent, {
goToWalletManualWithdraw: () => null, goToWalletManualWithdraw: () => null,
}); });
export const EnoughBalanceButRestricted = createExample(TestedComponent, {
state: {
status: "ready",
hook: undefined,
amount: Amounts.parseOrThrow("USD:10"),
balance: {
currency: "USD",
fraction: 40000000,
value: 19,
},
payHandler: {
onClick: async () => {
null;
},
},
totalFees: Amounts.parseOrThrow("USD:0"),
payResult: undefined,
uri: "",
payStatus: {
status: PreparePayResultType.InsufficientBalance,
noncePriv: "",
proposalId: "proposal1234",
contractTerms: {
merchant: {
name: "someone",
},
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
amountRaw: "USD:10",
},
},
goBack: () => null,
goToWalletManualWithdraw: () => null,
});
export const PaymentPossible = createExample(TestedComponent, { export const PaymentPossible = createExample(TestedComponent, {
state: { state: {
status: "ready", status: "ready",

View File

@ -542,23 +542,22 @@ function ButtonsSection({
); );
} }
if (payStatus.status === PreparePayResultType.InsufficientBalance) { if (payStatus.status === PreparePayResultType.InsufficientBalance) {
let BalanceMessage = "";
if (!state.balance) {
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
} else {
const balanceShouldBeEnough =
Amounts.cmp(state.balance, state.amount) !== -1;
if (balanceShouldBeEnough) {
BalanceMessage = i18n.str`Could not find enough coins to pay this order. Even if you have enough ${state.balance.currency} some restriction may apply.`;
} else {
BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
}
}
return ( return (
<Fragment> <Fragment>
<section> <section>
{state.balance ? ( <WarningBox>{BalanceMessage}</WarningBox>
<WarningBox>
<i18n.Translate>
Your balance of {<Amount value={state.balance} />} is not
enough to pay for this purchase
</i18n.Translate>
</WarningBox>
) : (
<WarningBox>
<i18n.Translate>
Your balance is not enough to pay for this purchase.
</i18n.Translate>
</WarningBox>
)}
</section> </section>
<section> <section>
<ButtonSuccess <ButtonSuccess

View File

@ -50,12 +50,24 @@ const normalTosState = {
reviewing: false, reviewing: false,
}; };
const ageRestrictionOptions: Record<string, string> = "6:12:18"
.split(":")
.reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {});
ageRestrictionOptions["0"] = "Not restricted";
const ageRestrictionSelectField = {
list: ageRestrictionOptions,
value: "0",
};
export const TermsOfServiceNotYetLoaded = createExample(TestedComponent, { export const TermsOfServiceNotYetLoaded = createExample(TestedComponent, {
state: { state: {
hook: undefined, hook: undefined,
status: "success", status: "success",
cancelEditExchange: nullHandler, cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler, confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: { chosenAmount: {
currency: "USD", currency: "USD",
value: 2, value: 2,
@ -91,6 +103,7 @@ export const WithSomeFee = createExample(TestedComponent, {
status: "success", status: "success",
cancelEditExchange: nullHandler, cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler, confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: { chosenAmount: {
currency: "USD", currency: "USD",
value: 2, value: 2,
@ -127,6 +140,7 @@ export const WithoutFee = createExample(TestedComponent, {
status: "success", status: "success",
cancelEditExchange: nullHandler, cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler, confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: { chosenAmount: {
currency: "USD", currency: "USD",
value: 2, value: 2,
@ -163,6 +177,7 @@ export const EditExchangeUntouched = createExample(TestedComponent, {
status: "success", status: "success",
cancelEditExchange: nullHandler, cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler, confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: { chosenAmount: {
currency: "USD", currency: "USD",
value: 2, value: 2,
@ -199,6 +214,7 @@ export const EditExchangeModified = createExample(TestedComponent, {
status: "success", status: "success",
cancelEditExchange: nullHandler, cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler, confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: { chosenAmount: {
currency: "USD", currency: "USD",
value: 2, value: 2,
@ -236,3 +252,40 @@ export const CompletedWithoutBankURL = createExample(TestedComponent, {
hook: undefined, hook: undefined,
}, },
}); });
export const WithAgeRestrictionSelected = createExample(TestedComponent, {
state: {
hook: undefined,
status: "success",
cancelEditExchange: nullHandler,
confirmEditExchange: nullHandler,
ageRestriction: ageRestrictionSelectField,
chosenAmount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
doWithdrawal: nullHandler,
editExchange: nullHandler,
exchange: {
list: exchangeList,
value: "exchange.demo.taler.net",
onChange: async () => {
null;
},
},
showExchangeSelection: false,
mustAcceptFirst: false,
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
toBeReceived: {
currency: "USD",
fraction: 0,
value: 2,
},
tosProps: normalTosState,
},
});

View File

@ -35,6 +35,7 @@ import { SelectList } from "../components/SelectList.js";
import { import {
ButtonSuccess, ButtonSuccess,
ButtonWarning, ButtonWarning,
Input,
LinkSuccess, LinkSuccess,
SubTitle, SubTitle,
SuccessBox, SuccessBox,
@ -43,12 +44,18 @@ import {
import { useTranslationContext } from "../context/translation.js"; import { useTranslationContext } from "../context/translation.js";
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { buildTermsOfServiceState } from "../utils/index.js"; import { buildTermsOfServiceState } from "../utils/index.js";
import { ButtonHandler, SelectFieldHandler } from "../mui/handlers.js"; import {
ButtonHandler,
SelectFieldHandler,
ToggleHandler,
} from "../mui/handlers.js";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
import { import {
Props as TermsOfServiceSectionProps, Props as TermsOfServiceSectionProps,
TermsOfServiceSection, TermsOfServiceSection,
} from "./TermsOfServiceSection.js"; } from "./TermsOfServiceSection.js";
import { startOfWeekYear } from "date-fns/esm";
import { Checkbox } from "../components/Checkbox.js";
interface Props { interface Props {
talerWithdrawUri?: string; talerWithdrawUri?: string;
@ -97,6 +104,8 @@ type Success = {
doWithdrawal: ButtonHandler; doWithdrawal: ButtonHandler;
tosProps?: TermsOfServiceSectionProps; tosProps?: TermsOfServiceSectionProps;
mustAcceptFirst: boolean; mustAcceptFirst: boolean;
ageRestriction: SelectFieldHandler;
}; };
export function useComponentState( export function useComponentState(
@ -106,6 +115,7 @@ export function useComponentState(
const [customExchange, setCustomExchange] = useState<string | undefined>( const [customExchange, setCustomExchange] = useState<string | undefined>(
undefined, undefined,
); );
const [ageRestricted, setAgeRestricted] = useState(0);
/** /**
* Ask the wallet about the withdraw URI * Ask the wallet about the withdraw URI
@ -228,6 +238,7 @@ export function useComponentState(
const res = await api.acceptWithdrawal( const res = await api.acceptWithdrawal(
talerWithdrawUri, talerWithdrawUri,
selectedExchange, selectedExchange,
!ageRestricted ? undefined : ageRestricted,
); );
if (res.confirmTransferUrl) { if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl; document.location.href = res.confirmTransferUrl;
@ -320,6 +331,14 @@ export function useComponentState(
termsState !== undefined && termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new"); (termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions: Record<string, string> | undefined = "6:12:18"
.split(":")
.reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {});
if (ageRestrictionOptions) {
ageRestrictionOptions["0"] = "Not restricted";
}
return { return {
status: "success", status: "success",
hook: undefined, hook: undefined,
@ -331,6 +350,11 @@ export function useComponentState(
toBeReceived, toBeReceived,
withdrawalFee, withdrawalFee,
chosenAmount: amount, chosenAmount: amount,
ageRestriction: {
list: ageRestrictionOptions,
value: String(ageRestricted),
onChange: async (v) => setAgeRestricted(parseInt(v, 10)),
},
doWithdrawal: { doWithdrawal: {
onClick: onClick:
doingWithdraw || (mustAcceptFirst && !reviewed) doingWithdraw || (mustAcceptFirst && !reviewed)
@ -486,6 +510,18 @@ export function View({ state }: { state: State }): VNode {
</LinkSuccess> </LinkSuccess>
)} )}
</section> </section>
<section>
<Input>
<SelectList
label={<i18n.Translate>Age restriction</i18n.Translate>}
list={state.ageRestriction.list}
name="age"
maxWidth
value={state.ageRestriction.value}
onChange={state.ageRestriction.onChange}
/>
</Input>
</section>
{state.tosProps && <TermsOfServiceSection {...state.tosProps} />} {state.tosProps && <TermsOfServiceSection {...state.tosProps} />}
{state.tosProps ? ( {state.tosProps ? (
<section> <section>

View File

@ -17,7 +17,7 @@ export interface ToggleHandler {
} }
export interface SelectFieldHandler { export interface SelectFieldHandler {
onChange: (value: string) => Promise<void>; onChange?: (value: string) => Promise<void>;
error?: string; error?: string;
value: string; value: string;
isDirty?: boolean; isDirty?: boolean;

View File

@ -87,7 +87,7 @@ describe("CreateManualWithdraw states", () => {
const { exchange, currency } = getLastResultOrThrow() const { exchange, currency } = getLastResultOrThrow()
expect(exchange.value).equal("url2") expect(exchange.value).equal("url2")
if (currency.onChange === undefined) expect.fail();
currency.onChange("USD") currency.onChange("USD")
} }
@ -111,6 +111,7 @@ describe("CreateManualWithdraw states", () => {
expect(exchange.value).equal("url2") expect(exchange.value).equal("url2")
expect(currency.value).equal("ARS") expect(currency.value).equal("ARS")
if (exchange.onChange === undefined) expect.fail();
exchange.onChange("url1") exchange.onChange("url1")
} }
@ -205,6 +206,7 @@ async function defaultTestForInputSelect(awaiter: () => Promise<void>, getField:
throw new Error('no enough values') throw new Error('no enough values')
} }
nextValue = keys[nextIdx] nextValue = keys[nextIdx]
if (field.onChange === undefined) expect.fail();
field.onChange(nextValue) field.onChange(nextValue)
} }

View File

@ -258,6 +258,7 @@ describe("DepositPage states", () => {
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
if (r.account.onChange === undefined) expect.fail();
r.account.onChange("1") r.account.onChange("1")
} }
@ -290,6 +291,7 @@ describe("DepositPage states", () => {
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`))
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
if (r.account.onChange === undefined) expect.fail();
r.account.onChange("0") r.account.onChange("0")
} }

View File

@ -81,6 +81,7 @@ export function DeveloperPage(): VNode {
type CoinsInfo = CoinDumpJson["coins"]; type CoinsInfo = CoinDumpJson["coins"];
type CalculatedCoinfInfo = { type CalculatedCoinfInfo = {
ageKeysCount: number | undefined;
denom_value: number; denom_value: number;
remain_value: number; remain_value: number;
status: string; status: string;
@ -132,11 +133,13 @@ export function View({
const money_by_exchange = coins.reduce( const money_by_exchange = coins.reduce(
(prev, cur) => { (prev, cur) => {
const denom = Amounts.parseOrThrow(cur.denom_value); const denom = Amounts.parseOrThrow(cur.denom_value);
console.log(cur);
if (!prev[cur.exchange_base_url]) { if (!prev[cur.exchange_base_url]) {
prev[cur.exchange_base_url] = []; prev[cur.exchange_base_url] = [];
currencies[cur.exchange_base_url] = denom.currency; currencies[cur.exchange_base_url] = denom.currency;
} }
prev[cur.exchange_base_url].push({ prev[cur.exchange_base_url].push({
ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
denom_value: parseFloat(Amounts.stringifyValue(denom)), denom_value: parseFloat(Amounts.stringifyValue(denom)),
remain_value: parseFloat( remain_value: parseFloat(
Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)), Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
@ -305,7 +308,7 @@ function ShowAllCoins({
<p> <p>
<b>{ex}</b>: {total} {currencies[ex]} <b>{ex}</b>: {total} {currencies[ex]}
</p> </p>
<p> <p onClick={() => setCollapsedUnspent(true)}>
<b> <b>
<i18n.Translate>usable coins</i18n.Translate> <i18n.Translate>usable coins</i18n.Translate>
</b> </b>
@ -313,7 +316,7 @@ function ShowAllCoins({
{collapsedUnspent ? ( {collapsedUnspent ? (
<div onClick={() => setCollapsedUnspent(false)}>click to show</div> <div onClick={() => setCollapsedUnspent(false)}>click to show</div>
) : ( ) : (
<table onClick={() => setCollapsedUnspent(true)}> <table>
<tr> <tr>
<td> <td>
<i18n.Translate>id</i18n.Translate> <i18n.Translate>id</i18n.Translate>
@ -330,6 +333,9 @@ function ShowAllCoins({
<td> <td>
<i18n.Translate>from refresh?</i18n.Translate> <i18n.Translate>from refresh?</i18n.Translate>
</td> </td>
<td>
<i18n.Translate>age key count</i18n.Translate>
</td>
</tr> </tr>
{coins.usable.map((c, idx) => { {coins.usable.map((c, idx) => {
return ( return (
@ -339,12 +345,13 @@ function ShowAllCoins({
<td>{c.remain_value}</td> <td>{c.remain_value}</td>
<td>{c.status}</td> <td>{c.status}</td>
<td>{c.from_refresh ? "true" : "false"}</td> <td>{c.from_refresh ? "true" : "false"}</td>
<td>{String(c.ageKeysCount)}</td>
</tr> </tr>
); );
})} })}
</table> </table>
)} )}
<p> <p onClick={() => setCollapsedSpent(true)}>
<i18n.Translate>spent coins</i18n.Translate> <i18n.Translate>spent coins</i18n.Translate>
</p> </p>
{collapsedSpent ? ( {collapsedSpent ? (
@ -352,7 +359,7 @@ function ShowAllCoins({
<i18n.Translate>click to show</i18n.Translate> <i18n.Translate>click to show</i18n.Translate>
</div> </div>
) : ( ) : (
<table onClick={() => setCollapsedSpent(true)}> <table>
<tr> <tr>
<td> <td>
<i18n.Translate>id</i18n.Translate> <i18n.Translate>id</i18n.Translate>

View File

@ -324,10 +324,12 @@ export function preparePay(talerPayUri: string): Promise<PreparePayResult> {
export function acceptWithdrawal( export function acceptWithdrawal(
talerWithdrawUri: string, talerWithdrawUri: string,
selectedExchange: string, selectedExchange: string,
restrictAge?: number,
): Promise<AcceptWithdrawalResponse> { ): Promise<AcceptWithdrawalResponse> {
return callBackend("acceptBankIntegratedWithdrawal", { return callBackend("acceptBankIntegratedWithdrawal", {
talerWithdrawUri, talerWithdrawUri,
exchangeBaseUrl: selectedExchange, exchangeBaseUrl: selectedExchange,
restrictAge
}); });
} }
@ -340,10 +342,12 @@ export function acceptWithdrawal(
export function acceptManualWithdrawal( export function acceptManualWithdrawal(
exchangeBaseUrl: string, exchangeBaseUrl: string,
amount: string, amount: string,
restrictAge?: number,
): Promise<AcceptManualWithdrawalResult> { ): Promise<AcceptManualWithdrawalResult> {
return callBackend("acceptManualWithdrawal", { return callBackend("acceptManualWithdrawal", {
amount, amount,
exchangeBaseUrl, exchangeBaseUrl,
restrictAge
}); });
} }