handle kyc error on invoice and transfer
This commit is contained in:
parent
f414ca39e4
commit
efbde0e160
@ -62,7 +62,7 @@ export enum NotificationType {
|
|||||||
PendingOperationProcessed = "pending-operation-processed",
|
PendingOperationProcessed = "pending-operation-processed",
|
||||||
ProposalRefused = "proposal-refused",
|
ProposalRefused = "proposal-refused",
|
||||||
ReserveRegisteredWithBank = "reserve-registered-with-bank",
|
ReserveRegisteredWithBank = "reserve-registered-with-bank",
|
||||||
WithdrawalGroupKycRequested = "withdrawal-group-kyc-requested",
|
KycRequested = "kyc-requested",
|
||||||
WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
|
WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
|
||||||
WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
|
WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
|
||||||
PeerPullCreditReady = "peer-pull-credit-ready",
|
PeerPullCreditReady = "peer-pull-credit-ready",
|
||||||
@ -125,8 +125,8 @@ export interface RefreshMeltedNotification {
|
|||||||
type: NotificationType.RefreshMelted;
|
type: NotificationType.RefreshMelted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawalGroupKycRequested {
|
export interface KycRequestedNotification {
|
||||||
type: NotificationType.WithdrawalGroupKycRequested;
|
type: NotificationType.KycRequested;
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
kycUrl: string;
|
kycUrl: string;
|
||||||
}
|
}
|
||||||
@ -324,7 +324,7 @@ export type WalletNotification =
|
|||||||
| ReserveRegisteredWithBankNotification
|
| ReserveRegisteredWithBankNotification
|
||||||
| ReserveNotYetFoundNotification
|
| ReserveNotYetFoundNotification
|
||||||
| PayOperationSuccessNotification
|
| PayOperationSuccessNotification
|
||||||
| WithdrawalGroupKycRequested
|
| KycRequestedNotification
|
||||||
| WithdrawalGroupBankConfirmed
|
| WithdrawalGroupBankConfirmed
|
||||||
| WithdrawalGroupReserveReadyNotification
|
| WithdrawalGroupReserveReadyNotification
|
||||||
| PeerPullCreditReadyNotification;
|
| PeerPullCreditReadyNotification;
|
||||||
|
@ -1781,6 +1781,7 @@ export enum PeerPullPaymentInitiationStatus {
|
|||||||
* invoice and deposit money into it.
|
* invoice and deposit money into it.
|
||||||
*/
|
*/
|
||||||
PurseCreated = 11 /* ACTIVE_START + 1 */,
|
PurseCreated = 11 /* ACTIVE_START + 1 */,
|
||||||
|
KycRequired = 12 /* ACTIVE_START + 2 */,
|
||||||
PurseDeposited = 50 /* DORMANT_START */,
|
PurseDeposited = 50 /* DORMANT_START */,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1831,12 +1832,15 @@ export interface PeerPullPaymentInitiationRecord {
|
|||||||
*/
|
*/
|
||||||
status: PeerPullPaymentInitiationStatus;
|
status: PeerPullPaymentInitiationStatus;
|
||||||
|
|
||||||
|
kycInfo?: KycPendingInfo;
|
||||||
|
|
||||||
withdrawalGroupId: string | undefined;
|
withdrawalGroupId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PeerPushPaymentIncomingStatus {
|
export enum PeerPushPaymentIncomingStatus {
|
||||||
Proposed = 30 /* USER_ATTENTION_START */,
|
Proposed = 30 /* USER_ATTENTION_START */,
|
||||||
Accepted = 10 /* ACTIVE_START */,
|
Accepted = 10 /* ACTIVE_START */,
|
||||||
|
KycRequired = 11 /* ACTIVE_START + 1 */,
|
||||||
/**
|
/**
|
||||||
* Merge was successful and withdrawal group has been created, now
|
* Merge was successful and withdrawal group has been created, now
|
||||||
* everything is in the hand of the withdrawal group.
|
* everything is in the hand of the withdrawal group.
|
||||||
@ -1887,6 +1891,8 @@ export interface PeerPushPaymentIncomingRecord {
|
|||||||
* with older (ver_minor<4) DB versions.
|
* with older (ver_minor<4) DB versions.
|
||||||
*/
|
*/
|
||||||
currency: string | undefined;
|
currency: string | undefined;
|
||||||
|
|
||||||
|
kycInfo?: KycPendingInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PeerPullPaymentIncomingStatus {
|
export enum PeerPullPaymentIncomingStatus {
|
||||||
|
@ -73,10 +73,15 @@ import {
|
|||||||
codecForTimestamp,
|
codecForTimestamp,
|
||||||
CancellationToken,
|
CancellationToken,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
HttpStatusCode,
|
||||||
|
codecForWalletKycUuid,
|
||||||
|
WalletKycUuid,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
|
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
|
KycPendingInfo,
|
||||||
|
KycUserType,
|
||||||
OperationStatus,
|
OperationStatus,
|
||||||
PeerPullPaymentIncomingStatus,
|
PeerPullPaymentIncomingStatus,
|
||||||
PeerPullPaymentInitiationRecord,
|
PeerPullPaymentInitiationRecord,
|
||||||
@ -115,6 +120,7 @@ import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
|
|||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import { getTotalRefreshCost } from "./refresh.js";
|
import { getTotalRefreshCost } from "./refresh.js";
|
||||||
import {
|
import {
|
||||||
|
checkWithdrawalKycStatus,
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
internalCreateWithdrawalGroup,
|
internalCreateWithdrawalGroup,
|
||||||
processWithdrawalGroup,
|
processWithdrawalGroup,
|
||||||
@ -866,6 +872,23 @@ export async function processPeerPushCredit(
|
|||||||
|
|
||||||
const amount = Amounts.parseOrThrow(contractTerms.amount);
|
const amount = Amounts.parseOrThrow(contractTerms.amount);
|
||||||
|
|
||||||
|
if (
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.KycRequired &&
|
||||||
|
peerInc.kycInfo
|
||||||
|
) {
|
||||||
|
const txId = makeTransactionId(
|
||||||
|
TransactionType.PeerPushCredit,
|
||||||
|
peerInc.peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
await checkWithdrawalKycStatus(
|
||||||
|
ws,
|
||||||
|
peerInc.exchangeBaseUrl,
|
||||||
|
txId,
|
||||||
|
peerInc.kycInfo,
|
||||||
|
"individual",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
const mergeReserveInfo = await getMergeReserveInfo(ws, {
|
||||||
exchangeBaseUrl: peerInc.exchangeBaseUrl,
|
exchangeBaseUrl: peerInc.exchangeBaseUrl,
|
||||||
});
|
});
|
||||||
@ -902,10 +925,40 @@ export async function processPeerPushCredit(
|
|||||||
reserve_sig: sigRes.accountSig,
|
reserve_sig: sigRes.accountSig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
||||||
|
|
||||||
|
if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const respJson = await mergeHttpResp.json();
|
||||||
|
const kycPending = codecForWalletKycUuid().decode(respJson);
|
||||||
|
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPushPaymentIncoming])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const peerInc = await tx.peerPushPaymentIncoming.get(
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
|
);
|
||||||
|
if (!peerInc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peerInc.kycInfo = {
|
||||||
|
paytoHash: kycPending.h_payto,
|
||||||
|
requirementRow: kycPending.requirement_row,
|
||||||
|
};
|
||||||
|
peerInc.status = PeerPushPaymentIncomingStatus.KycRequired;
|
||||||
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Pending,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace(`merge request: ${j2s(mergeReq)}`);
|
logger.trace(`merge request: ${j2s(mergeReq)}`);
|
||||||
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
const res = await readSuccessResponseJsonOrThrow(
|
||||||
|
mergeHttpResp,
|
||||||
|
codecForAny(),
|
||||||
|
);
|
||||||
logger.trace(`merge response: ${j2s(res)}`);
|
logger.trace(`merge response: ${j2s(res)}`);
|
||||||
|
|
||||||
await internalCreateWithdrawalGroup(ws, {
|
await internalCreateWithdrawalGroup(ws, {
|
||||||
@ -932,7 +985,10 @@ export async function processPeerPushCredit(
|
|||||||
if (!peerInc) {
|
if (!peerInc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (peerInc.status === PeerPushPaymentIncomingStatus.Accepted) {
|
if (
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.Accepted ||
|
||||||
|
peerInc.status === PeerPushPaymentIncomingStatus.KycRequired
|
||||||
|
) {
|
||||||
peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated;
|
peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated;
|
||||||
}
|
}
|
||||||
await tx.peerPushPaymentIncoming.put(peerInc);
|
await tx.peerPushPaymentIncoming.put(peerInc);
|
||||||
@ -1423,6 +1479,22 @@ export async function processPeerPullCredit(
|
|||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Longpoll,
|
type: OperationAttemptResultType.Longpoll,
|
||||||
};
|
};
|
||||||
|
case PeerPullPaymentInitiationStatus.KycRequired: {
|
||||||
|
if (pullIni.kycInfo) {
|
||||||
|
const txId = makeTransactionId(
|
||||||
|
TransactionType.PeerPullCredit,
|
||||||
|
pullIni.pursePub,
|
||||||
|
);
|
||||||
|
await checkWithdrawalKycStatus(
|
||||||
|
ws,
|
||||||
|
pullIni.exchangeBaseUrl,
|
||||||
|
txId,
|
||||||
|
pullIni.kycInfo,
|
||||||
|
"individual",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case PeerPullPaymentInitiationStatus.Initial:
|
case PeerPullPaymentInitiationStatus.Initial:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1496,6 +1568,31 @@ export async function processPeerPullCredit(
|
|||||||
reservePurseReqBody,
|
reservePurseReqBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
|
const respJson = await httpResp.json();
|
||||||
|
const kycPending = codecForWalletKycUuid().decode(respJson);
|
||||||
|
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const peerIni = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||||
|
if (!peerIni) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peerIni.kycInfo = {
|
||||||
|
paytoHash: kycPending.h_payto,
|
||||||
|
requirementRow: kycPending.requirement_row,
|
||||||
|
};
|
||||||
|
peerIni.status = PeerPullPaymentInitiationStatus.KycRequired;
|
||||||
|
await tx.peerPullPaymentInitiations.put(peerIni);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Pending,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
|
||||||
|
|
||||||
logger.info(`reserve merge response: ${j2s(resp)}`);
|
logger.info(`reserve merge response: ${j2s(resp)}`);
|
||||||
|
@ -116,9 +116,7 @@ import {
|
|||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
} from "../versions.js";
|
} from "../versions.js";
|
||||||
import {
|
import { makeTransactionId } from "./common.js";
|
||||||
makeTransactionId,
|
|
||||||
} from "./common.js";
|
|
||||||
import {
|
import {
|
||||||
getExchangeDetails,
|
getExchangeDetails,
|
||||||
getExchangePaytoUri,
|
getExchangePaytoUri,
|
||||||
@ -1226,9 +1224,14 @@ export async function processWithdrawalGroup(
|
|||||||
|
|
||||||
if (numKycRequired > 0) {
|
if (numKycRequired > 0) {
|
||||||
if (kycInfo) {
|
if (kycInfo) {
|
||||||
|
const txId = makeTransactionId(
|
||||||
|
TransactionType.Withdrawal,
|
||||||
|
withdrawalGroup.withdrawalGroupId,
|
||||||
|
);
|
||||||
await checkWithdrawalKycStatus(
|
await checkWithdrawalKycStatus(
|
||||||
ws,
|
ws,
|
||||||
withdrawalGroup,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
|
txId,
|
||||||
kycInfo,
|
kycInfo,
|
||||||
"individual",
|
"individual",
|
||||||
);
|
);
|
||||||
@ -1271,42 +1274,44 @@ export async function processWithdrawalGroup(
|
|||||||
|
|
||||||
export async function checkWithdrawalKycStatus(
|
export async function checkWithdrawalKycStatus(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
wg: WithdrawalGroupRecord,
|
exchangeUrl: string,
|
||||||
|
txId: string,
|
||||||
kycInfo: KycPendingInfo,
|
kycInfo: KycPendingInfo,
|
||||||
userType: KycUserType,
|
userType: KycUserType,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const exchangeUrl = wg.exchangeBaseUrl;
|
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
|
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
|
||||||
exchangeUrl,
|
exchangeUrl,
|
||||||
);
|
);
|
||||||
logger.info(`kyc url ${url.href}`);
|
logger.info(`kyc url ${url.href}`);
|
||||||
const kycStatusReq = await ws.http.fetch(url.href, {
|
const kycStatusRes = await ws.http.fetch(url.href, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
if (kycStatusReq.status === HttpStatusCode.Ok) {
|
if (
|
||||||
|
kycStatusRes.status === HttpStatusCode.Ok ||
|
||||||
|
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
|
||||||
|
// remove after the exchange is fixed or clarified
|
||||||
|
kycStatusRes.status === HttpStatusCode.NoContent
|
||||||
|
) {
|
||||||
logger.warn("kyc requested, but already fulfilled");
|
logger.warn("kyc requested, but already fulfilled");
|
||||||
return;
|
return;
|
||||||
} else if (kycStatusReq.status === HttpStatusCode.Accepted) {
|
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
|
||||||
const kycStatus = await kycStatusReq.json();
|
const kycStatus = await kycStatusRes.json();
|
||||||
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.WithdrawalGroupKycRequested,
|
type: NotificationType.KycRequested,
|
||||||
kycUrl: kycStatus.kyc_url,
|
kycUrl: kycStatus.kyc_url,
|
||||||
transactionId: makeTransactionId(
|
transactionId: txId,
|
||||||
TransactionType.Withdrawal,
|
|
||||||
wg.withdrawalGroupId,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
|
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, //FIXME: another error code or rename for merge
|
||||||
{
|
{
|
||||||
kycUrl: kycStatus.kyc_url,
|
kycUrl: kycStatus.kyc_url,
|
||||||
},
|
},
|
||||||
`KYC check required for withdrawal`,
|
`KYC check required for transfer`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
|
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,12 +140,19 @@ export function View({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const api = useBackendContext();
|
const api = useBackendContext();
|
||||||
|
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
async function onImportDatabase(str: string): Promise<void> {
|
async function onImportDatabase(str: string): Promise<void> {
|
||||||
return api.wallet.call(WalletApiOperation.ImportDb, {
|
return api.wallet.call(WalletApiOperation.ImportDb, {
|
||||||
dump: JSON.parse(str),
|
dump: JSON.parse(str),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hook = useAsyncAsHook(() =>
|
||||||
|
api.wallet.call(WalletApiOperation.ListExchanges, {}),
|
||||||
|
);
|
||||||
|
const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];
|
||||||
|
|
||||||
const currencies: { [ex: string]: string } = {};
|
const currencies: { [ex: string]: string } = {};
|
||||||
const money_by_exchange = coins.reduce(
|
const money_by_exchange = coins.reduce(
|
||||||
(prev, cur) => {
|
(prev, cur) => {
|
||||||
@ -171,7 +178,6 @@ export function View({
|
|||||||
[exchange_name: string]: CalculatedCoinfInfo[];
|
[exchange_name: string]: CalculatedCoinfInfo[];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const exchanges = Object.keys(money_by_exchange);
|
|
||||||
|
|
||||||
const [tagName, setTagName] = useState("");
|
const [tagName, setTagName] = useState("");
|
||||||
const [logLevel, setLogLevel] = useState("info");
|
const [logLevel, setLogLevel] = useState("info");
|
||||||
@ -324,27 +330,28 @@ export function View({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
exchanges.map(async (ex) => {
|
exchangeList.map(async (exchange) => {
|
||||||
|
const url = exchange.exchangeBaseUrl;
|
||||||
const oldKeys = JSON.stringify(
|
const oldKeys = JSON.stringify(
|
||||||
await (await fetch(`${ex}keys`)).json(),
|
await (await fetch(`${url}keys`)).json(),
|
||||||
);
|
);
|
||||||
const oldWire = JSON.stringify(
|
const oldWire = JSON.stringify(
|
||||||
await (await fetch(`${ex}wire`)).json(),
|
await (await fetch(`${url}wire`)).json(),
|
||||||
);
|
);
|
||||||
const newKeys = JSON.stringify(
|
const newKeys = JSON.stringify(
|
||||||
await (
|
await (
|
||||||
await fetch(`${ex}keys`, { cache: "no-cache" })
|
await fetch(`${url}keys`, { cache: "no-cache" })
|
||||||
).json(),
|
).json(),
|
||||||
);
|
);
|
||||||
const newWire = JSON.stringify(
|
const newWire = JSON.stringify(
|
||||||
await (
|
await (
|
||||||
await fetch(`${ex}wire`, { cache: "no-cache" })
|
await fetch(`${url}wire`, { cache: "no-cache" })
|
||||||
).json(),
|
).json(),
|
||||||
);
|
);
|
||||||
return oldKeys !== newKeys || newWire !== oldWire;
|
return oldKeys !== newKeys || newWire !== oldWire;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const ex = exchanges.filter((e, i) => result[i]);
|
const ex = exchangeList.filter((e, i) => result[i]);
|
||||||
if (!ex.length) {
|
if (!ex.length) {
|
||||||
alert("no exchange was outdated");
|
alert("no exchange was outdated");
|
||||||
} else {
|
} else {
|
||||||
|
@ -879,13 +879,14 @@ export function TransactionView({
|
|||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
{transaction.extendedStatus ===
|
{transaction.extendedStatus ===
|
||||||
ExtendedStatus.Pending /** pending is not-pay */ && (
|
ExtendedStatus.Pending /** pending is not-pay */ &&
|
||||||
<Part
|
!transaction.error && (
|
||||||
title={i18n.str`URI`}
|
<Part
|
||||||
text={<ShowQrWithCopy text={transaction.talerUri} />}
|
title={i18n.str`URI`}
|
||||||
kind="neutral"
|
text={<ShowQrWithCopy text={transaction.talerUri} />}
|
||||||
/>
|
kind="neutral"
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
<Part
|
<Part
|
||||||
title={i18n.str`Details`}
|
title={i18n.str`Details`}
|
||||||
text={
|
text={
|
||||||
|
Loading…
Reference in New Issue
Block a user