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 {
|
||||
const pi = parseProtoInfo(s, talerActionPayPush);
|
||||
if (!pi) {
|
||||
|
@ -40,6 +40,7 @@ import {
|
||||
codecForAny,
|
||||
codecForBoolean,
|
||||
codecForConstString,
|
||||
codecForEither,
|
||||
codecForList,
|
||||
codecForMap,
|
||||
codecForNumber,
|
||||
@ -384,6 +385,7 @@ export enum PreparePayResultType {
|
||||
PaymentPossible = "payment-possible",
|
||||
InsufficientBalance = "insufficient-balance",
|
||||
AlreadyConfirmed = "already-confirmed",
|
||||
Lost = "lost",
|
||||
}
|
||||
|
||||
export const codecForPreparePayResultPaymentPossible =
|
||||
@ -394,6 +396,7 @@ export const codecForPreparePayResultPaymentPossible =
|
||||
.property("contractTerms", codecForMerchantContractTerms())
|
||||
.property("proposalId", codecForString())
|
||||
.property("contractTermsHash", codecForString())
|
||||
.property("talerUri", codecForString())
|
||||
.property("noncePriv", codecForString())
|
||||
.property(
|
||||
"status",
|
||||
@ -406,6 +409,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
||||
buildCodecForObject<PreparePayResultInsufficientBalance>()
|
||||
.property("amountRaw", codecForAmountString())
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("talerUri", codecForString())
|
||||
.property("proposalId", codecForString())
|
||||
.property("noncePriv", codecForString())
|
||||
.property(
|
||||
@ -424,11 +428,18 @@ export const codecForPreparePayResultAlreadyConfirmed =
|
||||
.property("amountEffective", codecForAmountString())
|
||||
.property("amountRaw", codecForAmountString())
|
||||
.property("paid", codecForBoolean())
|
||||
.property("talerUri", codecOptional(codecForString()))
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("contractTermsHash", codecForString())
|
||||
.property("proposalId", codecForString())
|
||||
.build("PreparePayResultAlreadyConfirmed");
|
||||
|
||||
export const codecForPreparePayResultPaymentLost =
|
||||
(): Codec<PreparePayResultPaymentLost> =>
|
||||
buildCodecForObject<PreparePayResultPaymentLost>()
|
||||
.property("status", codecForConstString(PreparePayResultType.Lost))
|
||||
.build("PreparePayResultLost");
|
||||
|
||||
export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
||||
buildCodecForUnion<PreparePayResult>()
|
||||
.discriminateOn("status")
|
||||
@ -444,6 +455,10 @@ export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
||||
PreparePayResultType.PaymentPossible,
|
||||
codecForPreparePayResultPaymentPossible(),
|
||||
)
|
||||
.alternative(
|
||||
PreparePayResultType.Lost,
|
||||
codecForPreparePayResultPaymentLost(),
|
||||
)
|
||||
.build("PreparePayResult");
|
||||
|
||||
/**
|
||||
@ -452,7 +467,8 @@ export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
|
||||
export type PreparePayResult =
|
||||
| PreparePayResultInsufficientBalance
|
||||
| PreparePayResultAlreadyConfirmed
|
||||
| PreparePayResultPaymentPossible;
|
||||
| PreparePayResultPaymentPossible
|
||||
| PreparePayResultPaymentLost;
|
||||
|
||||
/**
|
||||
* Payment is possible.
|
||||
@ -465,6 +481,7 @@ export interface PreparePayResultPaymentPossible {
|
||||
amountRaw: string;
|
||||
amountEffective: string;
|
||||
noncePriv: string;
|
||||
talerUri: string;
|
||||
}
|
||||
|
||||
export interface PreparePayResultInsufficientBalance {
|
||||
@ -473,6 +490,7 @@ export interface PreparePayResultInsufficientBalance {
|
||||
contractTerms: MerchantContractTerms;
|
||||
amountRaw: string;
|
||||
noncePriv: string;
|
||||
talerUri: string;
|
||||
}
|
||||
|
||||
export interface PreparePayResultAlreadyConfirmed {
|
||||
@ -483,6 +501,11 @@ export interface PreparePayResultAlreadyConfirmed {
|
||||
amountEffective: string;
|
||||
contractTermsHash: string;
|
||||
proposalId: string;
|
||||
talerUri?: string;
|
||||
}
|
||||
|
||||
export interface PreparePayResultPaymentLost {
|
||||
status: PreparePayResultType.Lost;
|
||||
}
|
||||
|
||||
export interface BankWithdrawDetails {
|
||||
@ -1677,6 +1700,170 @@ export interface WithdrawFakebankRequest {
|
||||
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 =
|
||||
(): Codec<WithdrawFakebankRequest> =>
|
||||
buildCodecForObject<WithdrawFakebankRequest>()
|
||||
|
@ -48,6 +48,9 @@ import {
|
||||
WireInfo,
|
||||
HashCodeString,
|
||||
Amounts,
|
||||
AttentionPriority,
|
||||
AttentionInfo,
|
||||
AbsoluteTime,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
describeContents,
|
||||
@ -1540,6 +1543,8 @@ export interface BackupProviderRecord {
|
||||
*/
|
||||
currentPaymentProposalId?: string;
|
||||
|
||||
shouldRetryFreshProposal: boolean;
|
||||
|
||||
/**
|
||||
* Proposals that were used to pay (or attempt to pay) the provider.
|
||||
*
|
||||
@ -1841,6 +1846,21 @@ export interface ContractTermsRecord {
|
||||
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
|
||||
* 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 {
|
||||
AbsoluteTime,
|
||||
AmountString,
|
||||
AttentionType,
|
||||
BackupRecovery,
|
||||
buildCodecForObject,
|
||||
buildCodecForUnion,
|
||||
@ -57,13 +58,17 @@ import {
|
||||
kdf,
|
||||
Logger,
|
||||
notEmpty,
|
||||
PaymentStatus,
|
||||
PreparePayResult,
|
||||
PreparePayResultType,
|
||||
RecoveryLoadRequest,
|
||||
RecoveryMergeStrategy,
|
||||
ReserveTransactionType,
|
||||
rsaBlind,
|
||||
secretbox,
|
||||
secretbox_open,
|
||||
stringToBytes,
|
||||
TalerErrorCode,
|
||||
TalerErrorDetail,
|
||||
TalerProtocolTimestamp,
|
||||
URL,
|
||||
@ -80,6 +85,7 @@ import {
|
||||
ConfigRecordKey,
|
||||
WalletBackupConfState,
|
||||
} from "../../db.js";
|
||||
import { TalerError } from "../../errors.js";
|
||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||
import {
|
||||
@ -96,6 +102,7 @@ import {
|
||||
RetryTags,
|
||||
scheduleRetryInTx,
|
||||
} from "../../util/retries.js";
|
||||
import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
|
||||
import {
|
||||
checkPaymentByProposalId,
|
||||
confirmPay,
|
||||
@ -198,6 +205,7 @@ async function computeBackupCryptoData(
|
||||
);
|
||||
}
|
||||
for (const purch of backupContent.purchases) {
|
||||
if (!purch.contract_terms_raw) continue;
|
||||
const { h: contractTermsHash } = await cryptoApi.hashString({
|
||||
str: canonicalJson(purch.contract_terms_raw),
|
||||
});
|
||||
@ -251,7 +259,7 @@ function getNextBackupTimestamp(): TalerProtocolTimestamp {
|
||||
async function runBackupCycleForProvider(
|
||||
ws: InternalWalletState,
|
||||
args: BackupForProviderArgs,
|
||||
): Promise<OperationAttemptResult<unknown, { talerUri: string }>> {
|
||||
): Promise<OperationAttemptResult<unknown, { talerUri?: string }>> {
|
||||
const provider = await ws.db
|
||||
.mktx((x) => [x.backupProviders])
|
||||
.runReadOnly(async (tx) => {
|
||||
@ -292,6 +300,10 @@ async function runBackupCycleForProvider(
|
||||
provider.baseUrl,
|
||||
);
|
||||
|
||||
if (provider.shouldRetryFreshProposal) {
|
||||
accountBackupUrl.searchParams.set("fresh", "yes");
|
||||
}
|
||||
|
||||
const resp = await ws.http.fetch(accountBackupUrl.href, {
|
||||
method: "POST",
|
||||
body: encBackup,
|
||||
@ -324,6 +336,12 @@ async function runBackupCycleForProvider(
|
||||
};
|
||||
await tx.backupProviders.put(prov);
|
||||
});
|
||||
|
||||
removeAttentionRequest(ws, {
|
||||
entityId: provider.baseUrl,
|
||||
type: AttentionType.BackupUnpaid,
|
||||
});
|
||||
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
result: undefined,
|
||||
@ -340,8 +358,51 @@ async function runBackupCycleForProvider(
|
||||
|
||||
//We can't delay downloading the proposal since we need the id
|
||||
//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
|
||||
.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.shouldRetryFreshProposal = true;
|
||||
prov.state = {
|
||||
tag: BackupProviderStateTag.Retrying,
|
||||
};
|
||||
await tx.backupProviders.put(prov);
|
||||
});
|
||||
|
||||
return {
|
||||
type: OperationAttemptResultType.Pending,
|
||||
result: {
|
||||
talerUri,
|
||||
},
|
||||
};
|
||||
}
|
||||
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])
|
||||
@ -353,13 +414,24 @@ async function runBackupCycleForProvider(
|
||||
}
|
||||
const opId = RetryTags.forBackup(prov);
|
||||
await scheduleRetryInTx(ws, tx, opId);
|
||||
prov.currentPaymentProposalId = res.proposalId;
|
||||
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: {
|
||||
@ -384,6 +456,12 @@ async function runBackupCycleForProvider(
|
||||
};
|
||||
await tx.backupProviders.put(prov);
|
||||
});
|
||||
|
||||
removeAttentionRequest(ws, {
|
||||
entityId: provider.baseUrl,
|
||||
type: AttentionType.BackupUnpaid,
|
||||
});
|
||||
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
result: undefined,
|
||||
@ -564,7 +642,7 @@ interface AddBackupProviderOk {
|
||||
}
|
||||
interface AddBackupProviderPaymentRequired {
|
||||
status: "payment-required";
|
||||
talerUri: string;
|
||||
talerUri?: string;
|
||||
}
|
||||
interface AddBackupProviderError {
|
||||
status: "error";
|
||||
@ -580,7 +658,7 @@ export const codecForAddBackupProviderPaymenrRequired =
|
||||
(): Codec<AddBackupProviderPaymentRequired> =>
|
||||
buildCodecForObject<AddBackupProviderPaymentRequired>()
|
||||
.property("status", codecForConstString("payment-required"))
|
||||
.property("talerUri", codecForString())
|
||||
.property("talerUri", codecOptional(codecForString()))
|
||||
.build("AddBackupProviderPaymentRequired");
|
||||
|
||||
export const codecForAddBackupProviderError =
|
||||
@ -655,6 +733,7 @@ export async function addBackupProvider(
|
||||
storageLimitInMegabytes: terms.storage_limit_in_megabytes,
|
||||
supportedProtocolVersion: terms.version,
|
||||
},
|
||||
shouldRetryFreshProposal: false,
|
||||
paymentProposalIds: [],
|
||||
baseUrl: canonUrl,
|
||||
uids: [encodeCrock(getRandomBytes(32))],
|
||||
@ -779,10 +858,12 @@ export interface ProviderPaymentUnpaid {
|
||||
|
||||
export interface ProviderPaymentInsufficientBalance {
|
||||
type: ProviderPaymentType.InsufficientBalance;
|
||||
amount: AmountString;
|
||||
}
|
||||
|
||||
export interface ProviderPaymentPending {
|
||||
type: ProviderPaymentType.Pending;
|
||||
talerUri?: string;
|
||||
}
|
||||
|
||||
export interface ProviderPaymentPaid {
|
||||
@ -810,32 +891,40 @@ async function getProviderPaymentInfo(
|
||||
ws,
|
||||
provider.currentPaymentProposalId,
|
||||
);
|
||||
if (status.status === PreparePayResultType.InsufficientBalance) {
|
||||
return {
|
||||
type: ProviderPaymentType.InsufficientBalance,
|
||||
};
|
||||
}
|
||||
if (status.status === PreparePayResultType.PaymentPossible) {
|
||||
return {
|
||||
type: ProviderPaymentType.Pending,
|
||||
};
|
||||
}
|
||||
if (status.status === PreparePayResultType.AlreadyConfirmed) {
|
||||
if (status.paid) {
|
||||
|
||||
switch (status.status) {
|
||||
case PreparePayResultType.InsufficientBalance:
|
||||
return {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: AbsoluteTime.addDuration(
|
||||
AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
|
||||
durationFromSpec({ years: 1 }),
|
||||
),
|
||||
type: ProviderPaymentType.InsufficientBalance,
|
||||
amount: status.amountRaw,
|
||||
};
|
||||
} else {
|
||||
case PreparePayResultType.PaymentPossible:
|
||||
return {
|
||||
type: ProviderPaymentType.Pending,
|
||||
talerUri: status.talerUri,
|
||||
};
|
||||
}
|
||||
case PreparePayResultType.Lost:
|
||||
return {
|
||||
type: ProviderPaymentType.Unpaid,
|
||||
};
|
||||
case PreparePayResultType.AlreadyConfirmed:
|
||||
if (status.paid) {
|
||||
return {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: AbsoluteTime.addDuration(
|
||||
AbsoluteTime.fromTimestamp(status.contractTerms.timestamp),
|
||||
durationFromSpec({ years: 1 }), //FIXME: take this from the contract term
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: ProviderPaymentType.Pending,
|
||||
talerUri: status.talerUri,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertUnreachable(status);
|
||||
}
|
||||
throw Error("not reached");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -936,6 +1025,7 @@ async function backupRecoveryTheirs(
|
||||
baseUrl: prov.url,
|
||||
name: prov.name,
|
||||
paymentProposalIds: [],
|
||||
shouldRetryFreshProposal: false,
|
||||
state: {
|
||||
tag: BackupProviderStateTag.Ready,
|
||||
nextBackupTimestamp: TalerProtocolTimestamp.now(),
|
||||
|
@ -72,6 +72,7 @@ import {
|
||||
TalerProtocolTimestamp,
|
||||
TransactionType,
|
||||
URL,
|
||||
constructPayUri,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -1290,7 +1291,10 @@ export async function checkPaymentByProposalId(
|
||||
return tx.purchases.get(proposalId);
|
||||
});
|
||||
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) {
|
||||
const existingProposalId = proposal.repurchaseProposalId;
|
||||
@ -1316,6 +1320,14 @@ export async function checkPaymentByProposalId(
|
||||
|
||||
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.
|
||||
const purchase = await ws.db
|
||||
.mktx((x) => [x.purchases])
|
||||
@ -1345,6 +1357,7 @@ export async function checkPaymentByProposalId(
|
||||
proposalId: proposal.proposalId,
|
||||
noncePriv: proposal.noncePriv,
|
||||
amountRaw: Amounts.stringify(d.contractData.amount),
|
||||
talerUri,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1360,6 +1373,7 @@ export async function checkPaymentByProposalId(
|
||||
amountEffective: Amounts.stringify(totalCost),
|
||||
amountRaw: Amounts.stringify(res.paymentAmount),
|
||||
contractTermsHash: d.contractData.contractTermsHash,
|
||||
talerUri,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1396,6 +1410,7 @@ export async function checkPaymentByProposalId(
|
||||
amountRaw: Amounts.stringify(download.contractData.amount),
|
||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||
proposalId,
|
||||
talerUri,
|
||||
};
|
||||
} else if (!purchase.timestampFirstSuccessfulPay) {
|
||||
const download = await expectProposalDownload(ws, purchase);
|
||||
@ -1407,6 +1422,7 @@ export async function checkPaymentByProposalId(
|
||||
amountRaw: Amounts.stringify(download.contractData.amount),
|
||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||
proposalId,
|
||||
talerUri,
|
||||
};
|
||||
} else {
|
||||
const paid =
|
||||
@ -1423,6 +1439,7 @@ export async function checkPaymentByProposalId(
|
||||
amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
|
||||
...(paid ? { nextUrl: download.contractData.orderId } : {}),
|
||||
proposalId,
|
||||
talerUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1468,7 +1485,7 @@ export async function preparePayForUri(
|
||||
);
|
||||
}
|
||||
|
||||
let proposalId = await startDownloadProposal(
|
||||
const proposalId = await startDownloadProposal(
|
||||
ws,
|
||||
uriResult.merchantBaseUrl,
|
||||
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) {
|
||||
const err = await readTalerErrorResponse(resp);
|
||||
if (
|
||||
|
@ -15,5 +15,5 @@
|
||||
*/
|
||||
|
||||
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,
|
||||
ListKnownBankAccountsRequest,
|
||||
ManualWithdrawalDetails,
|
||||
UserAttentionsCountResponse,
|
||||
UserAttentionsRequest,
|
||||
UserAttentionsResponse,
|
||||
PrepareDepositRequest,
|
||||
PrepareDepositResponse,
|
||||
PreparePayRequest,
|
||||
@ -102,6 +105,7 @@ import {
|
||||
WithdrawFakebankRequest,
|
||||
WithdrawTestBalanceRequest,
|
||||
WithdrawUriInfoResponse,
|
||||
UserAttentionByIdRequest,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { WalletContractData } from "./db.js";
|
||||
import {
|
||||
@ -133,6 +137,9 @@ export enum WalletApiOperation {
|
||||
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
||||
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
||||
GetBalances = "getBalances",
|
||||
GetUserAttentionRequests = "getUserAttentionRequests",
|
||||
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
|
||||
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
||||
GetPendingOperations = "getPendingOperations",
|
||||
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
||||
ApplyRefund = "applyRefund",
|
||||
@ -746,6 +753,33 @@ export type WithdrawFakebankOp = {
|
||||
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.
|
||||
*/
|
||||
@ -798,6 +832,9 @@ export type WalletOperations = {
|
||||
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
|
||||
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
|
||||
[WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
|
||||
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
|
||||
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
|
||||
[WalletApiOperation.MarkAttentionRequestAsRead]: MarkAttentionRequestAsRead;
|
||||
[WalletApiOperation.DumpCoins]: DumpCoinsOp;
|
||||
[WalletApiOperation.SetCoinSuspended]: SetCoinSuspendedOp;
|
||||
[WalletApiOperation.ForceRefresh]: ForceRefreshOp;
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
codecForInitiatePeerPushPaymentRequest,
|
||||
codecForIntegrationTestArgs,
|
||||
codecForListKnownBankAccounts,
|
||||
codecForUserAttentionsRequest,
|
||||
codecForPrepareDepositRequest,
|
||||
codecForPreparePayRequest,
|
||||
codecForPreparePeerPullPaymentRequest,
|
||||
@ -98,6 +99,7 @@ import {
|
||||
URL,
|
||||
WalletCoreVersion,
|
||||
WalletNotification,
|
||||
codecForUserAttentionByIdRequest,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -147,6 +149,11 @@ import {
|
||||
} from "./operations/backup/index.js";
|
||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
||||
import { getBalances } from "./operations/balance.js";
|
||||
import {
|
||||
getUserAttentions,
|
||||
getUserAttentionsUnreadCount,
|
||||
markAttentionRequestAsRead,
|
||||
} from "./operations/attention.js";
|
||||
import {
|
||||
getExchangeTosStatus,
|
||||
makeExchangeListItem,
|
||||
@ -1094,6 +1101,18 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
case WalletApiOperation.GetBalances: {
|
||||
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: {
|
||||
return await getPendingOperations(ws);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { h, VNode } from "preact";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import {
|
||||
NavigationHeader,
|
||||
NavigationHeaderHolder,
|
||||
@ -33,6 +33,11 @@ import {
|
||||
import { useTranslationContext } from "./context/translation.js";
|
||||
import settingsIcon from "./svg/settings_black_24dp.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
|
||||
@ -102,6 +107,7 @@ export const Pages = {
|
||||
backupProviderAdd: "/backup/provider/add",
|
||||
|
||||
qr: "/qr",
|
||||
notifications: "/notifications",
|
||||
settings: "/settings",
|
||||
settingsExchangeAdd: pageDefinition<{ currency?: string }>(
|
||||
"/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();
|
||||
return (
|
||||
<NavigationHeader>
|
||||
@ -141,6 +161,17 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
|
||||
<i18n.Translate>Backup</i18n.Translate>
|
||||
</a>
|
||||
<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}>
|
||||
<SvgIcon
|
||||
title={i18n.str`QR Reader and Taler URI`}
|
||||
@ -178,10 +209,16 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
|
||||
<i18n.Translate>Backup</i18n.Translate>
|
||||
</a>
|
||||
|
||||
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
|
||||
<i18n.Translate>Dev</i18n.Translate>
|
||||
<a href={Pages.notifications}>
|
||||
<i18n.Translate>Notifications</i18n.Translate>
|
||||
</a>
|
||||
|
||||
<JustInDevMode>
|
||||
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
|
||||
<i18n.Translate>Dev</i18n.Translate>
|
||||
</a>
|
||||
</JustInDevMode>
|
||||
|
||||
<div
|
||||
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
|
||||
>
|
||||
|
@ -50,7 +50,6 @@ function RenderAmount(): VNode {
|
||||
<AmountField
|
||||
required
|
||||
label={<i18n.Translate>Amount</i18n.Translate>}
|
||||
currency="USD"
|
||||
highestDenom={2000000}
|
||||
lowestDenom={0.01}
|
||||
handler={handler}
|
||||
|
@ -27,6 +27,7 @@ import { h, VNode } from "preact";
|
||||
import { useTranslationContext } from "../context/translation.js";
|
||||
import { Avatar } from "../mui/Avatar.js";
|
||||
import { Pages } from "../NavigationBar.js";
|
||||
import { assertUnreachable } from "../utils/index.js";
|
||||
import {
|
||||
Column,
|
||||
ExtraLargeText,
|
||||
@ -175,8 +176,7 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const pe: never = tx;
|
||||
throw Error(`unsupported transaction type ${pe}`);
|
||||
assertUnreachable(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ export function useComponentState(
|
||||
|
||||
const insufficientBalance: PreparePayResult = {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
talerUri: "taler://pay",
|
||||
proposalId: "fakeID",
|
||||
contractTerms: {} as any,
|
||||
amountRaw: hook.response.p2p.amount,
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
AmountJson,
|
||||
PreparePayResult,
|
||||
PreparePayResultAlreadyConfirmed,
|
||||
PreparePayResultInsufficientBalance,
|
||||
PreparePayResultPaymentPossible,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { Loading } from "../../components/Loading.js";
|
||||
@ -26,7 +27,7 @@ import { ButtonHandler } from "../../mui/handlers.js";
|
||||
import { compose, StateViewMap } from "../../utils/index.js";
|
||||
import { wxApi } from "../../wxApi.js";
|
||||
import { useComponentState } from "./state.js";
|
||||
import { BaseView, LoadingUriView } from "./views.js";
|
||||
import { BaseView, LoadingUriView, LostView } from "./views.js";
|
||||
|
||||
export interface Props {
|
||||
talerPayUri?: string;
|
||||
@ -40,6 +41,7 @@ export type State =
|
||||
| State.LoadingUriError
|
||||
| State.Ready
|
||||
| State.NoEnoughBalance
|
||||
| State.Lost
|
||||
| State.NoBalanceForCurrency
|
||||
| State.Confirmed;
|
||||
|
||||
@ -62,12 +64,15 @@ export namespace State {
|
||||
}
|
||||
export interface NoBalanceForCurrency extends BaseInfo {
|
||||
status: "no-balance-for-currency";
|
||||
payStatus: PreparePayResult;
|
||||
payStatus:
|
||||
| PreparePayResultInsufficientBalance
|
||||
| PreparePayResultPaymentPossible
|
||||
| PreparePayResultAlreadyConfirmed;
|
||||
balance: undefined;
|
||||
}
|
||||
export interface NoEnoughBalance extends BaseInfo {
|
||||
status: "no-enough-balance";
|
||||
payStatus: PreparePayResult;
|
||||
payStatus: PreparePayResultInsufficientBalance;
|
||||
balance: AmountJson;
|
||||
}
|
||||
export interface Ready extends BaseInfo {
|
||||
@ -77,6 +82,11 @@ export namespace State {
|
||||
balance: AmountJson;
|
||||
}
|
||||
|
||||
export interface Lost {
|
||||
status: "lost";
|
||||
error: undefined;
|
||||
}
|
||||
|
||||
export interface Confirmed extends BaseInfo {
|
||||
status: "confirmed";
|
||||
payStatus: PreparePayResultAlreadyConfirmed;
|
||||
@ -89,6 +99,7 @@ const viewMapping: StateViewMap<State> = {
|
||||
"loading-uri": LoadingUriView,
|
||||
"no-balance-for-currency": BaseView,
|
||||
"no-enough-balance": BaseView,
|
||||
lost: LostView,
|
||||
confirmed: BaseView,
|
||||
ready: BaseView,
|
||||
};
|
||||
|
@ -82,6 +82,14 @@ export function useComponentState(
|
||||
};
|
||||
}
|
||||
const { payStatus } = hook.response;
|
||||
|
||||
if (payStatus.status === PreparePayResultType.Lost) {
|
||||
return {
|
||||
status: "lost",
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
|
||||
|
||||
const foundBalance = hook.response.balance.balances.find(
|
||||
|
@ -44,6 +44,7 @@ export const NoBalance = createExample(BaseView, {
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
talerUri: "taler://pay/..",
|
||||
noncePriv: "",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
@ -73,6 +74,7 @@ export const NoEnoughBalance = createExample(BaseView, {
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
talerUri: "taler://pay/..",
|
||||
noncePriv: "",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
@ -102,6 +104,7 @@ export const EnoughBalanceButRestricted = createExample(BaseView, {
|
||||
uri: "",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
talerUri: "taler://pay/..",
|
||||
noncePriv: "",
|
||||
proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
|
||||
contractTerms: {
|
||||
@ -136,6 +139,7 @@ export const PaymentPossible = createExample(BaseView, {
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.PaymentPossible,
|
||||
talerUri: "taler://pay/..",
|
||||
amountEffective: "USD:10",
|
||||
amountRaw: "USD:10",
|
||||
noncePriv: "",
|
||||
@ -176,6 +180,7 @@ export const PaymentPossibleWithFee = createExample(BaseView, {
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.PaymentPossible,
|
||||
talerUri: "taler://pay/..",
|
||||
amountEffective: "USD:10.20",
|
||||
amountRaw: "USD:10",
|
||||
noncePriv: "",
|
||||
@ -213,6 +218,7 @@ export const TicketWithAProductList = createExample(BaseView, {
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.PaymentPossible,
|
||||
talerUri: "taler://pay/..",
|
||||
amountEffective: "USD:10.20",
|
||||
amountRaw: "USD:10",
|
||||
noncePriv: "",
|
||||
@ -269,6 +275,7 @@ export const TicketWithShipping = createExample(BaseView, {
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.PaymentPossible,
|
||||
talerUri: "taler://pay/..",
|
||||
amountEffective: "USD:10.20",
|
||||
amountRaw: "USD:10",
|
||||
noncePriv: "",
|
||||
@ -315,6 +322,7 @@ export const AlreadyConfirmedByOther = createExample(BaseView, {
|
||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||
payStatus: {
|
||||
status: PreparePayResultType.AlreadyConfirmed,
|
||||
talerUri: "taler://pay/..",
|
||||
amountEffective: "USD:10",
|
||||
amountRaw: "USD:10",
|
||||
contractTerms: {
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Amount } from "../../components/Amount.js";
|
||||
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
||||
import { LoadingError } from "../../components/LoadingError.js";
|
||||
import { LogoHeader } from "../../components/LogoHeader.js";
|
||||
import { Part } from "../../components/Part.js";
|
||||
@ -43,6 +44,7 @@ import { Time } from "../../components/Time.js";
|
||||
import { useTranslationContext } from "../../context/translation.js";
|
||||
import { Button } from "../../mui/Button.js";
|
||||
import { ButtonHandler } from "../../mui/handlers.js";
|
||||
import { assertUnreachable } from "../../utils/index.js";
|
||||
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
|
||||
import { State } from "./index.js";
|
||||
|
||||
@ -63,8 +65,24 @@ type SupportedStates =
|
||||
| State.NoBalanceForCurrency
|
||||
| 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 {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
||||
|
||||
const price = {
|
||||
@ -399,8 +417,9 @@ export function ButtonsSection({
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (payStatus.status === PreparePayResultType.Lost) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
const error: never = payStatus;
|
||||
|
||||
return <Fragment />;
|
||||
assertUnreachable(payStatus);
|
||||
}
|
||||
|
@ -150,6 +150,10 @@ export function Application(): VNode {
|
||||
component={RedirectToWalletPage}
|
||||
/>
|
||||
<Route path={Pages.dev} component={RedirectToWalletPage} />
|
||||
<Route
|
||||
path={Pages.notifications}
|
||||
component={RedirectToWalletPage}
|
||||
/>
|
||||
|
||||
<Route default component={Redirect} to={Pages.balance} />
|
||||
</Router>
|
||||
|
@ -171,7 +171,11 @@ export function useComponentState(
|
||||
|
||||
switch (resp.status) {
|
||||
case "payment-required":
|
||||
return onPaymentRequired(resp.talerUri);
|
||||
if (resp.talerUri) {
|
||||
return onPaymentRequired(resp.talerUri);
|
||||
} else {
|
||||
return onComplete(url);
|
||||
}
|
||||
case "error":
|
||||
return setOperationError(resp.error);
|
||||
case "ok":
|
||||
|
@ -66,6 +66,7 @@ import { TransferPickupPage } from "../cta/TransferPickup/index.js";
|
||||
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
|
||||
import { RecoveryPage } from "../cta/Recovery/index.js";
|
||||
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
|
||||
import { NotificationsPage } from "./Notifications/index.js";
|
||||
|
||||
export function Application(): VNode {
|
||||
const [globalNotification, setGlobalNotification] = useState<
|
||||
@ -206,6 +207,7 @@ export function Application(): VNode {
|
||||
/>
|
||||
|
||||
<Route path={Pages.settings} component={SettingsPage} />
|
||||
<Route path={Pages.notifications} component={NotificationsPage} />
|
||||
|
||||
{/**
|
||||
* BACKUP
|
||||
@ -218,6 +220,12 @@ export function Application(): VNode {
|
||||
<Route
|
||||
path={Pages.backupProviderDetail.pattern}
|
||||
component={ProviderDetailPage}
|
||||
onPayProvider={(uri: string) =>
|
||||
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
|
||||
}
|
||||
onWithdraw={(amount: string) =>
|
||||
redirectTo(Pages.receiveCash({ amount }))
|
||||
}
|
||||
onBack={() => redirectTo(Pages.backup)}
|
||||
/>
|
||||
<Route
|
||||
@ -254,7 +262,7 @@ export function Application(): VNode {
|
||||
path={Pages.ctaPay}
|
||||
component={PaymentPage}
|
||||
goToWalletManualWithdraw={(amount?: string) =>
|
||||
redirectTo(Pages.ctaWithdrawManual({ amount }))
|
||||
redirectTo(Pages.receiveCash({ amount }))
|
||||
}
|
||||
cancel={() => redirectTo(Pages.balance)}
|
||||
onSuccess={(tid: string) =>
|
||||
@ -321,7 +329,7 @@ export function Application(): VNode {
|
||||
path={Pages.ctaInvoicePay}
|
||||
component={InvoicePayPage}
|
||||
goToWalletManualWithdraw={(amount?: string) =>
|
||||
redirectTo(Pages.ctaWithdrawManual({ amount }))
|
||||
redirectTo(Pages.receiveCash({ amount }))
|
||||
}
|
||||
onClose={() => redirectTo(Pages.balance)}
|
||||
onSuccess={(tid: string) =>
|
||||
|
@ -89,6 +89,7 @@ export const LotOfProviders = createExample(TestedComponent, {
|
||||
paymentProposalIds: [],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Pending,
|
||||
talerUri: "taler://",
|
||||
},
|
||||
terms: {
|
||||
annualFee: "KUDOS:0.1",
|
||||
@ -103,6 +104,7 @@ export const LotOfProviders = createExample(TestedComponent, {
|
||||
paymentProposalIds: [],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.InsufficientBalance,
|
||||
amount: "KUDOS:10",
|
||||
},
|
||||
terms: {
|
||||
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: [],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.InsufficientBalance,
|
||||
amount: "EUR:123",
|
||||
},
|
||||
terms: {
|
||||
annualFee: "EUR:0.1",
|
||||
@ -191,6 +192,7 @@ export const InactivePending = createExample(TestedComponent, {
|
||||
paymentProposalIds: [],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Pending,
|
||||
talerUri: "taler://pay/sad",
|
||||
},
|
||||
terms: {
|
||||
annualFee: "EUR:0.1",
|
||||
|
@ -36,9 +36,16 @@ import { wxApi } from "../wxApi.js";
|
||||
interface Props {
|
||||
pid: string;
|
||||
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();
|
||||
async function getProviderInfo(): Promise<ProviderInfo | null> {
|
||||
//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 (
|
||||
<ProviderView
|
||||
url={providerURL}
|
||||
info={state.response}
|
||||
info={info}
|
||||
onSync={async () =>
|
||||
wxApi.wallet
|
||||
.call(WalletApiOperation.RunBackupCycle, {
|
||||
@ -83,6 +109,16 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
||||
})
|
||||
.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={() =>
|
||||
wxApi.wallet
|
||||
.call(WalletApiOperation.RemoveBackupProvider, {
|
||||
@ -99,42 +135,25 @@ export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
url: string;
|
||||
info: ProviderInfo | null;
|
||||
info: ProviderInfo;
|
||||
onDelete: () => Promise<void>;
|
||||
onSync: () => Promise<void>;
|
||||
onBack: () => Promise<void>;
|
||||
onExtend: () => Promise<void>;
|
||||
onPayProvider: () => Promise<void>;
|
||||
onWithdraw: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function ProviderView({
|
||||
info,
|
||||
url,
|
||||
onDelete,
|
||||
onPayProvider,
|
||||
onWithdraw,
|
||||
onSync,
|
||||
onBack,
|
||||
onExtend,
|
||||
}: ViewProps): VNode {
|
||||
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
|
||||
? AbsoluteTime.fromTimestamp(info.lastSuccessfulBackupTimestamp)
|
||||
: undefined;
|
||||
@ -230,6 +249,18 @@ export function ProviderView({
|
||||
<Button variant="contained" color="error" onClick={onDelete}>
|
||||
<i18n.Translate>Remove provider</i18n.Translate>
|
||||
</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>
|
||||
</footer>
|
||||
</Fragment>
|
||||
|
@ -36,6 +36,7 @@ import * as a17 from "./QrReader.stories.js";
|
||||
import * as a18 from "./DestinationSelection.stories.js";
|
||||
import * as a19 from "./ExchangeSelection/stories.js";
|
||||
import * as a20 from "./ManageAccount/stories.js";
|
||||
import * as a21 from "./Notifications/stories.js";
|
||||
|
||||
export default [
|
||||
a1,
|
||||
@ -55,4 +56,5 @@ export default [
|
||||
a18,
|
||||
a19,
|
||||
a20,
|
||||
a21,
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user