aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-08-24 19:39:09 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-08-24 19:39:09 +0530
commit0e88ef9bd2ea76e5b44cc0d4459b9a2e553b8d24 (patch)
treebadf53269fb0775b11fc0160ab5b5c0d66903dd1
parent69c495076252a22bda341f58d7976e55078bd78c (diff)
implement fulfillment_message and make fulfillment_url optional
-rw-r--r--packages/taler-integrationtests/src/harness.ts2
-rw-r--r--packages/taler-wallet-core/src/i18n/index.ts9
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts33
-rw-r--r--packages/taler-wallet-core/src/operations/state.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts4
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts5
-rw-r--r--packages/taler-wallet-core/src/types/talerTypes.ts28
-rw-r--r--packages/taler-wallet-core/src/types/transactions.ts51
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.ts29
-rw-r--r--packages/taler-wallet-webextension/src/pages/pay.tsx57
10 files changed, 140 insertions, 81 deletions
diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts
index 5fd642e33..fd96c3165 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -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"],
});
diff --git a/packages/taler-wallet-core/src/i18n/index.ts b/packages/taler-wallet-core/src/i18n/index.ts
index c5b70b1fd..b8788115c 100644
--- a/packages/taler-wallet-core/src/i18n/index.ts
+++ b/packages/taler-wallet-core/src/i18n/index.ts
@@ -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];
+} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 0d1d4f993..6b45e3da2 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -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),
};
diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts
index 582dd92d3..131e9083d 100644
--- a/packages/taler-wallet-core/src/operations/state.ts
+++ b/packages/taler-wallet-core/src/operations/state.ts
@@ -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<
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 8300864b2..7b42b9a5f 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -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,
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts
index e36e322d1..79100b69f 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -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;
diff --git a/packages/taler-wallet-core/src/types/talerTypes.ts b/packages/taler-wallet-core/src/types/talerTypes.ts
index f14e2a2ab..14e1b5751 100644
--- a/packages/taler-wallet-core/src/types/talerTypes.ts
+++ b/packages/taler-wallet-core/src/types/talerTypes.ts
@@ -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()))
diff --git a/packages/taler-wallet-core/src/types/transactions.ts b/packages/taler-wallet-core/src/types/transactions.ts
index 5ee09384f..061ce28f4 100644
--- a/packages/taler-wallet-core/src/types/transactions.ts
+++ b/packages/taler-wallet-core/src/types/transactions.ts
@@ -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"); \ No newline at end of file
+ .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");
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts
index 921c63a1e..2cf3c7fbc 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -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;
}
diff --git a/packages/taler-wallet-webextension/src/pages/pay.tsx b/packages/taler-wallet-webextension/src/pages/pay.tsx
index a7c5526ed..fcf50cf37 100644
--- a/packages/taler-wallet-webextension/src/pages/pay.tsx
+++ b/packages/taler-wallet-webextension/src/pages/pay.tsx
@@ -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>