diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-08-24 19:39:09 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-08-24 19:39:09 +0530 |
commit | 0e88ef9bd2ea76e5b44cc0d4459b9a2e553b8d24 (patch) | |
tree | badf53269fb0775b11fc0160ab5b5c0d66903dd1 | |
parent | 69c495076252a22bda341f58d7976e55078bd78c (diff) |
implement fulfillment_message and make fulfillment_url optional
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> |