implement fulfillment_message and make fulfillment_url optional
This commit is contained in:
parent
69c4950762
commit
0e88ef9bd2
@ -349,7 +349,7 @@ export class GlobalTestState {
|
||||
args: string[],
|
||||
logName: string,
|
||||
): ProcessWrapper {
|
||||
console.log(`spawning process (${command})`);
|
||||
console.log(`spawning process ${command} with arguments ${args})`);
|
||||
const proc = spawn(command, args, {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
@ -79,3 +79,12 @@ export function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
|
||||
.fetch(...values);
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an internationalized string (based on the globally set, current language)
|
||||
* from a JSON object. Fall back to the default language of the JSON object
|
||||
* if no match exists.
|
||||
*/
|
||||
export function getJsonI18n<K extends string>(obj: Record<K, string>, key: K): string {
|
||||
return obj[key];
|
||||
}
|
@ -513,17 +513,6 @@ async function recordConfirmPay(
|
||||
return t;
|
||||
}
|
||||
|
||||
function getNextUrl(contractData: WalletContractData): string {
|
||||
const f = contractData.fulfillmentUrl;
|
||||
if (f.startsWith("http://") || f.startsWith("https://")) {
|
||||
const fu = new URL(contractData.fulfillmentUrl);
|
||||
fu.searchParams.set("order_id", contractData.orderId);
|
||||
return fu.href;
|
||||
} else {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
async function incrementProposalRetry(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
@ -642,7 +631,10 @@ async function processDownloadProposalImpl(
|
||||
const httpResponse = await ws.http.postJson(orderClaimUrl, requestBody, {
|
||||
timeout: getProposalRequestTimeout(proposal),
|
||||
});
|
||||
const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codecForProposal());
|
||||
const r = await readSuccessResponseJsonOrErrorCode(
|
||||
httpResponse,
|
||||
codecForProposal(),
|
||||
);
|
||||
if (r.isError) {
|
||||
switch (r.talerErrorResponse.code) {
|
||||
case TalerErrorCode.ORDERS_ALREADY_CLAIMED:
|
||||
@ -652,7 +644,8 @@ async function processDownloadProposalImpl(
|
||||
{
|
||||
orderId: proposal.orderId,
|
||||
claimUrl: orderClaimUrl,
|
||||
});
|
||||
},
|
||||
);
|
||||
default:
|
||||
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
|
||||
}
|
||||
@ -723,8 +716,9 @@ async function processDownloadProposalImpl(
|
||||
contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
|
||||
};
|
||||
if (
|
||||
fulfillmentUrl.startsWith("http://") ||
|
||||
fulfillmentUrl.startsWith("https://")
|
||||
fulfillmentUrl &&
|
||||
(fulfillmentUrl.startsWith("http://") ||
|
||||
fulfillmentUrl.startsWith("https://"))
|
||||
) {
|
||||
const differentPurchase = await tx.getIndexed(
|
||||
Stores.purchases.fulfillmentUrlIndex,
|
||||
@ -968,15 +962,9 @@ export async function submitPay(
|
||||
await storePayReplaySuccess(ws, proposalId, sessionId);
|
||||
}
|
||||
|
||||
const nextUrl = getNextUrl(purchase.contractData);
|
||||
ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
|
||||
nextUrl,
|
||||
lastSessionId: sessionId,
|
||||
};
|
||||
|
||||
return {
|
||||
type: ConfirmPayResultType.Done,
|
||||
nextUrl,
|
||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1089,7 +1077,6 @@ export async function preparePayForUri(
|
||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
||||
paid: true,
|
||||
nextUrl: r.nextUrl,
|
||||
amountRaw: Amounts.stringify(purchase.contractData.amount),
|
||||
amountEffective: Amounts.stringify(purchase.payCostInfo.totalCost),
|
||||
};
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { HttpRequestLibrary } from "../util/http";
|
||||
import { NextUrlResult, BalancesResponse } from "../types/walletTypes";
|
||||
import { BalancesResponse } from "../types/walletTypes";
|
||||
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
||||
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
|
||||
import { Logger } from "../util/logging";
|
||||
@ -32,7 +32,6 @@ export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
|
||||
export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
|
||||
|
||||
export class InternalWalletState {
|
||||
cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
|
||||
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||
memoGetPending: AsyncOpMemoSingle<
|
||||
|
@ -35,7 +35,7 @@ import {
|
||||
PaymentStatus,
|
||||
WithdrawalType,
|
||||
WithdrawalDetails,
|
||||
PaymentShortInfo,
|
||||
OrderShortInfo,
|
||||
} from "../types/transactions";
|
||||
import { getFundingPaytoUris } from "./reserves";
|
||||
|
||||
@ -234,7 +234,7 @@ export async function getTransactions(
|
||||
if (!proposal) {
|
||||
return;
|
||||
}
|
||||
const info: PaymentShortInfo = {
|
||||
const info: OrderShortInfo = {
|
||||
fulfillmentUrl: pr.contractData.fulfillmentUrl,
|
||||
merchant: pr.contractData.merchant,
|
||||
orderId: pr.contractData.orderId,
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
ExchangeSignKeyJson,
|
||||
MerchantInfo,
|
||||
Product,
|
||||
InternationalizedString,
|
||||
} from "./talerTypes";
|
||||
|
||||
import { Index, Store } from "../util/query";
|
||||
@ -1270,8 +1271,10 @@ export interface AllowedExchangeInfo {
|
||||
export interface WalletContractData {
|
||||
products?: Product[];
|
||||
summaryI18n: { [lang_tag: string]: string } | undefined;
|
||||
fulfillmentUrl: string;
|
||||
fulfillmentUrl?: string;
|
||||
contractTermsHash: string;
|
||||
fulfillmentMessage?: string;
|
||||
fulfillmentMessageI18n?: InternationalizedString;
|
||||
merchantSig: string;
|
||||
merchantPub: string;
|
||||
merchant: MerchantInfo;
|
||||
|
@ -314,6 +314,10 @@ export interface Product {
|
||||
delivery_location?: string;
|
||||
}
|
||||
|
||||
export interface InternationalizedString {
|
||||
[lang_tag: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract terms from a merchant.
|
||||
*/
|
||||
@ -338,7 +342,7 @@ export class ContractTerms {
|
||||
*/
|
||||
summary: string;
|
||||
|
||||
summary_i18n?: { [lang_tag: string]: string };
|
||||
summary_i18n?: InternationalizedString;
|
||||
|
||||
/**
|
||||
* Nonce used to ensure freshness.
|
||||
@ -420,7 +424,17 @@ export class ContractTerms {
|
||||
* Fulfillment URL to view the product or
|
||||
* delivery status.
|
||||
*/
|
||||
fulfillment_url: string;
|
||||
fulfillment_url?: string;
|
||||
|
||||
/**
|
||||
* Plain text fulfillment message in the merchant's default language.
|
||||
*/
|
||||
fulfillment_message?: string;
|
||||
|
||||
/**
|
||||
* Internationalized fulfillment messages.
|
||||
*/
|
||||
fulfillment_message_i18n?: InternationalizedString;
|
||||
|
||||
/**
|
||||
* Share of the wire fee that must be settled with one payment.
|
||||
@ -1032,14 +1046,14 @@ export const codecForTax = (): Codec<Tax> =>
|
||||
.property("tax", codecForString())
|
||||
.build("Tax");
|
||||
|
||||
export const codecForI18n = (): Codec<{ [lang_tag: string]: string }> =>
|
||||
export const codecForInternationalizedString = (): Codec<InternationalizedString> =>
|
||||
codecForMap(codecForString());
|
||||
|
||||
export const codecForProduct = (): Codec<Product> =>
|
||||
buildCodecForObject<Product>()
|
||||
.property("product_id", codecOptional(codecForString()))
|
||||
.property("description", codecForString())
|
||||
.property("description_i18n", codecOptional(codecForI18n()))
|
||||
.property("description_i18n", codecOptional(codecForInternationalizedString()))
|
||||
.property("quantity", codecOptional(codecForNumber()))
|
||||
.property("unit", codecOptional(codecForString()))
|
||||
.property("price", codecOptional(codecForString()))
|
||||
@ -1050,13 +1064,15 @@ export const codecForProduct = (): Codec<Product> =>
|
||||
export const codecForContractTerms = (): Codec<ContractTerms> =>
|
||||
buildCodecForObject<ContractTerms>()
|
||||
.property("order_id", codecForString())
|
||||
.property("fulfillment_url", codecForString())
|
||||
.property("fulfillment_url", codecOptional(codecForString()))
|
||||
.property("fulfillment_message", codecOptional(codecForString()))
|
||||
.property("fulfillment_message_i18n", codecOptional(codecForInternationalizedString()))
|
||||
.property("merchant_base_url", codecForString())
|
||||
.property("h_wire", codecForString())
|
||||
.property("auto_refund", codecOptional(codecForDuration))
|
||||
.property("wire_method", codecForString())
|
||||
.property("summary", codecForString())
|
||||
.property("summary_i18n", codecOptional(codecForI18n()))
|
||||
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
|
||||
.property("nonce", codecForString())
|
||||
.property("amount", codecForString())
|
||||
.property("auditors", codecForList(codecForAuditorHandle()))
|
||||
|
@ -25,7 +25,15 @@
|
||||
* Imports.
|
||||
*/
|
||||
import { Timestamp } from "../util/time";
|
||||
import { AmountString, Product } from "./talerTypes";
|
||||
import {
|
||||
AmountString,
|
||||
Product,
|
||||
InternationalizedString,
|
||||
MerchantInfo,
|
||||
codecForInternationalizedString,
|
||||
codecForMerchantInfo,
|
||||
codecForProduct,
|
||||
} from "./talerTypes";
|
||||
import {
|
||||
Codec,
|
||||
buildCodecForObject,
|
||||
@ -202,7 +210,7 @@ export interface TransactionPayment extends TransactionCommon {
|
||||
/**
|
||||
* Additional information about the payment.
|
||||
*/
|
||||
info: PaymentShortInfo;
|
||||
info: OrderShortInfo;
|
||||
|
||||
/**
|
||||
* How far did the wallet get with processing the payment?
|
||||
@ -220,7 +228,7 @@ export interface TransactionPayment extends TransactionCommon {
|
||||
amountEffective: AmountString;
|
||||
}
|
||||
|
||||
export interface PaymentShortInfo {
|
||||
export interface OrderShortInfo {
|
||||
/**
|
||||
* Order ID, uniquely identifies the order within a merchant instance
|
||||
*/
|
||||
@ -234,7 +242,7 @@ export interface PaymentShortInfo {
|
||||
/**
|
||||
* More information about the merchant
|
||||
*/
|
||||
merchant: any;
|
||||
merchant: MerchantInfo;
|
||||
|
||||
/**
|
||||
* Summary of the order, given by the merchant
|
||||
@ -244,7 +252,7 @@ export interface PaymentShortInfo {
|
||||
/**
|
||||
* Map from IETF BCP 47 language tags to localized summaries
|
||||
*/
|
||||
summary_i18n?: { [lang_tag: string]: string };
|
||||
summary_i18n?: InternationalizedString;
|
||||
|
||||
/**
|
||||
* List of products that are part of the order
|
||||
@ -254,7 +262,18 @@ export interface PaymentShortInfo {
|
||||
/**
|
||||
* URL of the fulfillment, given by the merchant
|
||||
*/
|
||||
fulfillmentUrl: string;
|
||||
fulfillmentUrl?: string;
|
||||
|
||||
/**
|
||||
* Plain text message that should be shown to the user
|
||||
* when the payment is complete.
|
||||
*/
|
||||
fulfillmentMessage?: string;
|
||||
|
||||
/**
|
||||
* Translations of fulfillmentMessage.
|
||||
*/
|
||||
fulfillmentMessage_i18n?: InternationalizedString;
|
||||
}
|
||||
|
||||
interface TransactionRefund extends TransactionCommon {
|
||||
@ -264,7 +283,7 @@ interface TransactionRefund extends TransactionCommon {
|
||||
refundedTransactionId: string;
|
||||
|
||||
// Additional information about the refunded payment
|
||||
info: PaymentShortInfo;
|
||||
info: OrderShortInfo;
|
||||
|
||||
// Amount that has been refunded by the merchant
|
||||
amountRaw: AmountString;
|
||||
@ -321,4 +340,20 @@ export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
||||
export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
|
||||
buildCodecForObject<TransactionsResponse>()
|
||||
.property("transactions", codecForList(codecForAny()))
|
||||
.build("TransactionsResponse");
|
||||
.build("TransactionsResponse");
|
||||
|
||||
export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
|
||||
buildCodecForObject<OrderShortInfo>()
|
||||
.property("contractTermsHash", codecForString())
|
||||
.property("fulfillmentMessage", codecOptional(codecForString()))
|
||||
.property(
|
||||
"fulfillmentMessage_i18n",
|
||||
codecOptional(codecForInternationalizedString()),
|
||||
)
|
||||
.property("fulfillmentUrl", codecOptional(codecForString()))
|
||||
.property("merchant", codecForMerchantInfo())
|
||||
.property("orderId", codecForString())
|
||||
.property("products", codecOptional(codecForList(codecForProduct())))
|
||||
.property("summary", codecForString())
|
||||
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
|
||||
.build("OrderShortInfo");
|
||||
|
@ -50,8 +50,8 @@ import {
|
||||
codecForAny,
|
||||
buildCodecForUnion,
|
||||
} from "../util/codec";
|
||||
import { AmountString, codecForContractTerms } from "./talerTypes";
|
||||
import { TransactionError } from "./transactions";
|
||||
import { AmountString, codecForContractTerms, ContractTerms } from "./talerTypes";
|
||||
import { TransactionError, OrderShortInfo, codecForOrderShortInfo } from "./transactions";
|
||||
|
||||
/**
|
||||
* Response for the create reserve request to the wallet.
|
||||
@ -209,8 +209,7 @@ export const enum ConfirmPayResultType {
|
||||
*/
|
||||
export interface ConfirmPayResultDone {
|
||||
type: ConfirmPayResultType.Done;
|
||||
|
||||
nextUrl: string;
|
||||
contractTerms: ContractTerms;
|
||||
}
|
||||
|
||||
export interface ConfirmPayResultPending {
|
||||
@ -232,7 +231,7 @@ export const codecForConfirmPayResultPending = (): Codec<
|
||||
export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
|
||||
buildCodecForObject<ConfirmPayResultDone>()
|
||||
.property("type", codecForConstString(ConfirmPayResultType.Done))
|
||||
.property("nextUrl", codecForString())
|
||||
.property("contractTerms", codecForContractTerms())
|
||||
.build("ConfirmPayResultDone");
|
||||
|
||||
export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
|
||||
@ -368,14 +367,6 @@ export interface BenchmarkResult {
|
||||
repetitions: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached next URL for a particular session id.
|
||||
*/
|
||||
export interface NextUrlResult {
|
||||
nextUrl: string;
|
||||
lastSessionId: string | undefined;
|
||||
}
|
||||
|
||||
export const enum PreparePayResultType {
|
||||
PaymentPossible = "payment-possible",
|
||||
InsufficientBalance = "insufficient-balance",
|
||||
@ -388,7 +379,7 @@ export const codecForPreparePayResultPaymentPossible = (): Codec<
|
||||
buildCodecForObject<PreparePayResultPaymentPossible>()
|
||||
.property("amountEffective", codecForAmountString())
|
||||
.property("amountRaw", codecForAmountString())
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("contractTerms", codecForContractTerms())
|
||||
.property("proposalId", codecForString())
|
||||
.property(
|
||||
"status",
|
||||
@ -419,7 +410,6 @@ export const codecForPreparePayResultAlreadyConfirmed = (): Codec<
|
||||
)
|
||||
.property("amountEffective", codecForAmountString())
|
||||
.property("amountRaw", codecForAmountString())
|
||||
.property("nextUrl", codecForString())
|
||||
.property("paid", codecForBoolean)
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("contractTermsHash", codecForString())
|
||||
@ -450,7 +440,7 @@ export type PreparePayResult =
|
||||
export interface PreparePayResultPaymentPossible {
|
||||
status: PreparePayResultType.PaymentPossible;
|
||||
proposalId: string;
|
||||
contractTerms: Record<string, unknown>;
|
||||
contractTerms: ContractTerms;
|
||||
amountRaw: string;
|
||||
amountEffective: string;
|
||||
}
|
||||
@ -458,19 +448,16 @@ export interface PreparePayResultPaymentPossible {
|
||||
export interface PreparePayResultInsufficientBalance {
|
||||
status: PreparePayResultType.InsufficientBalance;
|
||||
proposalId: string;
|
||||
contractTerms: Record<string, unknown>;
|
||||
contractTerms: ContractTerms;
|
||||
amountRaw: string;
|
||||
}
|
||||
|
||||
export interface PreparePayResultAlreadyConfirmed {
|
||||
status: PreparePayResultType.AlreadyConfirmed;
|
||||
contractTerms: Record<string, unknown>;
|
||||
contractTerms: ContractTerms;
|
||||
paid: boolean;
|
||||
amountRaw: string;
|
||||
amountEffective: string;
|
||||
// Only specified if paid.
|
||||
nextUrl?: string;
|
||||
|
||||
contractTermsHash: string;
|
||||
}
|
||||
|
||||
|
@ -37,10 +37,13 @@ import {
|
||||
ContractTerms,
|
||||
codecForContractTerms,
|
||||
ConfirmPayResultType,
|
||||
ConfirmPayResult,
|
||||
getJsonI18n,
|
||||
} from "taler-wallet-core";
|
||||
|
||||
function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
|
||||
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>();
|
||||
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>();
|
||||
const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
|
||||
const [numTries, setNumTries] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -71,25 +74,25 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
|
||||
payStatus.status === PreparePayResultType.AlreadyConfirmed &&
|
||||
numTries === 0
|
||||
) {
|
||||
return (
|
||||
const fulfillmentUrl = payStatus.contractTerms.fulfillment_url;
|
||||
if (fulfillmentUrl) {
|
||||
return (
|
||||
<span>
|
||||
You have already paid for this article. Click{" "}
|
||||
<a href={fulfillmentUrl}>here</a> to view it again.
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
<span>
|
||||
You have already paid for this article. Click{" "}
|
||||
<a href={payStatus.nextUrl}>here</a> to view it again.
|
||||
</span>
|
||||
);
|
||||
You have already paid for this article:{" "}
|
||||
<em>
|
||||
{payStatus.contractTerms.fulfillment_message ?? "no message given"}
|
||||
</em>
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
||||
let contractTerms: ContractTerms;
|
||||
|
||||
try {
|
||||
contractTerms = codecForContractTerms().decode(payStatus.contractTerms);
|
||||
} catch (e) {
|
||||
// This should never happen, as the wallet is supposed to check the contract terms
|
||||
// before storing them.
|
||||
console.error(e);
|
||||
console.log("raw contract terms were", payStatus.contractTerms);
|
||||
return <span>Invalid contract terms.</span>;
|
||||
}
|
||||
let contractTerms: ContractTerms = payStatus.contractTerms;
|
||||
|
||||
if (!contractTerms) {
|
||||
return (
|
||||
@ -122,13 +125,33 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
|
||||
if (res.type !== ConfirmPayResultType.Done) {
|
||||
throw Error("payment pending");
|
||||
}
|
||||
document.location.href = res.nextUrl;
|
||||
const fu = res.contractTerms.fulfillment_url;
|
||||
if (fu) {
|
||||
document.location.href = fu;
|
||||
}
|
||||
setPayResult(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setPayErrMsg(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (payResult && payResult.type === ConfirmPayResultType.Done) {
|
||||
if (payResult.contractTerms.fulfillment_message) {
|
||||
const obj = {
|
||||
fulfillment_message: payResult.contractTerms.fulfillment_message,
|
||||
fulfillment_message_i18n: payResult.contractTerms.fulfillment_message_i18n,
|
||||
};
|
||||
const msg = getJsonI18n(obj, "fulfillment_message")
|
||||
return <div>
|
||||
<p>Payment succeeded.</p>
|
||||
<p>{msg}</p>
|
||||
</div>;
|
||||
} else {
|
||||
return <span>Redirecting ...</span>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
|
Loading…
Reference in New Issue
Block a user