fix 7465
This commit is contained in:
parent
88618df7b8
commit
e05ba843a0
@ -216,6 +216,23 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function constructPayUri(
|
||||||
|
merchantBaseUrl: string,
|
||||||
|
orderId: string,
|
||||||
|
sessionId: string,
|
||||||
|
claimToken?: string,
|
||||||
|
noncePriv?: string,
|
||||||
|
): string {
|
||||||
|
const base = canonicalizeBaseUrl(merchantBaseUrl);
|
||||||
|
const url = new URL(base);
|
||||||
|
const isHttp = base.startsWith("http://");
|
||||||
|
let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
|
||||||
|
result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`;
|
||||||
|
if (claimToken) result += `c=${claimToken}`;
|
||||||
|
if (noncePriv) result += `n=${noncePriv}`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
|
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
|
||||||
const pi = parseProtoInfo(s, talerActionPayPush);
|
const pi = parseProtoInfo(s, talerActionPayPush);
|
||||||
if (!pi) {
|
if (!pi) {
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
codecForAny,
|
codecForAny,
|
||||||
codecForBoolean,
|
codecForBoolean,
|
||||||
codecForConstString,
|
codecForConstString,
|
||||||
|
codecForEither,
|
||||||
codecForList,
|
codecForList,
|
||||||
codecForMap,
|
codecForMap,
|
||||||
codecForNumber,
|
codecForNumber,
|
||||||
@ -384,6 +385,7 @@ export enum PreparePayResultType {
|
|||||||
PaymentPossible = "payment-possible",
|
PaymentPossible = "payment-possible",
|
||||||
InsufficientBalance = "insufficient-balance",
|
InsufficientBalance = "insufficient-balance",
|
||||||
AlreadyConfirmed = "already-confirmed",
|
AlreadyConfirmed = "already-confirmed",
|
||||||
|
Lost = "lost",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForPreparePayResultPaymentPossible =
|
export const codecForPreparePayResultPaymentPossible =
|
||||||
@ -394,6 +396,7 @@ export const codecForPreparePayResultPaymentPossible =
|
|||||||
.property("contractTerms", codecForMerchantContractTerms())
|
.property("contractTerms", codecForMerchantContractTerms())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.property("contractTermsHash", codecForString())
|
.property("contractTermsHash", codecForString())
|
||||||
|
.property("talerUri", codecForString())
|
||||||
.property("noncePriv", codecForString())
|
.property("noncePriv", codecForString())
|
||||||
.property(
|
.property(
|
||||||
"status",
|
"status",
|
||||||
@ -406,6 +409,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
|||||||
buildCodecForObject<PreparePayResultInsufficientBalance>()
|
buildCodecForObject<PreparePayResultInsufficientBalance>()
|
||||||
.property("amountRaw", codecForAmountString())
|
.property("amountRaw", codecForAmountString())
|
||||||
.property("contractTerms", codecForAny())
|
.property("contractTerms", codecForAny())
|
||||||
|
.property("talerUri", codecForString())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.property("noncePriv", codecForString())
|
.property("noncePriv", codecForString())
|
||||||
.property(
|
.property(
|
||||||
@ -424,11 +428,18 @@ export const codecForPreparePayResultAlreadyConfirmed =
|
|||||||
.property("amountEffective", codecForAmountString())
|
.property("amountEffective", codecForAmountString())
|
||||||
.property("amountRaw", codecForAmountString())
|
.property("amountRaw", codecForAmountString())
|
||||||
.property("paid", codecForBoolean())
|
.property("paid", codecForBoolean())
|
||||||
|
.property("talerUri", codecOptional(codecForString()))
|
||||||
.property("contractTerms", codecForAny())
|
.property("contractTerms", codecForAny())
|
||||||
.property("contractTermsHash", codecForString())
|
.property("contractTermsHash", codecForString())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.build("PreparePayResultAlreadyConfirmed");
|
.build("PreparePayResultAlreadyConfirmed");
|
||||||
|
|
||||||
|
export const codecForPreparePayResultPaymentLost =
|
||||||
|
(): Codec<PreparePayResultPaymentLost> =>
|
||||||
|
buildCodecForObject<PreparePayResultPaymentLost>()
|
||||||
|
.property("status", codecForConstString(PreparePayResultType.Lost))
|
||||||
|
.build("PreparePayResultLost");
|
||||||
|
|
||||||
export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
||||||
buildCodecForUnion<PreparePayResult>()
|
buildCodecForUnion<PreparePayResult>()
|
||||||
.discriminateOn("status")
|
.discriminateOn("status")
|
||||||
@ -444,6 +455,10 @@ export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
|||||||
PreparePayResultType.PaymentPossible,
|
PreparePayResultType.PaymentPossible,
|
||||||
codecForPreparePayResultPaymentPossible(),
|
codecForPreparePayResultPaymentPossible(),
|
||||||
)
|
)
|
||||||
|
.alternative(
|
||||||
|
PreparePayResultType.Lost,
|
||||||
|
codecForPreparePayResultPaymentLost(),
|
||||||
|
)
|
||||||
.build("PreparePayResult");
|
.build("PreparePayResult");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -452,7 +467,8 @@ export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
|||||||
export type PreparePayResult =
|
export type PreparePayResult =
|
||||||
| PreparePayResultInsufficientBalance
|
| PreparePayResultInsufficientBalance
|
||||||
| PreparePayResultAlreadyConfirmed
|
| PreparePayResultAlreadyConfirmed
|
||||||
| PreparePayResultPaymentPossible;
|
| PreparePayResultPaymentPossible
|
||||||
|
| PreparePayResultPaymentLost;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment is possible.
|
* Payment is possible.
|
||||||
@ -465,6 +481,7 @@ export interface PreparePayResultPaymentPossible {
|
|||||||
amountRaw: string;
|
amountRaw: string;
|
||||||
amountEffective: string;
|
amountEffective: string;
|
||||||
noncePriv: string;
|
noncePriv: string;
|
||||||
|
talerUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResultInsufficientBalance {
|
export interface PreparePayResultInsufficientBalance {
|
||||||
@ -473,6 +490,7 @@ export interface PreparePayResultInsufficientBalance {
|
|||||||
contractTerms: MerchantContractTerms;
|
contractTerms: MerchantContractTerms;
|
||||||
amountRaw: string;
|
amountRaw: string;
|
||||||
noncePriv: string;
|
noncePriv: string;
|
||||||
|
talerUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResultAlreadyConfirmed {
|
export interface PreparePayResultAlreadyConfirmed {
|
||||||
@ -483,6 +501,11 @@ export interface PreparePayResultAlreadyConfirmed {
|
|||||||
amountEffective: string;
|
amountEffective: string;
|
||||||
contractTermsHash: string;
|
contractTermsHash: string;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
|
talerUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparePayResultPaymentLost {
|
||||||
|
status: PreparePayResultType.Lost;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BankWithdrawDetails {
|
export interface BankWithdrawDetails {
|
||||||
@ -1677,6 +1700,170 @@ export interface WithdrawFakebankRequest {
|
|||||||
bank: string;
|
bank: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AttentionPriority {
|
||||||
|
High = "high",
|
||||||
|
Medium = "medium",
|
||||||
|
Low = "low",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAttentionByIdRequest {
|
||||||
|
entityId: string;
|
||||||
|
type: AttentionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForUserAttentionByIdRequest =
|
||||||
|
(): Codec<UserAttentionByIdRequest> =>
|
||||||
|
buildCodecForObject<UserAttentionByIdRequest>()
|
||||||
|
.property("type", codecForAny())
|
||||||
|
.property("entityId", codecForString())
|
||||||
|
.build("UserAttentionByIdRequest");
|
||||||
|
|
||||||
|
export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> =>
|
||||||
|
buildCodecForObject<UserAttentionsRequest>()
|
||||||
|
.property(
|
||||||
|
"priority",
|
||||||
|
codecOptional(
|
||||||
|
codecForEither(
|
||||||
|
codecForConstString(AttentionPriority.Low),
|
||||||
|
codecForConstString(AttentionPriority.Medium),
|
||||||
|
codecForConstString(AttentionPriority.High),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build("UserAttentionsRequest");
|
||||||
|
|
||||||
|
export interface UserAttentionsRequest {
|
||||||
|
priority?: AttentionPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AttentionInfo =
|
||||||
|
| AttentionKycWithdrawal
|
||||||
|
| AttentionBackupUnpaid
|
||||||
|
| AttentionBackupExpiresSoon
|
||||||
|
| AttentionMerchantRefund
|
||||||
|
| AttentionExchangeTosChanged
|
||||||
|
| AttentionExchangeKeyExpired
|
||||||
|
| AttentionExchangeDenominationExpired
|
||||||
|
| AttentionAuditorTosChanged
|
||||||
|
| AttentionAuditorKeyExpires
|
||||||
|
| AttentionAuditorDenominationExpires
|
||||||
|
| AttentionPullPaymentPaid
|
||||||
|
| AttentionPushPaymentReceived;
|
||||||
|
|
||||||
|
export enum AttentionType {
|
||||||
|
KycWithdrawal = "kyc-withdrawal",
|
||||||
|
|
||||||
|
BackupUnpaid = "backup-unpaid",
|
||||||
|
BackupExpiresSoon = "backup-expires-soon",
|
||||||
|
MerchantRefund = "merchant-refund",
|
||||||
|
|
||||||
|
ExchangeTosChanged = "exchange-tos-changed",
|
||||||
|
ExchangeKeyExpired = "exchange-key-expired",
|
||||||
|
ExchangeKeyExpiresSoon = "exchange-key-expires-soon",
|
||||||
|
ExchangeDenominationsExpired = "exchange-denominations-expired",
|
||||||
|
ExchangeDenominationsExpiresSoon = "exchange-denominations-expires-soon",
|
||||||
|
|
||||||
|
AuditorTosChanged = "auditor-tos-changed",
|
||||||
|
AuditorKeyExpires = "auditor-key-expires",
|
||||||
|
AuditorDenominationsExpires = "auditor-denominations-expires",
|
||||||
|
|
||||||
|
PullPaymentPaid = "pull-payment-paid",
|
||||||
|
PushPaymentReceived = "push-payment-withdrawn",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserAttentionPriority: {
|
||||||
|
[type in AttentionType]: AttentionPriority;
|
||||||
|
} = {
|
||||||
|
"kyc-withdrawal": AttentionPriority.Medium,
|
||||||
|
|
||||||
|
"backup-unpaid": AttentionPriority.High,
|
||||||
|
"backup-expires-soon": AttentionPriority.Medium,
|
||||||
|
"merchant-refund": AttentionPriority.Medium,
|
||||||
|
|
||||||
|
"exchange-tos-changed": AttentionPriority.Medium,
|
||||||
|
|
||||||
|
"exchange-key-expired": AttentionPriority.High,
|
||||||
|
"exchange-key-expires-soon": AttentionPriority.Medium,
|
||||||
|
"exchange-denominations-expired": AttentionPriority.High,
|
||||||
|
"exchange-denominations-expires-soon": AttentionPriority.Medium,
|
||||||
|
|
||||||
|
"auditor-tos-changed": AttentionPriority.Medium,
|
||||||
|
"auditor-key-expires": AttentionPriority.Medium,
|
||||||
|
"auditor-denominations-expires": AttentionPriority.Medium,
|
||||||
|
|
||||||
|
"pull-payment-paid": AttentionPriority.High,
|
||||||
|
"push-payment-withdrawn": AttentionPriority.High,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AttentionBackupExpiresSoon {
|
||||||
|
type: AttentionType.BackupExpiresSoon;
|
||||||
|
provider_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionBackupUnpaid {
|
||||||
|
type: AttentionType.BackupUnpaid;
|
||||||
|
provider_base_url: string;
|
||||||
|
talerUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttentionMerchantRefund {
|
||||||
|
type: AttentionType.MerchantRefund;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttentionKycWithdrawal {
|
||||||
|
type: AttentionType.KycWithdrawal;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttentionExchangeTosChanged {
|
||||||
|
type: AttentionType.ExchangeTosChanged;
|
||||||
|
exchange_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionExchangeKeyExpired {
|
||||||
|
type: AttentionType.ExchangeKeyExpired;
|
||||||
|
exchange_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionExchangeDenominationExpired {
|
||||||
|
type: AttentionType.ExchangeDenominationsExpired;
|
||||||
|
exchange_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionAuditorTosChanged {
|
||||||
|
type: AttentionType.AuditorTosChanged;
|
||||||
|
auditor_base_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttentionAuditorKeyExpires {
|
||||||
|
type: AttentionType.AuditorKeyExpires;
|
||||||
|
auditor_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionAuditorDenominationExpires {
|
||||||
|
type: AttentionType.AuditorDenominationsExpires;
|
||||||
|
auditor_base_url: string;
|
||||||
|
}
|
||||||
|
interface AttentionPullPaymentPaid {
|
||||||
|
type: AttentionType.PullPaymentPaid;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttentionPushPaymentReceived {
|
||||||
|
type: AttentionType.PushPaymentReceived;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserAttentionUnreadList = Array<{
|
||||||
|
info: AttentionInfo;
|
||||||
|
when: AbsoluteTime;
|
||||||
|
read: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface UserAttentionsResponse {
|
||||||
|
pending: UserAttentionUnreadList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAttentionsCountResponse {
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForWithdrawFakebankRequest =
|
export const codecForWithdrawFakebankRequest =
|
||||||
(): Codec<WithdrawFakebankRequest> =>
|
(): Codec<WithdrawFakebankRequest> =>
|
||||||
buildCodecForObject<WithdrawFakebankRequest>()
|
buildCodecForObject<WithdrawFakebankRequest>()
|
||||||
|
@ -48,6 +48,9 @@ import {
|
|||||||
WireInfo,
|
WireInfo,
|
||||||
HashCodeString,
|
HashCodeString,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
AttentionPriority,
|
||||||
|
AttentionInfo,
|
||||||
|
AbsoluteTime,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
describeContents,
|
describeContents,
|
||||||
@ -1540,6 +1543,8 @@ export interface BackupProviderRecord {
|
|||||||
*/
|
*/
|
||||||
currentPaymentProposalId?: string;
|
currentPaymentProposalId?: string;
|
||||||
|
|
||||||
|
shouldRetryFreshProposal: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proposals that were used to pay (or attempt to pay) the provider.
|
* Proposals that were used to pay (or attempt to pay) the provider.
|
||||||
*
|
*
|
||||||
@ -1841,6 +1846,21 @@ export interface ContractTermsRecord {
|
|||||||
contractTermsRaw: any;
|
contractTermsRaw: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserAttentionRecord {
|
||||||
|
info: AttentionInfo;
|
||||||
|
|
||||||
|
entityId: string;
|
||||||
|
/**
|
||||||
|
* When the notification was created.
|
||||||
|
*/
|
||||||
|
createdMs: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user mark this notification as read.
|
||||||
|
*/
|
||||||
|
read: TalerProtocolTimestamp | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema definition for the IndexedDB
|
* Schema definition for the IndexedDB
|
||||||
* wallet database.
|
* wallet database.
|
||||||
@ -2137,6 +2157,13 @@ export const WalletStoresV1 = {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
userAttention: describeStore(
|
||||||
|
"userAttention",
|
||||||
|
describeContents<UserAttentionRecord>({
|
||||||
|
keyPath: ["entityId", "info.type"],
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
145
packages/taler-wallet-core/src/operations/attention.ts
Normal file
145
packages/taler-wallet-core/src/operations/attention.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2019 GNUnet e.V.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AttentionInfo,
|
||||||
|
Logger,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
|
UserAttentionByIdRequest,
|
||||||
|
UserAttentionPriority,
|
||||||
|
UserAttentionsCountResponse,
|
||||||
|
UserAttentionsRequest,
|
||||||
|
UserAttentionsResponse,
|
||||||
|
UserAttentionUnreadList,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
|
||||||
|
const logger = new Logger("operations/attention.ts");
|
||||||
|
|
||||||
|
export async function getUserAttentionsUnreadCount(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: UserAttentionsRequest,
|
||||||
|
): Promise<UserAttentionsCountResponse> {
|
||||||
|
const total = await ws.db
|
||||||
|
.mktx((x) => [x.userAttention])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
let count = 0;
|
||||||
|
await tx.userAttention.iter().forEach((x) => {
|
||||||
|
if (
|
||||||
|
req.priority !== undefined &&
|
||||||
|
UserAttentionPriority[x.info.type] !== req.priority
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (x.read !== undefined) return;
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { total };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserAttentions(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: UserAttentionsRequest,
|
||||||
|
): Promise<UserAttentionsResponse> {
|
||||||
|
return await ws.db
|
||||||
|
.mktx((x) => [x.userAttention])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const pending: UserAttentionUnreadList = [];
|
||||||
|
await tx.userAttention.iter().forEach((x) => {
|
||||||
|
if (
|
||||||
|
req.priority !== undefined &&
|
||||||
|
UserAttentionPriority[x.info.type] !== req.priority
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
pending.push({
|
||||||
|
info: x.info,
|
||||||
|
when: {
|
||||||
|
t_ms: x.createdMs,
|
||||||
|
},
|
||||||
|
read: x.read !== undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { pending };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markAttentionRequestAsRead(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: UserAttentionByIdRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.userAttention])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ua = await tx.userAttention.get([req.entityId, req.type]);
|
||||||
|
if (!ua) throw Error("attention request not found");
|
||||||
|
tx.userAttention.put({
|
||||||
|
...ua,
|
||||||
|
read: TalerProtocolTimestamp.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the wallet need the user attention to complete a task
|
||||||
|
* internal API
|
||||||
|
*
|
||||||
|
* @param ws
|
||||||
|
* @param info
|
||||||
|
*/
|
||||||
|
export async function addAttentionRequest(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
info: AttentionInfo,
|
||||||
|
entityId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.userAttention])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.userAttention.put({
|
||||||
|
info,
|
||||||
|
entityId,
|
||||||
|
createdMs: AbsoluteTime.now().t_ms as number,
|
||||||
|
read: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* user completed the task, attention request is not needed
|
||||||
|
* internal API
|
||||||
|
*
|
||||||
|
* @param ws
|
||||||
|
* @param created
|
||||||
|
*/
|
||||||
|
export async function removeAttentionRequest(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: UserAttentionByIdRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.userAttention])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const ua = await tx.userAttention.get([req.entityId, req.type]);
|
||||||
|
if (!ua) throw Error("attention request not found");
|
||||||
|
await tx.userAttention.delete([req.entityId, req.type]);
|
||||||
|
});
|
||||||
|
}
|
@ -27,6 +27,7 @@
|
|||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AmountString,
|
AmountString,
|
||||||
|
AttentionType,
|
||||||
BackupRecovery,
|
BackupRecovery,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
buildCodecForUnion,
|
buildCodecForUnion,
|
||||||
@ -57,13 +58,17 @@ import {
|
|||||||
kdf,
|
kdf,
|
||||||
Logger,
|
Logger,
|
||||||
notEmpty,
|
notEmpty,
|
||||||
|
PaymentStatus,
|
||||||
|
PreparePayResult,
|
||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
RecoveryLoadRequest,
|
RecoveryLoadRequest,
|
||||||
RecoveryMergeStrategy,
|
RecoveryMergeStrategy,
|
||||||
|
ReserveTransactionType,
|
||||||
rsaBlind,
|
rsaBlind,
|
||||||
secretbox,
|
secretbox,
|
||||||
secretbox_open,
|
secretbox_open,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
URL,
|
URL,
|
||||||
@ -80,6 +85,7 @@ import {
|
|||||||
ConfigRecordKey,
|
ConfigRecordKey,
|
||||||
WalletBackupConfState,
|
WalletBackupConfState,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
|
import { TalerError } from "../../errors.js";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||||
import {
|
import {
|
||||||
@ -96,6 +102,7 @@ import {
|
|||||||
RetryTags,
|
RetryTags,
|
||||||
scheduleRetryInTx,
|
scheduleRetryInTx,
|
||||||
} from "../../util/retries.js";
|
} from "../../util/retries.js";
|
||||||
|
import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
|
||||||
import {
|
import {
|
||||||
checkPaymentByProposalId,
|
checkPaymentByProposalId,
|
||||||
confirmPay,
|
confirmPay,
|
||||||
@ -198,6 +205,7 @@ async function computeBackupCryptoData(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const purch of backupContent.purchases) {
|
for (const purch of backupContent.purchases) {
|
||||||
|
if (!purch.contract_terms_raw) continue;
|
||||||
const { h: contractTermsHash } = await cryptoApi.hashString({
|
const { h: contractTermsHash } = await cryptoApi.hashString({
|
||||||
str: canonicalJson(purch.contract_terms_raw),
|
str: canonicalJson(purch.contract_terms_raw),
|
||||||
});
|
});
|
||||||
@ -251,7 +259,7 @@ function getNextBackupTimestamp(): TalerProtocolTimestamp {
|
|||||||
async function runBackupCycleForProvider(
|
async function runBackupCycleForProvider(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
args: BackupForProviderArgs,
|
args: BackupForProviderArgs,
|
||||||
): Promise<OperationAttemptResult<unknown, { talerUri: string }>> {
|
): Promise<OperationAttemptResult<unknown, { talerUri?: string }>> {
|
||||||
const provider = await ws.db
|
const provider = await ws.db
|
||||||
.mktx((x) => [x.backupProviders])
|
.mktx((x) => [x.backupProviders])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
@ -292,6 +300,10 @@ async function runBackupCycleForProvider(
|
|||||||
provider.baseUrl,
|
provider.baseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (provider.shouldRetryFreshProposal) {
|
||||||
|
accountBackupUrl.searchParams.set("fresh", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await ws.http.fetch(accountBackupUrl.href, {
|
const resp = await ws.http.fetch(accountBackupUrl.href, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: encBackup,
|
body: encBackup,
|
||||||
@ -324,6 +336,12 @@ async function runBackupCycleForProvider(
|
|||||||
};
|
};
|
||||||
await tx.backupProviders.put(prov);
|
await tx.backupProviders.put(prov);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
removeAttentionRequest(ws, {
|
||||||
|
entityId: provider.baseUrl,
|
||||||
|
type: AttentionType.BackupUnpaid,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Finished,
|
type: OperationAttemptResultType.Finished,
|
||||||
result: undefined,
|
result: undefined,
|
||||||
@ -340,8 +358,21 @@ async function runBackupCycleForProvider(
|
|||||||
|
|
||||||
//We can't delay downloading the proposal since we need the id
|
//We can't delay downloading the proposal since we need the id
|
||||||
//FIXME: check download errors
|
//FIXME: check download errors
|
||||||
|
let res: PreparePayResult | undefined = undefined;
|
||||||
|
try {
|
||||||
|
res = await preparePayForUri(ws, talerUri);
|
||||||
|
} catch (e) {
|
||||||
|
const error = TalerError.fromException(e);
|
||||||
|
if (!error.hasErrorCode(TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await preparePayForUri(ws, talerUri);
|
if (
|
||||||
|
res === undefined ||
|
||||||
|
res.status === PreparePayResultType.AlreadyConfirmed
|
||||||
|
) {
|
||||||
|
//claimed
|
||||||
|
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.backupProviders, x.operationRetries])
|
.mktx((x) => [x.backupProviders, x.operationRetries])
|
||||||
@ -353,7 +384,7 @@ async function runBackupCycleForProvider(
|
|||||||
}
|
}
|
||||||
const opId = RetryTags.forBackup(prov);
|
const opId = RetryTags.forBackup(prov);
|
||||||
await scheduleRetryInTx(ws, tx, opId);
|
await scheduleRetryInTx(ws, tx, opId);
|
||||||
prov.currentPaymentProposalId = res.proposalId;
|
prov.shouldRetryFreshProposal = true;
|
||||||
prov.state = {
|
prov.state = {
|
||||||
tag: BackupProviderStateTag.Retrying,
|
tag: BackupProviderStateTag.Retrying,
|
||||||
};
|
};
|
||||||
@ -367,6 +398,47 @@ async function runBackupCycleForProvider(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const result = res;
|
||||||
|
|
||||||
|
if (result.status === PreparePayResultType.Lost) {
|
||||||
|
throw Error("invalid state, could not get proposal for backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.backupProviders, x.operationRetries])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const prov = await tx.backupProviders.get(provider.baseUrl);
|
||||||
|
if (!prov) {
|
||||||
|
logger.warn("backup provider not found anymore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const opId = RetryTags.forBackup(prov);
|
||||||
|
await scheduleRetryInTx(ws, tx, opId);
|
||||||
|
prov.currentPaymentProposalId = result.proposalId;
|
||||||
|
prov.shouldRetryFreshProposal = false;
|
||||||
|
prov.state = {
|
||||||
|
tag: BackupProviderStateTag.Retrying,
|
||||||
|
};
|
||||||
|
await tx.backupProviders.put(prov);
|
||||||
|
});
|
||||||
|
|
||||||
|
addAttentionRequest(
|
||||||
|
ws,
|
||||||
|
{
|
||||||
|
type: AttentionType.BackupUnpaid,
|
||||||
|
provider_base_url: provider.baseUrl,
|
||||||
|
talerUri,
|
||||||
|
},
|
||||||
|
provider.baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Pending,
|
||||||
|
result: {
|
||||||
|
talerUri,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.NoContent) {
|
if (resp.status === HttpStatusCode.NoContent) {
|
||||||
await ws.db
|
await ws.db
|
||||||
@ -384,6 +456,12 @@ async function runBackupCycleForProvider(
|
|||||||
};
|
};
|
||||||
await tx.backupProviders.put(prov);
|
await tx.backupProviders.put(prov);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
removeAttentionRequest(ws, {
|
||||||
|
entityId: provider.baseUrl,
|
||||||
|
type: AttentionType.BackupUnpaid,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Finished,
|
type: OperationAttemptResultType.Finished,
|
||||||
result: undefined,
|
result: undefined,
|
||||||
@ -564,7 +642,7 @@ interface AddBackupProviderOk {
|
|||||||
}
|
}
|
||||||
interface AddBackupProviderPaymentRequired {
|
interface AddBackupProviderPaymentRequired {
|
||||||
status: "payment-required";
|
status: "payment-required";
|
||||||
talerUri: string;
|
talerUri?: string;
|
||||||
}
|
}
|
||||||
interface AddBackupProviderError {
|
interface AddBackupProviderError {
|
||||||
status: "error";
|
status: "error";
|
||||||
@ -580,7 +658,7 @@ export const codecForAddBackupProviderPaymenrRequired =
|
|||||||
(): Codec<AddBackupProviderPaymentRequired> =>
|
(): Codec<AddBackupProviderPaymentRequired> =>
|
||||||
buildCodecForObject<AddBackupProviderPaymentRequired>()
|
buildCodecForObject<AddBackupProviderPaymentRequired>()
|
||||||
.property("status", codecForConstString("payment-required"))
|
.property("status", codecForConstString("payment-required"))
|
||||||
.property("talerUri", codecForString())
|
.property("talerUri", codecOptional(codecForString()))
|
||||||
.build("AddBackupProviderPaymentRequired");
|
.build("AddBackupProviderPaymentRequired");
|
||||||
|
|
||||||
export const codecForAddBackupProviderError =
|
export const codecForAddBackupProviderError =
|
||||||
@ -655,6 +733,7 @@ export async function addBackupProvider(
|
|||||||
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
||||||
supportedProtocolVersion: terms.version,
|
supportedProtocolVersion: terms.version,
|
||||||
},
|
},
|
||||||
|
shouldRetryFreshProposal: false,
|
||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
baseUrl: canonUrl,
|
baseUrl: canonUrl,
|
||||||
uids: [encodeCrock(getRandomBytes(32))],
|
uids: [encodeCrock(getRandomBytes(32))],
|
||||||
@ -779,10 +858,12 @@ export interface ProviderPaymentUnpaid {
|
|||||||
|
|
||||||
export interface ProviderPaymentInsufficientBalance {
|
export interface ProviderPaymentInsufficientBalance {
|
||||||
type: ProviderPaymentType.InsufficientBalance;
|
type: ProviderPaymentType.InsufficientBalance;
|
||||||
|
amount: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderPaymentPending {
|
export interface ProviderPaymentPending {
|
||||||
type: ProviderPaymentType.Pending;
|
type: ProviderPaymentType.Pending;
|
||||||
|
talerUri?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderPaymentPaid {
|
export interface ProviderPaymentPaid {
|
||||||
@ -810,32 +891,40 @@ async function getProviderPaymentInfo(
|
|||||||
ws,
|
ws,
|
||||||
provider.currentPaymentProposalId,
|
provider.currentPaymentProposalId,
|
||||||
);
|
);
|
||||||
if (status.status === PreparePayResultType.InsufficientBalance) {
|
|
||||||
|
switch (status.status) {
|
||||||
|
case PreparePayResultType.InsufficientBalance:
|
||||||
return {
|
return {
|
||||||
type: ProviderPaymentType.InsufficientBalance,
|
type: ProviderPaymentType.InsufficientBalance,
|
||||||
|
amount: status.amountRaw,
|
||||||
};
|
};
|
||||||
}
|
case PreparePayResultType.PaymentPossible:
|
||||||
if (status.status === PreparePayResultType.PaymentPossible) {
|
|
||||||
return {
|
return {
|
||||||
type: ProviderPaymentType.Pending,
|
type: ProviderPaymentType.Pending,
|
||||||
|
talerUri: status.talerUri,
|
||||||
};
|
};
|
||||||
}
|
case PreparePayResultType.Lost:
|
||||||
if (status.status === PreparePayResultType.AlreadyConfirmed) {
|
return {
|
||||||
|
type: ProviderPaymentType.Unpaid,
|
||||||
|
};
|
||||||
|
case PreparePayResultType.AlreadyConfirmed:
|
||||||
if (status.paid) {
|
if (status.paid) {
|
||||||
return {
|
return {
|
||||||
type: ProviderPaymentType.Paid,
|
type: ProviderPaymentType.Paid,
|
||||||
paidUntil: AbsoluteTime.addDuration(
|
paidUntil: AbsoluteTime.addDuration(
|
||||||
AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
|
AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
|
||||||
durationFromSpec({ years: 1 }),
|
durationFromSpec({ years: 1 }), //FIXME: take this from the contract term
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
type: ProviderPaymentType.Pending,
|
type: ProviderPaymentType.Pending,
|
||||||
|
talerUri: status.talerUri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
assertUnreachable(status);
|
||||||
}
|
}
|
||||||
throw Error("not reached");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -936,6 +1025,7 @@ async function backupRecoveryTheirs(
|
|||||||
baseUrl: prov.url,
|
baseUrl: prov.url,
|
||||||
name: prov.name,
|
name: prov.name,
|
||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
|
shouldRetryFreshProposal: false,
|
||||||
state: {
|
state: {
|
||||||
tag: BackupProviderStateTag.Ready,
|
tag: BackupProviderStateTag.Ready,
|
||||||
nextBackupTimestamp: TalerProtocolTimestamp.now(),
|
nextBackupTimestamp: TalerProtocolTimestamp.now(),
|
||||||
|
@ -72,6 +72,7 @@ import {
|
|||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
|
constructPayUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -1290,7 +1291,10 @@ export async function checkPaymentByProposalId(
|
|||||||
return tx.purchases.get(proposalId);
|
return tx.purchases.get(proposalId);
|
||||||
});
|
});
|
||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
throw Error(`could not get proposal ${proposalId}`);
|
// throw Error(`could not get proposal ${proposalId}`);
|
||||||
|
return {
|
||||||
|
status: PreparePayResultType.Lost,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
|
if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
|
||||||
const existingProposalId = proposal.repurchaseProposalId;
|
const existingProposalId = proposal.repurchaseProposalId;
|
||||||
@ -1316,6 +1320,14 @@ export async function checkPaymentByProposalId(
|
|||||||
|
|
||||||
proposalId = proposal.proposalId;
|
proposalId = proposal.proposalId;
|
||||||
|
|
||||||
|
const talerUri = constructPayUri(
|
||||||
|
proposal.merchantBaseUrl,
|
||||||
|
proposal.orderId,
|
||||||
|
proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
|
||||||
|
proposal.claimToken,
|
||||||
|
proposal.noncePriv,
|
||||||
|
);
|
||||||
|
|
||||||
// First check if we already paid for it.
|
// First check if we already paid for it.
|
||||||
const purchase = await ws.db
|
const purchase = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
@ -1345,6 +1357,7 @@ export async function checkPaymentByProposalId(
|
|||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
noncePriv: proposal.noncePriv,
|
noncePriv: proposal.noncePriv,
|
||||||
amountRaw: Amounts.stringify(d.contractData.amount),
|
amountRaw: Amounts.stringify(d.contractData.amount),
|
||||||
|
talerUri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1360,6 +1373,7 @@ export async function checkPaymentByProposalId(
|
|||||||
amountEffective: Amounts.stringify(totalCost),
|
amountEffective: Amounts.stringify(totalCost),
|
||||||
amountRaw: Amounts.stringify(res.paymentAmount),
|
amountRaw: Amounts.stringify(res.paymentAmount),
|
||||||
contractTermsHash: d.contractData.contractTermsHash,
|
contractTermsHash: d.contractData.contractTermsHash,
|
||||||
|
talerUri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1396,6 +1410,7 @@ export async function checkPaymentByProposalId(
|
|||||||
amountRaw: Amounts.stringify(download.contractData.amount),
|
amountRaw: Amounts.stringify(download.contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||||
proposalId,
|
proposalId,
|
||||||
|
talerUri,
|
||||||
};
|
};
|
||||||
} else if (!purchase.timestampFirstSuccessfulPay) {
|
} else if (!purchase.timestampFirstSuccessfulPay) {
|
||||||
const download = await expectProposalDownload(ws, purchase);
|
const download = await expectProposalDownload(ws, purchase);
|
||||||
@ -1407,6 +1422,7 @@ export async function checkPaymentByProposalId(
|
|||||||
amountRaw: Amounts.stringify(download.contractData.amount),
|
amountRaw: Amounts.stringify(download.contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||||
proposalId,
|
proposalId,
|
||||||
|
talerUri,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const paid =
|
const paid =
|
||||||
@ -1423,6 +1439,7 @@ export async function checkPaymentByProposalId(
|
|||||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||||
...(paid ? { nextUrl: download.contractData.orderId } : {}),
|
...(paid ? { nextUrl: download.contractData.orderId } : {}),
|
||||||
proposalId,
|
proposalId,
|
||||||
|
talerUri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1468,7 +1485,7 @@ export async function preparePayForUri(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposalId = await startDownloadProposal(
|
const proposalId = await startDownloadProposal(
|
||||||
ws,
|
ws,
|
||||||
uriResult.merchantBaseUrl,
|
uriResult.merchantBaseUrl,
|
||||||
uriResult.orderId,
|
uriResult.orderId,
|
||||||
@ -1930,6 +1947,28 @@ export async function processPurchasePay(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.status === HttpStatusCode.Gone) {
|
||||||
|
const errDetails = await readUnexpectedResponseDetails(resp);
|
||||||
|
logger.warn("unexpected 410 response for /pay");
|
||||||
|
logger.warn(j2s(errDetails));
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => [x.purchases])
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const purch = await tx.purchases.get(proposalId);
|
||||||
|
if (!purch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// FIXME: Should be some "PayPermanentlyFailed" and error info should be stored
|
||||||
|
purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
|
||||||
|
await tx.purchases.put(purch);
|
||||||
|
});
|
||||||
|
throw makePendingOperationFailedError(
|
||||||
|
errDetails,
|
||||||
|
TransactionType.Payment,
|
||||||
|
proposalId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.Conflict) {
|
if (resp.status === HttpStatusCode.Conflict) {
|
||||||
const err = await readTalerErrorResponse(resp);
|
const err = await readTalerErrorResponse(resp);
|
||||||
if (
|
if (
|
||||||
|
@ -15,5 +15,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function assertUnreachable(x: never): never {
|
export function assertUnreachable(x: never): never {
|
||||||
throw new Error("Didn't expect to get here");
|
throw new Error(`Didn't expect to get here ${x}`);
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,9 @@ import {
|
|||||||
KnownBankAccounts,
|
KnownBankAccounts,
|
||||||
ListKnownBankAccountsRequest,
|
ListKnownBankAccountsRequest,
|
||||||
ManualWithdrawalDetails,
|
ManualWithdrawalDetails,
|
||||||
|
UserAttentionsCountResponse,
|
||||||
|
UserAttentionsRequest,
|
||||||
|
UserAttentionsResponse,
|
||||||
PrepareDepositRequest,
|
PrepareDepositRequest,
|
||||||
PrepareDepositResponse,
|
PrepareDepositResponse,
|
||||||
PreparePayRequest,
|
PreparePayRequest,
|
||||||
@ -102,6 +105,7 @@ import {
|
|||||||
WithdrawFakebankRequest,
|
WithdrawFakebankRequest,
|
||||||
WithdrawTestBalanceRequest,
|
WithdrawTestBalanceRequest,
|
||||||
WithdrawUriInfoResponse,
|
WithdrawUriInfoResponse,
|
||||||
|
UserAttentionByIdRequest,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { WalletContractData } from "./db.js";
|
import { WalletContractData } from "./db.js";
|
||||||
import {
|
import {
|
||||||
@ -133,6 +137,9 @@ export enum WalletApiOperation {
|
|||||||
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
||||||
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
||||||
GetBalances = "getBalances",
|
GetBalances = "getBalances",
|
||||||
|
GetUserAttentionRequests = "getUserAttentionRequests",
|
||||||
|
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
|
||||||
|
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
||||||
GetPendingOperations = "getPendingOperations",
|
GetPendingOperations = "getPendingOperations",
|
||||||
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
||||||
ApplyRefund = "applyRefund",
|
ApplyRefund = "applyRefund",
|
||||||
@ -746,6 +753,33 @@ export type WithdrawFakebankOp = {
|
|||||||
response: EmptyObject;
|
response: EmptyObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet-internal pending tasks.
|
||||||
|
*/
|
||||||
|
export type GetUserAttentionRequests = {
|
||||||
|
op: WalletApiOperation.GetUserAttentionRequests;
|
||||||
|
request: UserAttentionsRequest;
|
||||||
|
response: UserAttentionsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet-internal pending tasks.
|
||||||
|
*/
|
||||||
|
export type MarkAttentionRequestAsRead = {
|
||||||
|
op: WalletApiOperation.MarkAttentionRequestAsRead;
|
||||||
|
request: UserAttentionByIdRequest;
|
||||||
|
response: EmptyObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet-internal pending tasks.
|
||||||
|
*/
|
||||||
|
export type GetUserAttentionsUnreadCount = {
|
||||||
|
op: WalletApiOperation.GetUserAttentionUnreadCount;
|
||||||
|
request: UserAttentionsRequest;
|
||||||
|
response: UserAttentionsCountResponse;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get wallet-internal pending tasks.
|
* Get wallet-internal pending tasks.
|
||||||
*/
|
*/
|
||||||
@ -798,6 +832,9 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
|
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
|
||||||
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
|
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
|
||||||
[WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
|
[WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
|
||||||
|
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
|
||||||
|
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
|
||||||
|
[WalletApiOperation.MarkAttentionRequestAsRead]: MarkAttentionRequestAsRead;
|
||||||
[WalletApiOperation.DumpCoins]: DumpCoinsOp;
|
[WalletApiOperation.DumpCoins]: DumpCoinsOp;
|
||||||
[WalletApiOperation.SetCoinSuspended]: SetCoinSuspendedOp;
|
[WalletApiOperation.SetCoinSuspended]: SetCoinSuspendedOp;
|
||||||
[WalletApiOperation.ForceRefresh]: ForceRefreshOp;
|
[WalletApiOperation.ForceRefresh]: ForceRefreshOp;
|
||||||
|
@ -55,6 +55,7 @@ import {
|
|||||||
codecForInitiatePeerPushPaymentRequest,
|
codecForInitiatePeerPushPaymentRequest,
|
||||||
codecForIntegrationTestArgs,
|
codecForIntegrationTestArgs,
|
||||||
codecForListKnownBankAccounts,
|
codecForListKnownBankAccounts,
|
||||||
|
codecForUserAttentionsRequest,
|
||||||
codecForPrepareDepositRequest,
|
codecForPrepareDepositRequest,
|
||||||
codecForPreparePayRequest,
|
codecForPreparePayRequest,
|
||||||
codecForPreparePeerPullPaymentRequest,
|
codecForPreparePeerPullPaymentRequest,
|
||||||
@ -98,6 +99,7 @@ import {
|
|||||||
URL,
|
URL,
|
||||||
WalletCoreVersion,
|
WalletCoreVersion,
|
||||||
WalletNotification,
|
WalletNotification,
|
||||||
|
codecForUserAttentionByIdRequest,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -147,6 +149,11 @@ import {
|
|||||||
} from "./operations/backup/index.js";
|
} from "./operations/backup/index.js";
|
||||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
import { setWalletDeviceId } from "./operations/backup/state.js";
|
||||||
import { getBalances } from "./operations/balance.js";
|
import { getBalances } from "./operations/balance.js";
|
||||||
|
import {
|
||||||
|
getUserAttentions,
|
||||||
|
getUserAttentionsUnreadCount,
|
||||||
|
markAttentionRequestAsRead,
|
||||||
|
} from "./operations/attention.js";
|
||||||
import {
|
import {
|
||||||
getExchangeTosStatus,
|
getExchangeTosStatus,
|
||||||
makeExchangeListItem,
|
makeExchangeListItem,
|
||||||
@ -1094,6 +1101,18 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
case WalletApiOperation.GetBalances: {
|
case WalletApiOperation.GetBalances: {
|
||||||
return await getBalances(ws);
|
return await getBalances(ws);
|
||||||
}
|
}
|
||||||
|
case WalletApiOperation.GetUserAttentionRequests: {
|
||||||
|
const req = codecForUserAttentionsRequest().decode(payload);
|
||||||
|
return await getUserAttentions(ws, req);
|
||||||
|
}
|
||||||
|
case WalletApiOperation.MarkAttentionRequestAsRead: {
|
||||||
|
const req = codecForUserAttentionByIdRequest().decode(payload);
|
||||||
|
return await markAttentionRequestAsRead(ws, req);
|
||||||
|
}
|
||||||
|
case WalletApiOperation.GetUserAttentionUnreadCount: {
|
||||||
|
const req = codecForUserAttentionsRequest().decode(payload);
|
||||||
|
return await getUserAttentionsUnreadCount(ws, req);
|
||||||
|
}
|
||||||
case WalletApiOperation.GetPendingOperations: {
|
case WalletApiOperation.GetPendingOperations: {
|
||||||
return await getPendingOperations(ws);
|
return await getPendingOperations(ws);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import {
|
import {
|
||||||
NavigationHeader,
|
NavigationHeader,
|
||||||
NavigationHeaderHolder,
|
NavigationHeaderHolder,
|
||||||
@ -33,6 +33,11 @@ import {
|
|||||||
import { useTranslationContext } from "./context/translation.js";
|
import { useTranslationContext } from "./context/translation.js";
|
||||||
import settingsIcon from "./svg/settings_black_24dp.svg";
|
import settingsIcon from "./svg/settings_black_24dp.svg";
|
||||||
import qrIcon from "./svg/qr_code_24px.svg";
|
import qrIcon from "./svg/qr_code_24px.svg";
|
||||||
|
import warningIcon from "./svg/warning_24px.svg";
|
||||||
|
import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
|
||||||
|
import { wxApi } from "./wxApi.js";
|
||||||
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { JustInDevMode } from "./components/JustInDevMode.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of pages used by the wallet
|
* List of pages used by the wallet
|
||||||
@ -102,6 +107,7 @@ export const Pages = {
|
|||||||
backupProviderAdd: "/backup/provider/add",
|
backupProviderAdd: "/backup/provider/add",
|
||||||
|
|
||||||
qr: "/qr",
|
qr: "/qr",
|
||||||
|
notifications: "/notifications",
|
||||||
settings: "/settings",
|
settings: "/settings",
|
||||||
settingsExchangeAdd: pageDefinition<{ currency?: string }>(
|
settingsExchangeAdd: pageDefinition<{ currency?: string }>(
|
||||||
"/settings/exchange/add/:currency?",
|
"/settings/exchange/add/:currency?",
|
||||||
@ -127,7 +133,21 @@ export const Pages = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PopupNavBar({ path = "" }: { path?: string }): VNode {
|
export function PopupNavBar({
|
||||||
|
path = "",
|
||||||
|
}: {
|
||||||
|
path?: string;
|
||||||
|
}): // api: typeof wxApi,
|
||||||
|
VNode {
|
||||||
|
const api = wxApi; //FIXME: as parameter
|
||||||
|
const hook = useAsyncAsHook(async () => {
|
||||||
|
return await api.wallet.call(
|
||||||
|
WalletApiOperation.GetUserAttentionUnreadCount,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
|
||||||
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
return (
|
return (
|
||||||
<NavigationHeader>
|
<NavigationHeader>
|
||||||
@ -141,6 +161,17 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
|
|||||||
<i18n.Translate>Backup</i18n.Translate>
|
<i18n.Translate>Backup</i18n.Translate>
|
||||||
</a>
|
</a>
|
||||||
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
|
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
|
||||||
|
{attentionCount > 0 ? (
|
||||||
|
<a href={Pages.notifications}>
|
||||||
|
<SvgIcon
|
||||||
|
title={i18n.str`Notifications`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: warningIcon }}
|
||||||
|
color="yellow"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)}
|
||||||
<a href={Pages.qr}>
|
<a href={Pages.qr}>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
title={i18n.str`QR Reader and Taler URI`}
|
title={i18n.str`QR Reader and Taler URI`}
|
||||||
@ -178,9 +209,15 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
|
|||||||
<i18n.Translate>Backup</i18n.Translate>
|
<i18n.Translate>Backup</i18n.Translate>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href={Pages.notifications}>
|
||||||
|
<i18n.Translate>Notifications</i18n.Translate>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<JustInDevMode>
|
||||||
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
|
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
|
||||||
<i18n.Translate>Dev</i18n.Translate>
|
<i18n.Translate>Dev</i18n.Translate>
|
||||||
</a>
|
</a>
|
||||||
|
</JustInDevMode>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
|
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
|
||||||
|
@ -50,7 +50,6 @@ function RenderAmount(): VNode {
|
|||||||
<AmountField
|
<AmountField
|
||||||
required
|
required
|
||||||
label={<i18n.Translate>Amount</i18n.Translate>}
|
label={<i18n.Translate>Amount</i18n.Translate>}
|
||||||
currency="USD"
|
|
||||||
highestDenom={2000000}
|
highestDenom={2000000}
|
||||||
lowestDenom={0.01}
|
lowestDenom={0.01}
|
||||||
handler={handler}
|
handler={handler}
|
||||||
|
@ -27,6 +27,7 @@ import { h, VNode } from "preact";
|
|||||||
import { useTranslationContext } from "../context/translation.js";
|
import { useTranslationContext } from "../context/translation.js";
|
||||||
import { Avatar } from "../mui/Avatar.js";
|
import { Avatar } from "../mui/Avatar.js";
|
||||||
import { Pages } from "../NavigationBar.js";
|
import { Pages } from "../NavigationBar.js";
|
||||||
|
import { assertUnreachable } from "../utils/index.js";
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ExtraLargeText,
|
ExtraLargeText,
|
||||||
@ -175,8 +176,7 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default: {
|
default: {
|
||||||
const pe: never = tx;
|
assertUnreachable(tx);
|
||||||
throw Error(`unsupported transaction type ${pe}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,7 @@ export function useComponentState(
|
|||||||
|
|
||||||
const insufficientBalance: PreparePayResult = {
|
const insufficientBalance: PreparePayResult = {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
talerUri: "taler://pay",
|
||||||
proposalId: "fakeID",
|
proposalId: "fakeID",
|
||||||
contractTerms: {} as any,
|
contractTerms: {} as any,
|
||||||
amountRaw: hook.response.p2p.amount,
|
amountRaw: hook.response.p2p.amount,
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
AmountJson,
|
AmountJson,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
PreparePayResultAlreadyConfirmed,
|
PreparePayResultAlreadyConfirmed,
|
||||||
|
PreparePayResultInsufficientBalance,
|
||||||
PreparePayResultPaymentPossible,
|
PreparePayResultPaymentPossible,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
@ -26,7 +27,7 @@ import { ButtonHandler } from "../../mui/handlers.js";
|
|||||||
import { compose, StateViewMap } from "../../utils/index.js";
|
import { compose, StateViewMap } from "../../utils/index.js";
|
||||||
import { wxApi } from "../../wxApi.js";
|
import { wxApi } from "../../wxApi.js";
|
||||||
import { useComponentState } from "./state.js";
|
import { useComponentState } from "./state.js";
|
||||||
import { BaseView, LoadingUriView } from "./views.js";
|
import { BaseView, LoadingUriView, LostView } from "./views.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
talerPayUri?: string;
|
talerPayUri?: string;
|
||||||
@ -40,6 +41,7 @@ export type State =
|
|||||||
| State.LoadingUriError
|
| State.LoadingUriError
|
||||||
| State.Ready
|
| State.Ready
|
||||||
| State.NoEnoughBalance
|
| State.NoEnoughBalance
|
||||||
|
| State.Lost
|
||||||
| State.NoBalanceForCurrency
|
| State.NoBalanceForCurrency
|
||||||
| State.Confirmed;
|
| State.Confirmed;
|
||||||
|
|
||||||
@ -62,12 +64,15 @@ export namespace State {
|
|||||||
}
|
}
|
||||||
export interface NoBalanceForCurrency extends BaseInfo {
|
export interface NoBalanceForCurrency extends BaseInfo {
|
||||||
status: "no-balance-for-currency";
|
status: "no-balance-for-currency";
|
||||||
payStatus: PreparePayResult;
|
payStatus:
|
||||||
|
| PreparePayResultInsufficientBalance
|
||||||
|
| PreparePayResultPaymentPossible
|
||||||
|
| PreparePayResultAlreadyConfirmed;
|
||||||
balance: undefined;
|
balance: undefined;
|
||||||
}
|
}
|
||||||
export interface NoEnoughBalance extends BaseInfo {
|
export interface NoEnoughBalance extends BaseInfo {
|
||||||
status: "no-enough-balance";
|
status: "no-enough-balance";
|
||||||
payStatus: PreparePayResult;
|
payStatus: PreparePayResultInsufficientBalance;
|
||||||
balance: AmountJson;
|
balance: AmountJson;
|
||||||
}
|
}
|
||||||
export interface Ready extends BaseInfo {
|
export interface Ready extends BaseInfo {
|
||||||
@ -77,6 +82,11 @@ export namespace State {
|
|||||||
balance: AmountJson;
|
balance: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Lost {
|
||||||
|
status: "lost";
|
||||||
|
error: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Confirmed extends BaseInfo {
|
export interface Confirmed extends BaseInfo {
|
||||||
status: "confirmed";
|
status: "confirmed";
|
||||||
payStatus: PreparePayResultAlreadyConfirmed;
|
payStatus: PreparePayResultAlreadyConfirmed;
|
||||||
@ -89,6 +99,7 @@ const viewMapping: StateViewMap<State> = {
|
|||||||
"loading-uri": LoadingUriView,
|
"loading-uri": LoadingUriView,
|
||||||
"no-balance-for-currency": BaseView,
|
"no-balance-for-currency": BaseView,
|
||||||
"no-enough-balance": BaseView,
|
"no-enough-balance": BaseView,
|
||||||
|
lost: LostView,
|
||||||
confirmed: BaseView,
|
confirmed: BaseView,
|
||||||
ready: BaseView,
|
ready: BaseView,
|
||||||
};
|
};
|
||||||
|
@ -82,6 +82,14 @@ export function useComponentState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { payStatus } = hook.response;
|
const { payStatus } = hook.response;
|
||||||
|
|
||||||
|
if (payStatus.status === PreparePayResultType.Lost) {
|
||||||
|
return {
|
||||||
|
status: "lost",
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
|
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
|
||||||
|
|
||||||
const foundBalance = hook.response.balance.balances.find(
|
const foundBalance = hook.response.balance.balances.find(
|
||||||
|
@ -44,6 +44,7 @@ export const NoBalance = createExample(BaseView, {
|
|||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
@ -73,6 +74,7 @@ export const NoEnoughBalance = createExample(BaseView, {
|
|||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
@ -102,6 +104,7 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
|
|||||||
uri: "",
|
uri: "",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
@ -136,6 +139,7 @@ export const PaymentPossible = createExample(BaseView, {
|
|||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.PaymentPossible,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
amountEffective: "USD:10",
|
amountEffective: "USD:10",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
@ -176,6 +180,7 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
|||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.PaymentPossible,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
amountEffective: "USD:10.20",
|
amountEffective: "USD:10.20",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
@ -213,6 +218,7 @@ export const TicketWithAProductList = createExample(BaseView, {
|
|||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.PaymentPossible,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
amountEffective: "USD:10.20",
|
amountEffective: "USD:10.20",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
@ -269,6 +275,7 @@ export const TicketWithShipping = createExample(BaseView, {
|
|||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.PaymentPossible,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
amountEffective: "USD:10.20",
|
amountEffective: "USD:10.20",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
noncePriv: "",
|
noncePriv: "",
|
||||||
@ -315,6 +322,7 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
|||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
payStatus: {
|
payStatus: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
|
talerUri: "taler://pay/..",
|
||||||
amountEffective: "USD:10",
|
amountEffective: "USD:10",
|
||||||
amountRaw: "USD:10",
|
amountRaw: "USD:10",
|
||||||
contractTerms: {
|
contractTerms: {
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Amount } from "../../components/Amount.js";
|
import { Amount } from "../../components/Amount.js";
|
||||||
|
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
||||||
import { LoadingError } from "../../components/LoadingError.js";
|
import { LoadingError } from "../../components/LoadingError.js";
|
||||||
import { LogoHeader } from "../../components/LogoHeader.js";
|
import { LogoHeader } from "../../components/LogoHeader.js";
|
||||||
import { Part } from "../../components/Part.js";
|
import { Part } from "../../components/Part.js";
|
||||||
@ -43,6 +44,7 @@ import { Time } from "../../components/Time.js";
|
|||||||
import { useTranslationContext } from "../../context/translation.js";
|
import { useTranslationContext } from "../../context/translation.js";
|
||||||
import { Button } from "../../mui/Button.js";
|
import { Button } from "../../mui/Button.js";
|
||||||
import { ButtonHandler } from "../../mui/handlers.js";
|
import { ButtonHandler } from "../../mui/handlers.js";
|
||||||
|
import { assertUnreachable } from "../../utils/index.js";
|
||||||
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
|
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
|
||||||
import { State } from "./index.js";
|
import { State } from "./index.js";
|
||||||
|
|
||||||
@ -63,8 +65,24 @@ type SupportedStates =
|
|||||||
| State.NoBalanceForCurrency
|
| State.NoBalanceForCurrency
|
||||||
| State.NoEnoughBalance;
|
| State.NoEnoughBalance;
|
||||||
|
|
||||||
|
export function LostView(state: State.Lost): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorMessage
|
||||||
|
title={<i18n.Translate>Could not load pay status</i18n.Translate>}
|
||||||
|
description={
|
||||||
|
<i18n.Translate>
|
||||||
|
The proposal was lost, another should be downloaded
|
||||||
|
</i18n.Translate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BaseView(state: SupportedStates): VNode {
|
export function BaseView(state: SupportedStates): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
||||||
|
|
||||||
const price = {
|
const price = {
|
||||||
@ -399,8 +417,9 @@ export function ButtonsSection({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (payStatus.status === PreparePayResultType.Lost) {
|
||||||
const error: never = payStatus;
|
|
||||||
|
|
||||||
return <Fragment />;
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUnreachable(payStatus);
|
||||||
}
|
}
|
||||||
|
@ -150,6 +150,10 @@ export function Application(): VNode {
|
|||||||
component={RedirectToWalletPage}
|
component={RedirectToWalletPage}
|
||||||
/>
|
/>
|
||||||
<Route path={Pages.dev} component={RedirectToWalletPage} />
|
<Route path={Pages.dev} component={RedirectToWalletPage} />
|
||||||
|
<Route
|
||||||
|
path={Pages.notifications}
|
||||||
|
component={RedirectToWalletPage}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route default component={Redirect} to={Pages.balance} />
|
<Route default component={Redirect} to={Pages.balance} />
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -171,7 +171,11 @@ export function useComponentState(
|
|||||||
|
|
||||||
switch (resp.status) {
|
switch (resp.status) {
|
||||||
case "payment-required":
|
case "payment-required":
|
||||||
|
if (resp.talerUri) {
|
||||||
return onPaymentRequired(resp.talerUri);
|
return onPaymentRequired(resp.talerUri);
|
||||||
|
} else {
|
||||||
|
return onComplete(url);
|
||||||
|
}
|
||||||
case "error":
|
case "error":
|
||||||
return setOperationError(resp.error);
|
return setOperationError(resp.error);
|
||||||
case "ok":
|
case "ok":
|
||||||
|
@ -66,6 +66,7 @@ import { TransferPickupPage } from "../cta/TransferPickup/index.js";
|
|||||||
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
|
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
|
||||||
import { RecoveryPage } from "../cta/Recovery/index.js";
|
import { RecoveryPage } from "../cta/Recovery/index.js";
|
||||||
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
|
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
|
||||||
|
import { NotificationsPage } from "./Notifications/index.js";
|
||||||
|
|
||||||
export function Application(): VNode {
|
export function Application(): VNode {
|
||||||
const [globalNotification, setGlobalNotification] = useState<
|
const [globalNotification, setGlobalNotification] = useState<
|
||||||
@ -206,6 +207,7 @@ export function Application(): VNode {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path={Pages.settings} component={SettingsPage} />
|
<Route path={Pages.settings} component={SettingsPage} />
|
||||||
|
<Route path={Pages.notifications} component={NotificationsPage} />
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* BACKUP
|
* BACKUP
|
||||||
@ -218,6 +220,12 @@ export function Application(): VNode {
|
|||||||
<Route
|
<Route
|
||||||
path={Pages.backupProviderDetail.pattern}
|
path={Pages.backupProviderDetail.pattern}
|
||||||
component={ProviderDetailPage}
|
component={ProviderDetailPage}
|
||||||
|
onPayProvider={(uri: string) =>
|
||||||
|
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
|
||||||
|
}
|
||||||
|
onWithdraw={(amount: string) =>
|
||||||
|
redirectTo(Pages.receiveCash({ amount }))
|
||||||
|
}
|
||||||
onBack={() => redirectTo(Pages.backup)}
|
onBack={() => redirectTo(Pages.backup)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@ -254,7 +262,7 @@ export function Application(): VNode {
|
|||||||
path={Pages.ctaPay}
|
path={Pages.ctaPay}
|
||||||
component={PaymentPage}
|
component={PaymentPage}
|
||||||
goToWalletManualWithdraw={(amount?: string) =>
|
goToWalletManualWithdraw={(amount?: string) =>
|
||||||
redirectTo(Pages.ctaWithdrawManual({ amount }))
|
redirectTo(Pages.receiveCash({ amount }))
|
||||||
}
|
}
|
||||||
cancel={() => redirectTo(Pages.balance)}
|
cancel={() => redirectTo(Pages.balance)}
|
||||||
onSuccess={(tid: string) =>
|
onSuccess={(tid: string) =>
|
||||||
@ -321,7 +329,7 @@ export function Application(): VNode {
|
|||||||
path={Pages.ctaInvoicePay}
|
path={Pages.ctaInvoicePay}
|
||||||
component={InvoicePayPage}
|
component={InvoicePayPage}
|
||||||
goToWalletManualWithdraw={(amount?: string) =>
|
goToWalletManualWithdraw={(amount?: string) =>
|
||||||
redirectTo(Pages.ctaWithdrawManual({ amount }))
|
redirectTo(Pages.receiveCash({ amount }))
|
||||||
}
|
}
|
||||||
onClose={() => redirectTo(Pages.balance)}
|
onClose={() => redirectTo(Pages.balance)}
|
||||||
onSuccess={(tid: string) =>
|
onSuccess={(tid: string) =>
|
||||||
|
@ -89,6 +89,7 @@ export const LotOfProviders = createExample(TestedComponent, {
|
|||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
paymentStatus: {
|
paymentStatus: {
|
||||||
type: ProviderPaymentType.Pending,
|
type: ProviderPaymentType.Pending,
|
||||||
|
talerUri: "taler://",
|
||||||
},
|
},
|
||||||
terms: {
|
terms: {
|
||||||
annualFee: "KUDOS:0.1",
|
annualFee: "KUDOS:0.1",
|
||||||
@ -103,6 +104,7 @@ export const LotOfProviders = createExample(TestedComponent, {
|
|||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
paymentStatus: {
|
paymentStatus: {
|
||||||
type: ProviderPaymentType.InsufficientBalance,
|
type: ProviderPaymentType.InsufficientBalance,
|
||||||
|
amount: "KUDOS:10",
|
||||||
},
|
},
|
||||||
terms: {
|
terms: {
|
||||||
annualFee: "KUDOS:0.1",
|
annualFee: "KUDOS:0.1",
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UserAttentionUnreadList } from "@gnu-taler/taler-util";
|
||||||
|
import { Loading } from "../../components/Loading.js";
|
||||||
|
import { HookError } from "../../hooks/useAsyncAsHook.js";
|
||||||
|
import { compose, StateViewMap } from "../../utils/index.js";
|
||||||
|
import { wxApi } from "../../wxApi.js";
|
||||||
|
import { useComponentState } from "./state.js";
|
||||||
|
import { LoadingUriView, ReadyView } from "./views.js";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
export type State = State.Loading | State.LoadingUriError | State.Ready;
|
||||||
|
|
||||||
|
export namespace State {
|
||||||
|
export interface Loading {
|
||||||
|
status: "loading";
|
||||||
|
error: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadingUriError {
|
||||||
|
status: "loading-error";
|
||||||
|
error: HookError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseInfo {
|
||||||
|
error: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ready extends BaseInfo {
|
||||||
|
status: "ready";
|
||||||
|
error: undefined;
|
||||||
|
list: UserAttentionUnreadList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewMapping: StateViewMap<State> = {
|
||||||
|
loading: Loading,
|
||||||
|
"loading-error": LoadingUriView,
|
||||||
|
ready: ReadyView,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationsPage = compose(
|
||||||
|
"NotificationsPage",
|
||||||
|
(p: Props) => useComponentState(p, wxApi),
|
||||||
|
viewMapping,
|
||||||
|
);
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
||||||
|
import { wxApi } from "../../wxApi.js";
|
||||||
|
import { Props, State } from "./index.js";
|
||||||
|
|
||||||
|
export function useComponentState({}: Props, api: typeof wxApi): State {
|
||||||
|
const hook = useAsyncAsHook(async () => {
|
||||||
|
return await api.wallet.call(
|
||||||
|
WalletApiOperation.GetUserAttentionRequests,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hook) {
|
||||||
|
return {
|
||||||
|
status: "loading",
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hook.hasError) {
|
||||||
|
return {
|
||||||
|
status: "loading-error",
|
||||||
|
error: hook,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "ready",
|
||||||
|
error: undefined,
|
||||||
|
list: hook.response.pending,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AbsoluteTime, AttentionType } from "@gnu-taler/taler-util";
|
||||||
|
import { createExample } from "../../test-utils.js";
|
||||||
|
import { ReadyView } from "./views.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "wallet/notifications",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Ready = createExample(ReadyView, {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
when: AbsoluteTime.now(),
|
||||||
|
read: false,
|
||||||
|
info: {
|
||||||
|
type: AttentionType.KycWithdrawal,
|
||||||
|
transactionId: "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: AbsoluteTime.now(),
|
||||||
|
read: false,
|
||||||
|
info: {
|
||||||
|
type: AttentionType.MerchantRefund,
|
||||||
|
transactionId: "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: AbsoluteTime.now(),
|
||||||
|
read: false,
|
||||||
|
info: {
|
||||||
|
type: AttentionType.BackupUnpaid,
|
||||||
|
provider_base_url: "http://sync.taler.net",
|
||||||
|
talerUri: "taler://payment/asdasdasd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
describe("test description", () => {
|
||||||
|
it("should assert", () => {
|
||||||
|
expect([]).deep.equals([]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2022 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AttentionInfo,
|
||||||
|
AttentionType,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { LoadingError } from "../../components/LoadingError.js";
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DateSeparator,
|
||||||
|
HistoryRow,
|
||||||
|
LargeText,
|
||||||
|
SmallLightText,
|
||||||
|
} from "../../components/styled/index.js";
|
||||||
|
import { Time } from "../../components/Time.js";
|
||||||
|
import { useTranslationContext } from "../../context/translation.js";
|
||||||
|
import { Avatar } from "../../mui/Avatar.js";
|
||||||
|
import { Button } from "../../mui/Button.js";
|
||||||
|
import { Grid } from "../../mui/Grid.js";
|
||||||
|
import { Pages } from "../../NavigationBar.js";
|
||||||
|
import { assertUnreachable } from "../../utils/index.js";
|
||||||
|
import { State } from "./index.js";
|
||||||
|
|
||||||
|
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingError
|
||||||
|
title={<i18n.Translate>Could not load notifications</i18n.Translate>}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const term = 1000 * 60 * 60 * 24;
|
||||||
|
function normalizeToDay(x: number): number {
|
||||||
|
return Math.round(x / term) * term;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadyView({ list }: State.Ready): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
if (list.length < 1) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<i18n.Translate>No notification left</i18n.Translate>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byDate = list.reduce((rv, x) => {
|
||||||
|
const theDate = x.when.t_ms === "never" ? 0 : normalizeToDay(x.when.t_ms);
|
||||||
|
if (theDate) {
|
||||||
|
(rv[theDate] = rv[theDate] || []).push(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}, {} as { [x: string]: typeof list });
|
||||||
|
const datesWithNotifications = Object.keys(byDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
{datesWithNotifications.map((d, i) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<DateSeparator>
|
||||||
|
<Time
|
||||||
|
timestamp={{ t_ms: Number.parseInt(d, 10) }}
|
||||||
|
format="dd MMMM yyyy"
|
||||||
|
/>
|
||||||
|
</DateSeparator>
|
||||||
|
{byDate[d].map((n, i) => (
|
||||||
|
<NotificationItem
|
||||||
|
key={i}
|
||||||
|
info={n.info}
|
||||||
|
isRead={n.read}
|
||||||
|
timestamp={n.when}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationItem({
|
||||||
|
info,
|
||||||
|
isRead,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
info: AttentionInfo;
|
||||||
|
timestamp: AbsoluteTime;
|
||||||
|
isRead: boolean;
|
||||||
|
}): VNode {
|
||||||
|
switch (info.type) {
|
||||||
|
case AttentionType.KycWithdrawal:
|
||||||
|
return (
|
||||||
|
<NotificationLayout
|
||||||
|
timestamp={timestamp}
|
||||||
|
href={Pages.balanceTransaction({ tid: info.transactionId })}
|
||||||
|
title="Withdrawal on hold"
|
||||||
|
subtitle="Know-your-customer validation is required"
|
||||||
|
iconPath={"K"}
|
||||||
|
isRead={isRead}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case AttentionType.MerchantRefund:
|
||||||
|
return (
|
||||||
|
<NotificationLayout
|
||||||
|
timestamp={timestamp}
|
||||||
|
href={Pages.balanceTransaction({ tid: info.transactionId })}
|
||||||
|
title="Merchant has refund your payment"
|
||||||
|
subtitle="Accept or deny refund"
|
||||||
|
iconPath={"K"}
|
||||||
|
isRead={isRead}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case AttentionType.BackupUnpaid:
|
||||||
|
return (
|
||||||
|
<NotificationLayout
|
||||||
|
timestamp={timestamp}
|
||||||
|
href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
|
||||||
|
title="Backup provider is unpaid"
|
||||||
|
subtitle="Complete the payment or remove the service provider"
|
||||||
|
iconPath={"K"}
|
||||||
|
isRead={isRead}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case AttentionType.AuditorDenominationsExpires:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.AuditorKeyExpires:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.AuditorTosChanged:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.ExchangeDenominationsExpired:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
// case AttentionType.ExchangeDenominationsExpiresSoon:
|
||||||
|
// return <div>not implemented</div>;
|
||||||
|
case AttentionType.ExchangeKeyExpired:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
// case AttentionType.ExchangeKeyExpiresSoon:
|
||||||
|
// return <div>not implemented</div>;
|
||||||
|
case AttentionType.ExchangeTosChanged:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.BackupExpiresSoon:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.PushPaymentReceived:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
case AttentionType.PullPaymentPaid:
|
||||||
|
return <div>not implemented</div>;
|
||||||
|
default:
|
||||||
|
assertUnreachable(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationLayout(props: {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
subtitle?: string;
|
||||||
|
timestamp: AbsoluteTime;
|
||||||
|
iconPath: string;
|
||||||
|
isRead: boolean;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
return (
|
||||||
|
<HistoryRow
|
||||||
|
href={props.href}
|
||||||
|
style={{
|
||||||
|
backgroundColor: props.isRead ? "lightcyan" : "inherit",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
style={{
|
||||||
|
border: "solid gray 1px",
|
||||||
|
color: "gray",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.iconPath}
|
||||||
|
</Avatar>
|
||||||
|
<Column>
|
||||||
|
<LargeText>
|
||||||
|
<div>{props.title}</div>
|
||||||
|
{props.subtitle && (
|
||||||
|
<div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
|
||||||
|
{props.subtitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</LargeText>
|
||||||
|
<SmallLightText style={{ marginTop: 5 }}>
|
||||||
|
<Time timestamp={props.timestamp} format="HH:mm" />
|
||||||
|
</SmallLightText>
|
||||||
|
</Column>
|
||||||
|
<Column>
|
||||||
|
<Grid>
|
||||||
|
<Button variant="outlined">
|
||||||
|
<i18n.Translate>Ignore</i18n.Translate>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Column>
|
||||||
|
</HistoryRow>
|
||||||
|
);
|
||||||
|
}
|
@ -174,6 +174,7 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
|
|||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
paymentStatus: {
|
paymentStatus: {
|
||||||
type: ProviderPaymentType.InsufficientBalance,
|
type: ProviderPaymentType.InsufficientBalance,
|
||||||
|
amount: "EUR:123",
|
||||||
},
|
},
|
||||||
terms: {
|
terms: {
|
||||||
annualFee: "EUR:0.1",
|
annualFee: "EUR:0.1",
|
||||||
@ -191,6 +192,7 @@ export const InactivePending = createExample(TestedComponent, {
|
|||||||
paymentProposalIds: [],
|
paymentProposalIds: [],
|
||||||
paymentStatus: {
|
paymentStatus: {
|
||||||
type: ProviderPaymentType.Pending,
|
type: ProviderPaymentType.Pending,
|
||||||
|
talerUri: "taler://pay/sad",
|
||||||
},
|
},
|
||||||
terms: {
|
terms: {
|
||||||
annualFee: "EUR:0.1",
|
annualFee: "EUR:0.1",
|
||||||
|
@ -36,9 +36,16 @@ import { wxApi } from "../wxApi.js";
|
|||||||
interface Props {
|
interface Props {
|
||||||
pid: string;
|
pid: string;
|
||||||
onBack: () => Promise<void>;
|
onBack: () => Promise<void>;
|
||||||
|
onPayProvider: (uri: string) => Promise<void>;
|
||||||
|
onWithdraw: (amount: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
export function ProviderDetailPage({
|
||||||
|
pid: providerURL,
|
||||||
|
onBack,
|
||||||
|
onPayProvider,
|
||||||
|
onWithdraw,
|
||||||
|
}: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
async function getProviderInfo(): Promise<ProviderInfo | null> {
|
async function getProviderInfo(): Promise<ProviderInfo | null> {
|
||||||
//create a first list of backup info by currency
|
//create a first list of backup info by currency
|
||||||
@ -71,11 +78,30 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const info = state.response;
|
||||||
|
if (info === null) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>
|
||||||
|
There is not known provider with url "{providerURL}".
|
||||||
|
</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
<Button variant="contained" color="secondary" onClick={onBack}>
|
||||||
|
<i18n.Translate>See providers</i18n.Translate>
|
||||||
|
</Button>
|
||||||
|
<div />
|
||||||
|
</footer>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProviderView
|
<ProviderView
|
||||||
url={providerURL}
|
info={info}
|
||||||
info={state.response}
|
|
||||||
onSync={async () =>
|
onSync={async () =>
|
||||||
wxApi.wallet
|
wxApi.wallet
|
||||||
.call(WalletApiOperation.RunBackupCycle, {
|
.call(WalletApiOperation.RunBackupCycle, {
|
||||||
@ -83,6 +109,16 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
|||||||
})
|
})
|
||||||
.then()
|
.then()
|
||||||
}
|
}
|
||||||
|
onPayProvider={async () => {
|
||||||
|
if (info.paymentStatus.type !== ProviderPaymentType.Pending) return;
|
||||||
|
if (!info.paymentStatus.talerUri) return;
|
||||||
|
onPayProvider(info.paymentStatus.talerUri);
|
||||||
|
}}
|
||||||
|
onWithdraw={async () => {
|
||||||
|
if (info.paymentStatus.type !== ProviderPaymentType.InsufficientBalance)
|
||||||
|
return;
|
||||||
|
onWithdraw(info.paymentStatus.amount);
|
||||||
|
}}
|
||||||
onDelete={() =>
|
onDelete={() =>
|
||||||
wxApi.wallet
|
wxApi.wallet
|
||||||
.call(WalletApiOperation.RemoveBackupProvider, {
|
.call(WalletApiOperation.RemoveBackupProvider, {
|
||||||
@ -99,42 +135,25 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewProps {
|
export interface ViewProps {
|
||||||
url: string;
|
info: ProviderInfo;
|
||||||
info: ProviderInfo | null;
|
|
||||||
onDelete: () => Promise<void>;
|
onDelete: () => Promise<void>;
|
||||||
onSync: () => Promise<void>;
|
onSync: () => Promise<void>;
|
||||||
onBack: () => Promise<void>;
|
onBack: () => Promise<void>;
|
||||||
onExtend: () => Promise<void>;
|
onExtend: () => Promise<void>;
|
||||||
|
onPayProvider: () => Promise<void>;
|
||||||
|
onWithdraw: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProviderView({
|
export function ProviderView({
|
||||||
info,
|
info,
|
||||||
url,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onPayProvider,
|
||||||
|
onWithdraw,
|
||||||
onSync,
|
onSync,
|
||||||
onBack,
|
onBack,
|
||||||
onExtend,
|
onExtend,
|
||||||
}: ViewProps): VNode {
|
}: ViewProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
if (info === null) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
<i18n.Translate>
|
|
||||||
There is not known provider with url "{url}".
|
|
||||||
</i18n.Translate>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<footer>
|
|
||||||
<Button variant="contained" color="secondary" onClick={onBack}>
|
|
||||||
<i18n.Translate>See providers</i18n.Translate>
|
|
||||||
</Button>
|
|
||||||
<div />
|
|
||||||
</footer>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const lb = info.lastSuccessfulBackupTimestamp
|
const lb = info.lastSuccessfulBackupTimestamp
|
||||||
? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
|
? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -230,6 +249,18 @@ export function ProviderView({
|
|||||||
<Button variant="contained" color="error" onClick={onDelete}>
|
<Button variant="contained" color="error" onClick={onDelete}>
|
||||||
<i18n.Translate>Remove provider</i18n.Translate>
|
<i18n.Translate>Remove provider</i18n.Translate>
|
||||||
</Button>
|
</Button>
|
||||||
|
{info.paymentStatus.type === ProviderPaymentType.Pending &&
|
||||||
|
info.paymentStatus.talerUri ? (
|
||||||
|
<Button variant="contained" color="primary" onClick={onPayProvider}>
|
||||||
|
<i18n.Translate>Pay</i18n.Translate>
|
||||||
|
</Button>
|
||||||
|
) : undefined}
|
||||||
|
{info.paymentStatus.type ===
|
||||||
|
ProviderPaymentType.InsufficientBalance ? (
|
||||||
|
<Button variant="contained" color="primary" onClick={onWithdraw}>
|
||||||
|
<i18n.Translate>Withdraw</i18n.Translate>
|
||||||
|
</Button>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -36,6 +36,7 @@ import * as a17 from "./QrReader.stories.js";
|
|||||||
import * as a18 from "./DestinationSelection.stories.js";
|
import * as a18 from "./DestinationSelection.stories.js";
|
||||||
import * as a19 from "./ExchangeSelection/stories.js";
|
import * as a19 from "./ExchangeSelection/stories.js";
|
||||||
import * as a20 from "./ManageAccount/stories.js";
|
import * as a20 from "./ManageAccount/stories.js";
|
||||||
|
import * as a21 from "./Notifications/stories.js";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
a1,
|
a1,
|
||||||
@ -55,4 +56,5 @@ export default [
|
|||||||
a18,
|
a18,
|
||||||
a19,
|
a19,
|
||||||
a20,
|
a20,
|
||||||
|
a21,
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user