mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
fix(core): report receiver failures to sender
This commit is contained in:
@@ -88,6 +88,7 @@ enum ReceiverDeliveryStatus: Equatable, Sendable {
|
|||||||
case refused
|
case refused
|
||||||
case expired
|
case expired
|
||||||
case completed
|
case completed
|
||||||
|
case failed
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ private extension ReceiverRequest {
|
|||||||
case "refused": return .refused
|
case "refused": return .refused
|
||||||
case "expired": return .expired
|
case "expired": return .expired
|
||||||
case "completed": return .completed
|
case "completed": return .completed
|
||||||
|
case "failed": return .failed
|
||||||
default: return .unknown
|
default: return .unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ private struct ReceiverRow: View {
|
|||||||
let name = receiver.receiverName ?? receiver.receiverDeviceName ?? String(localized: "transfer_nearby_device")
|
let name = receiver.receiverName ?? receiver.receiverDeviceName ?? String(localized: "transfer_nearby_device")
|
||||||
let showLive = sendProgress != nil && receiver.status != .completed
|
let showLive = sendProgress != nil && receiver.status != .completed
|
||||||
&& receiver.status != .refused && receiver.status != .expired
|
&& receiver.status != .refused && receiver.status != .expired
|
||||||
|
&& receiver.status != .failed
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(name).font(VniType.bodyLarge).lineLimit(1)
|
Text(name).font(VniType.bodyLarge).lineLimit(1)
|
||||||
@@ -397,6 +398,7 @@ extension ReceiverDeliveryStatus {
|
|||||||
case .refused: return "transfer_receiver_refused"
|
case .refused: return "transfer_receiver_refused"
|
||||||
case .expired: return "transfer_receiver_expired"
|
case .expired: return "transfer_receiver_expired"
|
||||||
case .completed: return "transfer_receiver_completed"
|
case .completed: return "transfer_receiver_completed"
|
||||||
|
case .failed: return "transfer_receiver_failed"
|
||||||
case .unknown: return "transfer_receiver_unknown"
|
case .unknown: return "transfer_receiver_unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +406,7 @@ extension ReceiverDeliveryStatus {
|
|||||||
func statusColor(_ colors: VniDropColors) -> Color {
|
func statusColor(_ colors: VniDropColors) -> Color {
|
||||||
switch self {
|
switch self {
|
||||||
case .completed: return colors.brandDefault
|
case .completed: return colors.brandDefault
|
||||||
case .refused, .expired: return colors.destructiveDefault
|
case .refused, .expired, .failed: return colors.destructiveDefault
|
||||||
default: return colors.foregroundLighter
|
default: return colors.foregroundLighter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16581,6 +16581,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"transfer_receiver_failed": {
|
||||||
|
"comment": "Receiver status: the transfer failed.",
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Übertragung fehlgeschlagen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Transfer failed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Transferencia fallida"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Transfert échoué"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Trasferimento non riuscito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Overdracht mislukt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Przesyłanie nie powiodło się"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "A transferência falhou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Передача не удалась"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"transfer_receiver_expired": {
|
"transfer_receiver_expired": {
|
||||||
"comment": "Receiver status: the request expired.",
|
"comment": "Receiver status: the request expired.",
|
||||||
"extractionState": "manual",
|
"extractionState": "manual",
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
access_policy::{AccessPolicy, APPROVAL_SESSION_TTL_MS},
|
access_policy::{AccessPolicy, APPROVAL_SESSION_TTL_MS},
|
||||||
event_hub::EventHub,
|
event_hub::EventHub,
|
||||||
handshake::{DeliveryReceipt, DeliveryReceiptResponse, HandshakeResponse, RequestTransfer},
|
handshake::{
|
||||||
|
DeliveryFailureReceipt, DeliveryReceipt, DeliveryReceiptResponse, HandshakeResponse,
|
||||||
|
RequestTransfer,
|
||||||
|
},
|
||||||
repository::{ReceiverRequestInsert, Repository},
|
repository::{ReceiverRequestInsert, Repository},
|
||||||
transfer_state::ReceiverRequestStatus,
|
transfer_state::ReceiverRequestStatus,
|
||||||
util::now_ms,
|
util::now_ms,
|
||||||
@@ -70,6 +73,46 @@ impl ApprovalService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fail_delivery(
|
||||||
|
&self,
|
||||||
|
remote_endpoint_id: String,
|
||||||
|
receipt: DeliveryFailureReceipt,
|
||||||
|
) -> DeliveryReceiptResponse {
|
||||||
|
let token_hash = receipt_token_hash(&receipt.token);
|
||||||
|
match self
|
||||||
|
.repository
|
||||||
|
.fail_receiver_delivery(
|
||||||
|
&receipt.request_id,
|
||||||
|
receipt.transfer_id,
|
||||||
|
&remote_endpoint_id,
|
||||||
|
&token_hash,
|
||||||
|
&receipt.reason,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
self.event_hub.emit_transfer(
|
||||||
|
receipt.transfer_id,
|
||||||
|
"send",
|
||||||
|
"delivery",
|
||||||
|
"receiver-failed",
|
||||||
|
json!({
|
||||||
|
"request_id": receipt.request_id,
|
||||||
|
"remote_endpoint_id": remote_endpoint_id,
|
||||||
|
"reason": receipt.reason,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
DeliveryReceiptResponse::Recorded
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "rejected receiver delivery failure");
|
||||||
|
DeliveryReceiptResponse::Rejected {
|
||||||
|
reason: "invalid-receipt".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
event_hub: Arc<EventHub>,
|
event_hub: Arc<EventHub>,
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ impl ProtocolHandler for HandshakeService {
|
|||||||
.await;
|
.await;
|
||||||
let _ = tx.send(response).await;
|
let _ = tx.send(response).await;
|
||||||
}
|
}
|
||||||
|
HandshakeMessage::ReportDeliveryFailure(message) => {
|
||||||
|
let WithChannels { inner, tx, .. } = message;
|
||||||
|
let response = self
|
||||||
|
.approval
|
||||||
|
.fail_delivery(remote_endpoint_id.clone(), inner)
|
||||||
|
.await;
|
||||||
|
let _ = tx.send(response).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +117,13 @@ impl HandshakeClient {
|
|||||||
) -> Result<DeliveryReceiptResponse, irpc::Error> {
|
) -> Result<DeliveryReceiptResponse, irpc::Error> {
|
||||||
self.inner.rpc(receipt).await
|
self.inner.rpc(receipt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn report_delivery_failure(
|
||||||
|
&self,
|
||||||
|
receipt: DeliveryFailureReceipt,
|
||||||
|
) -> Result<DeliveryReceiptResponse, irpc::Error> {
|
||||||
|
self.inner.rpc(receipt).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -140,6 +155,14 @@ pub(crate) struct DeliveryReceipt {
|
|||||||
pub(crate) token: String,
|
pub(crate) token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct DeliveryFailureReceipt {
|
||||||
|
pub(crate) request_id: String,
|
||||||
|
pub(crate) transfer_id: u64,
|
||||||
|
pub(crate) token: String,
|
||||||
|
pub(crate) reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub(crate) enum DeliveryReceiptResponse {
|
pub(crate) enum DeliveryReceiptResponse {
|
||||||
Recorded,
|
Recorded,
|
||||||
@@ -153,4 +176,6 @@ enum HandshakeProtocol {
|
|||||||
RequestTransfer(RequestTransfer),
|
RequestTransfer(RequestTransfer),
|
||||||
#[rpc(tx=oneshot::Sender<DeliveryReceiptResponse>)]
|
#[rpc(tx=oneshot::Sender<DeliveryReceiptResponse>)]
|
||||||
ReportDelivery(DeliveryReceipt),
|
ReportDelivery(DeliveryReceipt),
|
||||||
|
#[rpc(tx=oneshot::Sender<DeliveryReceiptResponse>)]
|
||||||
|
ReportDeliveryFailure(DeliveryFailureReceipt),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::{
|
|||||||
util::now_ms,
|
util::now_ms,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCHEMA_VERSION: i64 = 6;
|
const SCHEMA_VERSION: i64 = 7;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Repository {
|
pub(crate) struct Repository {
|
||||||
@@ -86,6 +86,7 @@ pub(crate) struct PendingDeliveryReceipt {
|
|||||||
pub(crate) request_id: String,
|
pub(crate) request_id: String,
|
||||||
pub(crate) sender_transfer_id: u64,
|
pub(crate) sender_transfer_id: u64,
|
||||||
pub(crate) token: String,
|
pub(crate) token: String,
|
||||||
|
pub(crate) failure_reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct PendingDeliveryReceiptInsert<'a> {
|
pub(crate) struct PendingDeliveryReceiptInsert<'a> {
|
||||||
@@ -94,6 +95,7 @@ pub(crate) struct PendingDeliveryReceiptInsert<'a> {
|
|||||||
pub(crate) request_id: &'a str,
|
pub(crate) request_id: &'a str,
|
||||||
pub(crate) sender_transfer_id: u64,
|
pub(crate) sender_transfer_id: u64,
|
||||||
pub(crate) token: &'a str,
|
pub(crate) token: &'a str,
|
||||||
|
pub(crate) failure_reason: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
@@ -294,12 +296,24 @@ impl Repository {
|
|||||||
sender_blob_ticket TEXT NOT NULL,
|
sender_blob_ticket TEXT NOT NULL,
|
||||||
sender_transfer_id INTEGER NOT NULL,
|
sender_transfer_id INTEGER NOT NULL,
|
||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL,
|
||||||
|
failure_reason TEXT,
|
||||||
created_at INTEGER NOT NULL
|
created_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
let receipt_columns = sqlx::query("PRAGMA table_info(pending_delivery_receipts)")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
if !receipt_columns
|
||||||
|
.iter()
|
||||||
|
.any(|row| row.get::<String, _>(1) == "failure_reason")
|
||||||
|
{
|
||||||
|
sqlx::query("ALTER TABLE pending_delivery_receipts ADD COLUMN failure_reason TEXT")
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query(&format!("PRAGMA user_version = {SCHEMA_VERSION}"))
|
sqlx::query(&format!("PRAGMA user_version = {SCHEMA_VERSION}"))
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
@@ -561,13 +575,14 @@ impl Repository {
|
|||||||
r#"
|
r#"
|
||||||
INSERT INTO pending_delivery_receipts (
|
INSERT INTO pending_delivery_receipts (
|
||||||
request_id, local_transfer_id, sender_blob_ticket,
|
request_id, local_transfer_id, sender_blob_ticket,
|
||||||
sender_transfer_id, token, created_at
|
sender_transfer_id, token, failure_reason, created_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
ON CONFLICT(request_id) DO UPDATE SET
|
ON CONFLICT(request_id) DO UPDATE SET
|
||||||
local_transfer_id = excluded.local_transfer_id,
|
local_transfer_id = excluded.local_transfer_id,
|
||||||
sender_blob_ticket = excluded.sender_blob_ticket,
|
sender_blob_ticket = excluded.sender_blob_ticket,
|
||||||
sender_transfer_id = excluded.sender_transfer_id,
|
sender_transfer_id = excluded.sender_transfer_id,
|
||||||
token = excluded.token
|
token = excluded.token,
|
||||||
|
failure_reason = excluded.failure_reason
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(receipt.request_id)
|
.bind(receipt.request_id)
|
||||||
@@ -575,6 +590,7 @@ impl Repository {
|
|||||||
.bind(receipt.sender_blob_ticket)
|
.bind(receipt.sender_blob_ticket)
|
||||||
.bind(to_db_id(receipt.sender_transfer_id)?)
|
.bind(to_db_id(receipt.sender_transfer_id)?)
|
||||||
.bind(receipt.token)
|
.bind(receipt.token)
|
||||||
|
.bind(receipt.failure_reason)
|
||||||
.bind(now_ms())
|
.bind(now_ms())
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -582,13 +598,46 @@ impl Repository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn queue_failed_delivery_receipt(
|
||||||
|
&self,
|
||||||
|
receipt: PendingDeliveryReceiptInsert<'_>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(reason) = receipt.failure_reason else {
|
||||||
|
anyhow::bail!("failed delivery receipt requires a reason");
|
||||||
|
};
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO pending_delivery_receipts (
|
||||||
|
request_id, local_transfer_id, sender_blob_ticket,
|
||||||
|
sender_transfer_id, token, failure_reason, created_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
|
ON CONFLICT(request_id) DO UPDATE SET
|
||||||
|
local_transfer_id = excluded.local_transfer_id,
|
||||||
|
sender_blob_ticket = excluded.sender_blob_ticket,
|
||||||
|
sender_transfer_id = excluded.sender_transfer_id,
|
||||||
|
token = excluded.token,
|
||||||
|
failure_reason = excluded.failure_reason
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(receipt.request_id)
|
||||||
|
.bind(to_db_id(receipt.local_transfer_id)?)
|
||||||
|
.bind(receipt.sender_blob_ticket)
|
||||||
|
.bind(to_db_id(receipt.sender_transfer_id)?)
|
||||||
|
.bind(receipt.token)
|
||||||
|
.bind(reason)
|
||||||
|
.bind(now_ms())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_pending_delivery_receipts(
|
pub(crate) async fn list_pending_delivery_receipts(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<PendingDeliveryReceipt>> {
|
) -> Result<Vec<PendingDeliveryReceipt>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT local_transfer_id, sender_blob_ticket, request_id,
|
SELECT local_transfer_id, sender_blob_ticket, request_id,
|
||||||
sender_transfer_id, token
|
sender_transfer_id, token, failure_reason
|
||||||
FROM pending_delivery_receipts
|
FROM pending_delivery_receipts
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
"#,
|
"#,
|
||||||
@@ -603,6 +652,7 @@ impl Repository {
|
|||||||
request_id: row.get("request_id"),
|
request_id: row.get("request_id"),
|
||||||
sender_transfer_id: row.get::<i64, _>("sender_transfer_id") as u64,
|
sender_transfer_id: row.get::<i64, _>("sender_transfer_id") as u64,
|
||||||
token: row.get("token"),
|
token: row.get("token"),
|
||||||
|
failure_reason: row.get("failure_reason"),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
@@ -883,6 +933,56 @@ impl Repository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fail_receiver_delivery(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
transfer_id: u64,
|
||||||
|
remote_endpoint_id: &str,
|
||||||
|
token_hash: &str,
|
||||||
|
reason: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE receiver_requests
|
||||||
|
SET status = 'failed', reason = ?1
|
||||||
|
WHERE id = ?2 AND transfer_id = ?3 AND remote_endpoint_id = ?4 AND receipt_token_hash = ?5
|
||||||
|
AND status = 'accepted'
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(reason)
|
||||||
|
.bind(id)
|
||||||
|
.bind(to_db_id(transfer_id)?)
|
||||||
|
.bind(remote_endpoint_id)
|
||||||
|
.bind(token_hash)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
if result.rows_affected() == 1 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let already_recorded = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM receiver_requests
|
||||||
|
WHERE id = ?1 AND transfer_id = ?2 AND remote_endpoint_id = ?3
|
||||||
|
AND receipt_token_hash = ?4 AND status = 'failed'
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(to_db_id(transfer_id)?)
|
||||||
|
.bind(remote_endpoint_id)
|
||||||
|
.bind(token_hash)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?
|
||||||
|
.get::<i64, _>(0)
|
||||||
|
!= 0;
|
||||||
|
if already_recorded {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("delivery failure did not match an accepted receiver request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn expire_pending_receiver_requests(&self, reason: &str) -> Result<u64> {
|
pub(crate) async fn expire_pending_receiver_requests(&self, reason: &str) -> Result<u64> {
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use serde_json::json;
|
|||||||
|
|
||||||
use super::{filter_peer_addr_for_relay_mode, CoreInner};
|
use super::{filter_peer_addr_for_relay_mode, CoreInner};
|
||||||
use crate::{
|
use crate::{
|
||||||
handshake::{DeliveryReceipt, DeliveryReceiptResponse, HandshakeService},
|
handshake::{
|
||||||
|
DeliveryFailureReceipt, DeliveryReceipt, DeliveryReceiptResponse, HandshakeService,
|
||||||
|
},
|
||||||
repository::PendingDeliveryReceipt,
|
repository::PendingDeliveryReceipt,
|
||||||
ticket::parse_persisted_sender_address,
|
ticket::parse_persisted_sender_address,
|
||||||
};
|
};
|
||||||
@@ -96,13 +98,29 @@ impl CoreInner {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let client = HandshakeService::client(self.endpoint.clone(), sender_addr);
|
let client = HandshakeService::client(self.endpoint.clone(), sender_addr);
|
||||||
let receipt = DeliveryReceipt {
|
let request_id = pending.request_id.clone();
|
||||||
request_id: pending.request_id.clone(),
|
let response = tokio::time::timeout(DELIVERY_RECEIPT_TIMEOUT, async {
|
||||||
|
if let Some(reason) = pending.failure_reason {
|
||||||
|
client
|
||||||
|
.report_delivery_failure(DeliveryFailureReceipt {
|
||||||
|
request_id,
|
||||||
transfer_id: pending.sender_transfer_id,
|
transfer_id: pending.sender_transfer_id,
|
||||||
token: pending.token,
|
token: pending.token,
|
||||||
};
|
reason,
|
||||||
match tokio::time::timeout(DELIVERY_RECEIPT_TIMEOUT, client.report_delivery(receipt)).await
|
})
|
||||||
{
|
.await
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.report_delivery(DeliveryReceipt {
|
||||||
|
request_id,
|
||||||
|
transfer_id: pending.sender_transfer_id,
|
||||||
|
token: pending.token,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match response {
|
||||||
Ok(Ok(DeliveryReceiptResponse::Recorded)) => {
|
Ok(Ok(DeliveryReceiptResponse::Recorded)) => {
|
||||||
if let Err(error) = self
|
if let Err(error) = self
|
||||||
.repository
|
.repository
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -222,6 +222,10 @@ impl CoreInner {
|
|||||||
self.persist_receive_start(transfer_id, &parsed, receiver_name.as_deref())
|
self.persist_receive_start(transfer_id, &parsed, receiver_name.as_deref())
|
||||||
.await
|
.await
|
||||||
.map_err(VnidropError::repository)?;
|
.map_err(VnidropError::repository)?;
|
||||||
|
let persisted_sender_address =
|
||||||
|
encode_persisted_sender_address(parsed.blob_ticket.addr())
|
||||||
|
.context("failed to encode sender address for delivery receipt")?;
|
||||||
|
let delivery_receipt = Arc::new(Mutex::new(None));
|
||||||
// Cancellation is cooperative: it stops our receive future and marks
|
// Cancellation is cooperative: it stops our receive future and marks
|
||||||
// local state while lower-level Iroh work unwinds naturally.
|
// local state while lower-level Iroh work unwinds naturally.
|
||||||
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
|
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
|
||||||
@@ -237,7 +241,14 @@ impl CoreInner {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let (result, cancelled) = tokio::select! {
|
let (result, cancelled) = tokio::select! {
|
||||||
result = self.receive_inner(transfer_id, parsed, target, receiver_name) => {
|
result = self.receive_inner(
|
||||||
|
transfer_id,
|
||||||
|
parsed,
|
||||||
|
target,
|
||||||
|
receiver_name,
|
||||||
|
persisted_sender_address.clone(),
|
||||||
|
delivery_receipt.clone(),
|
||||||
|
) => {
|
||||||
(result.map_err(VnidropError::transfer), false)
|
(result.map_err(VnidropError::transfer), false)
|
||||||
},
|
},
|
||||||
_ = &mut shutdown_rx => (Err(VnidropError::cancelled("transfer cancelled")), true),
|
_ = &mut shutdown_rx => (Err(VnidropError::cancelled("transfer cancelled")), true),
|
||||||
@@ -267,6 +278,34 @@ impl CoreInner {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
let receipt = delivery_receipt.lock().expect("delivery_receipt").take();
|
||||||
|
if let Some(receipt) = receipt {
|
||||||
|
let reason = if cancelled { "cancelled" } else { error.code() };
|
||||||
|
match self
|
||||||
|
.repository
|
||||||
|
.queue_failed_delivery_receipt(PendingDeliveryReceiptInsert {
|
||||||
|
local_transfer_id: transfer_id,
|
||||||
|
sender_blob_ticket: &persisted_sender_address,
|
||||||
|
request_id: &receipt.request_id,
|
||||||
|
sender_transfer_id: receipt.transfer_id,
|
||||||
|
token: &receipt.token,
|
||||||
|
failure_reason: Some(reason),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => self.delivery_receipt_notify.notify_one(),
|
||||||
|
Err(queue_error) => {
|
||||||
|
tracing::warn!(%queue_error, "failed to queue delivery failure receipt");
|
||||||
|
self.emit_transfer(
|
||||||
|
transfer_id,
|
||||||
|
"receive",
|
||||||
|
"delivery",
|
||||||
|
"receipt-failed",
|
||||||
|
json!({ "reason": queue_error.to_string() }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.map_err(anyhow::Error::new)
|
result.map_err(anyhow::Error::new)
|
||||||
}
|
}
|
||||||
@@ -277,6 +316,8 @@ impl CoreInner {
|
|||||||
parsed: ParsedTransferTicket,
|
parsed: ParsedTransferTicket,
|
||||||
target: ReceiveTarget,
|
target: ReceiveTarget,
|
||||||
receiver_name: Option<String>,
|
receiver_name: Option<String>,
|
||||||
|
persisted_sender_address: String,
|
||||||
|
pending_delivery_receipt: Arc<Mutex<Option<DeliveryReceipt>>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let ReceiveTarget::Directory(output_dir) = &target {
|
if let ReceiveTarget::Directory(output_dir) = &target {
|
||||||
tokio::fs::create_dir_all(output_dir)
|
tokio::fs::create_dir_all(output_dir)
|
||||||
@@ -284,8 +325,6 @@ impl CoreInner {
|
|||||||
.map_err(VnidropError::filesystem)?;
|
.map_err(VnidropError::filesystem)?;
|
||||||
}
|
}
|
||||||
let sender_addr = parsed.blob_ticket.addr().clone();
|
let sender_addr = parsed.blob_ticket.addr().clone();
|
||||||
let persisted_sender_address = encode_persisted_sender_address(&sender_addr)
|
|
||||||
.context("failed to encode sender address for delivery receipt")?;
|
|
||||||
|
|
||||||
self.emit_transfer(transfer_id, "receive", "network", "connecting", json!({}));
|
self.emit_transfer(transfer_id, "receive", "network", "connecting", json!({}));
|
||||||
// Every VniDrop ticket carries metadata and must complete the handshake.
|
// Every VniDrop ticket carries metadata and must complete the handshake.
|
||||||
@@ -297,6 +336,9 @@ impl CoreInner {
|
|||||||
receiver_name.as_deref(),
|
receiver_name.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
*pending_delivery_receipt
|
||||||
|
.lock()
|
||||||
|
.expect("pending_delivery_receipt") = Some(delivery_receipt.clone());
|
||||||
let connection = self
|
let connection = self
|
||||||
.endpoint
|
.endpoint
|
||||||
.connect(sender_addr.clone(), iroh_blobs::ALPN)
|
.connect(sender_addr.clone(), iroh_blobs::ALPN)
|
||||||
@@ -373,9 +415,14 @@ impl CoreInner {
|
|||||||
request_id: &delivery_receipt.request_id,
|
request_id: &delivery_receipt.request_id,
|
||||||
sender_transfer_id: delivery_receipt.transfer_id,
|
sender_transfer_id: delivery_receipt.transfer_id,
|
||||||
token: &delivery_receipt.token,
|
token: &delivery_receipt.token,
|
||||||
|
failure_reason: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(VnidropError::repository)?;
|
.map_err(VnidropError::repository)?;
|
||||||
|
pending_delivery_receipt
|
||||||
|
.lock()
|
||||||
|
.expect("pending_delivery_receipt")
|
||||||
|
.take();
|
||||||
drop(download_tag);
|
drop(download_tag);
|
||||||
self.emit_transfer(transfer_id, "receive", "lifecycle", "done", json!({}));
|
self.emit_transfer(transfer_id, "receive", "lifecycle", "done", json!({}));
|
||||||
self.delivery_receipt_notify.notify_one();
|
self.delivery_receipt_notify.notify_one();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async fn received_artifacts_survive_history_deletion() {
|
|||||||
async fn persists_transfers_and_events_across_reopen() {
|
async fn persists_transfers_and_events_across_reopen() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
let repository = Repository::open(temp.path()).await.unwrap();
|
let repository = Repository::open(temp.path()).await.unwrap();
|
||||||
assert_eq!(repository.schema_version().await.unwrap(), 6);
|
assert_eq!(repository.schema_version().await.unwrap(), 7);
|
||||||
repository
|
repository
|
||||||
.insert_transfer(transfer(
|
.insert_transfer(transfer(
|
||||||
7,
|
7,
|
||||||
@@ -135,6 +135,7 @@ async fn receive_completion_persists_delivery_receipt_until_recorded() {
|
|||||||
request_id: "request-93",
|
request_id: "request-93",
|
||||||
sender_transfer_id: 39,
|
sender_transfer_id: 39,
|
||||||
token: "receipt-token",
|
token: "receipt-token",
|
||||||
|
failure_reason: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -221,6 +222,68 @@ async fn receiver_request_can_only_be_resolved_once() {
|
|||||||
assert!(requests[0].completed_at.is_some());
|
assert!(requests[0].completed_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authenticated_delivery_failure_marks_accepted_receiver_failed() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let repository = Repository::open(temp.path()).await.unwrap();
|
||||||
|
repository
|
||||||
|
.insert_receiver_request(ReceiverRequestInsert {
|
||||||
|
id: "request-failed",
|
||||||
|
transfer_id: 78,
|
||||||
|
remote_endpoint_id: "node-a",
|
||||||
|
transfer_name: "demo",
|
||||||
|
receiver_name: Some("receiver"),
|
||||||
|
receiver_device_name: None,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repository
|
||||||
|
.update_receiver_request_status("request-failed", ReceiverRequestStatus::Accepted, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repository
|
||||||
|
.set_receiver_receipt_token("request-failed", "token-hash")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
repository
|
||||||
|
.fail_receiver_delivery(
|
||||||
|
"request-failed",
|
||||||
|
78,
|
||||||
|
"node-a",
|
||||||
|
"token-hash",
|
||||||
|
"destination_exists",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repository
|
||||||
|
.fail_receiver_delivery(
|
||||||
|
"request-failed",
|
||||||
|
78,
|
||||||
|
"node-a",
|
||||||
|
"token-hash",
|
||||||
|
"destination_exists",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(repository
|
||||||
|
.fail_receiver_delivery(
|
||||||
|
"request-failed",
|
||||||
|
78,
|
||||||
|
"node-b",
|
||||||
|
"token-hash",
|
||||||
|
"destination_exists",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
let requests = repository.list_receiver_requests(78).await.unwrap();
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(requests[0].status, "failed");
|
||||||
|
assert_eq!(requests[0].reason.as_deref(), Some("destination_exists"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn startup_expiration_is_idempotent_for_pending_requests() {
|
async fn startup_expiration_is_idempotent_for_pending_requests() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
@@ -582,7 +645,7 @@ async fn migrates_schema_v2_identity_without_losing_transfer() {
|
|||||||
pool.close().await;
|
pool.close().await;
|
||||||
|
|
||||||
let repository = Repository::open(temp.path()).await.unwrap();
|
let repository = Repository::open(temp.path()).await.unwrap();
|
||||||
assert_eq!(repository.schema_version().await.unwrap(), 6);
|
assert_eq!(repository.schema_version().await.unwrap(), 7);
|
||||||
let stored = repository.list_transfers().await.unwrap().remove(0);
|
let stored = repository.list_transfers().await.unwrap().remove(0);
|
||||||
assert_eq!(stored.transfer_id, 7);
|
assert_eq!(stored.transfer_id, 7);
|
||||||
assert_eq!(stored.local_id, "legacy-7-send");
|
assert_eq!(stored.local_id, "legacy-7-send");
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ fn startup_processes_persisted_delivery_receipts() {
|
|||||||
request_id: "request-94",
|
request_id: "request-94",
|
||||||
sender_transfer_id: 49,
|
sender_transfer_id: 49,
|
||||||
token: "receipt-token",
|
token: "receipt-token",
|
||||||
|
failure_reason: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub(crate) enum ReceiverRequestStatus {
|
|||||||
Refused,
|
Refused,
|
||||||
Expired,
|
Expired,
|
||||||
Completed,
|
Completed,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReceiverRequestStatus {
|
impl ReceiverRequestStatus {
|
||||||
@@ -97,6 +98,7 @@ impl ReceiverRequestStatus {
|
|||||||
Self::Refused => "refused",
|
Self::Refused => "refused",
|
||||||
Self::Expired => "expired",
|
Self::Expired => "expired",
|
||||||
Self::Completed => "completed",
|
Self::Completed => "completed",
|
||||||
|
Self::Failed => "failed",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +113,7 @@ impl TryFrom<&str> for ReceiverRequestStatus {
|
|||||||
"refused" => Ok(Self::Refused),
|
"refused" => Ok(Self::Refused),
|
||||||
"expired" => Ok(Self::Expired),
|
"expired" => Ok(Self::Expired),
|
||||||
"completed" => Ok(Self::Completed),
|
"completed" => Ok(Self::Completed),
|
||||||
|
"failed" => Ok(Self::Failed),
|
||||||
_ => bail!("unknown receiver request status: {value}"),
|
_ => bail!("unknown receiver request status: {value}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,30 @@ fn wait_for_sender_transfer_event(sender: &TestNode, transfer_id: u64, kind: &st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wait_for_receiver_status(
|
||||||
|
sender: &TestNode,
|
||||||
|
transfer_id: u64,
|
||||||
|
status: &str,
|
||||||
|
) -> vnidrop::ReceiverRequest {
|
||||||
|
let started = Instant::now();
|
||||||
|
loop {
|
||||||
|
if let Some(request) = sender
|
||||||
|
.core
|
||||||
|
.list_receiver_requests(transfer_id)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.find(|request| request.status == status)
|
||||||
|
{
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
started.elapsed() < Duration::from_secs(5),
|
||||||
|
"timed out waiting for receiver status {status}"
|
||||||
|
);
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transfers_file_between_two_cores() {
|
fn transfers_file_between_two_cores() {
|
||||||
let source_dir = tempfile::tempdir().unwrap();
|
let source_dir = tempfile::tempdir().unwrap();
|
||||||
@@ -162,4 +186,11 @@ fn receive_refuses_to_overwrite_existing_destination() {
|
|||||||
&& event.kind == "failed"
|
&& event.kind == "failed"
|
||||||
&& event.data_json.contains("\"code\":\"destination_exists\"")
|
&& event.data_json.contains("\"code\":\"destination_exists\"")
|
||||||
}));
|
}));
|
||||||
|
let failed = wait_for_receiver_status(&sender, share.transfer_id, "failed");
|
||||||
|
assert_eq!(failed.reason.as_deref(), Some("destination_exists"));
|
||||||
|
assert!(sender.sink.events().iter().any(|event| {
|
||||||
|
event.transfer_id == Some(share.transfer_id)
|
||||||
|
&& event.phase == "delivery"
|
||||||
|
&& event.kind == "receiver-failed"
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Noch niemand hat diese Übertragung angefragt.</string>
|
<string name="transfer_no_receivers">Noch niemand hat diese Übertragung angefragt.</string>
|
||||||
<string name="transfer_receiver_accepted">Genehmigt – wartet auf Abschluss</string>
|
<string name="transfer_receiver_accepted">Genehmigt – wartet auf Abschluss</string>
|
||||||
<string name="transfer_receiver_completed">Erfolgreich empfangen</string>
|
<string name="transfer_receiver_completed">Erfolgreich empfangen</string>
|
||||||
|
<string name="transfer_receiver_failed">Übertragung fehlgeschlagen</string>
|
||||||
<string name="transfer_receiver_expired">Anfrage abgelaufen</string>
|
<string name="transfer_receiver_expired">Anfrage abgelaufen</string>
|
||||||
<string name="transfer_receiver_refused">Anfrage abgelehnt</string>
|
<string name="transfer_receiver_refused">Anfrage abgelehnt</string>
|
||||||
<string name="transfer_receiver_requested">Wartet auf Ihre Genehmigung</string>
|
<string name="transfer_receiver_requested">Wartet auf Ihre Genehmigung</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Nadie ha solicitado aún esta transferencia.</string>
|
<string name="transfer_no_receivers">Nadie ha solicitado aún esta transferencia.</string>
|
||||||
<string name="transfer_receiver_accepted">Aprobado: esperando a que se complete</string>
|
<string name="transfer_receiver_accepted">Aprobado: esperando a que se complete</string>
|
||||||
<string name="transfer_receiver_completed">Recibido correctamente</string>
|
<string name="transfer_receiver_completed">Recibido correctamente</string>
|
||||||
|
<string name="transfer_receiver_failed">Transferencia fallida</string>
|
||||||
<string name="transfer_receiver_expired">Solicitud caducada</string>
|
<string name="transfer_receiver_expired">Solicitud caducada</string>
|
||||||
<string name="transfer_receiver_refused">Solicitud rechazada</string>
|
<string name="transfer_receiver_refused">Solicitud rechazada</string>
|
||||||
<string name="transfer_receiver_requested">Esperando su aprobación</string>
|
<string name="transfer_receiver_requested">Esperando su aprobación</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Personne n’a encore demandé ce transfert.</string>
|
<string name="transfer_no_receivers">Personne n’a encore demandé ce transfert.</string>
|
||||||
<string name="transfer_receiver_accepted">Approuvé — en attente de la fin</string>
|
<string name="transfer_receiver_accepted">Approuvé — en attente de la fin</string>
|
||||||
<string name="transfer_receiver_completed">Reçu avec succès</string>
|
<string name="transfer_receiver_completed">Reçu avec succès</string>
|
||||||
|
<string name="transfer_receiver_failed">Transfert échoué</string>
|
||||||
<string name="transfer_receiver_expired">Demande expirée</string>
|
<string name="transfer_receiver_expired">Demande expirée</string>
|
||||||
<string name="transfer_receiver_refused">Demande refusée</string>
|
<string name="transfer_receiver_refused">Demande refusée</string>
|
||||||
<string name="transfer_receiver_requested">En attente de votre approbation</string>
|
<string name="transfer_receiver_requested">En attente de votre approbation</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Nessuno ha ancora richiesto questo trasferimento.</string>
|
<string name="transfer_no_receivers">Nessuno ha ancora richiesto questo trasferimento.</string>
|
||||||
<string name="transfer_receiver_accepted">Approvato: in attesa del completamento</string>
|
<string name="transfer_receiver_accepted">Approvato: in attesa del completamento</string>
|
||||||
<string name="transfer_receiver_completed">Ricevuto correttamente</string>
|
<string name="transfer_receiver_completed">Ricevuto correttamente</string>
|
||||||
|
<string name="transfer_receiver_failed">Trasferimento non riuscito</string>
|
||||||
<string name="transfer_receiver_expired">Richiesta scaduta</string>
|
<string name="transfer_receiver_expired">Richiesta scaduta</string>
|
||||||
<string name="transfer_receiver_refused">Richiesta rifiutata</string>
|
<string name="transfer_receiver_refused">Richiesta rifiutata</string>
|
||||||
<string name="transfer_receiver_requested">In attesa della sua approvazione</string>
|
<string name="transfer_receiver_requested">In attesa della sua approvazione</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Nog niemand heeft deze overdracht aangevraagd.</string>
|
<string name="transfer_no_receivers">Nog niemand heeft deze overdracht aangevraagd.</string>
|
||||||
<string name="transfer_receiver_accepted">Goedgekeurd — wachten op voltooiing</string>
|
<string name="transfer_receiver_accepted">Goedgekeurd — wachten op voltooiing</string>
|
||||||
<string name="transfer_receiver_completed">Succesvol ontvangen</string>
|
<string name="transfer_receiver_completed">Succesvol ontvangen</string>
|
||||||
|
<string name="transfer_receiver_failed">Overdracht mislukt</string>
|
||||||
<string name="transfer_receiver_expired">Verzoek verlopen</string>
|
<string name="transfer_receiver_expired">Verzoek verlopen</string>
|
||||||
<string name="transfer_receiver_refused">Verzoek geweigerd</string>
|
<string name="transfer_receiver_refused">Verzoek geweigerd</string>
|
||||||
<string name="transfer_receiver_requested">Wachten op uw goedkeuring</string>
|
<string name="transfer_receiver_requested">Wachten op uw goedkeuring</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Nikt jeszcze nie poprosił o ten transfer.</string>
|
<string name="transfer_no_receivers">Nikt jeszcze nie poprosił o ten transfer.</string>
|
||||||
<string name="transfer_receiver_accepted">Zatwierdzono — oczekiwanie na ukończenie</string>
|
<string name="transfer_receiver_accepted">Zatwierdzono — oczekiwanie na ukończenie</string>
|
||||||
<string name="transfer_receiver_completed">Odebrano pomyślnie</string>
|
<string name="transfer_receiver_completed">Odebrano pomyślnie</string>
|
||||||
|
<string name="transfer_receiver_failed">Przesyłanie nie powiodło się</string>
|
||||||
<string name="transfer_receiver_expired">Prośba wygasła</string>
|
<string name="transfer_receiver_expired">Prośba wygasła</string>
|
||||||
<string name="transfer_receiver_refused">Prośba odrzucona</string>
|
<string name="transfer_receiver_refused">Prośba odrzucona</string>
|
||||||
<string name="transfer_receiver_requested">Oczekiwanie na Twoje zatwierdzenie</string>
|
<string name="transfer_receiver_requested">Oczekiwanie na Twoje zatwierdzenie</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Ainda ninguém pediu esta transferência.</string>
|
<string name="transfer_no_receivers">Ainda ninguém pediu esta transferência.</string>
|
||||||
<string name="transfer_receiver_accepted">Aprovado — a aguardar conclusão</string>
|
<string name="transfer_receiver_accepted">Aprovado — a aguardar conclusão</string>
|
||||||
<string name="transfer_receiver_completed">Recebido com sucesso</string>
|
<string name="transfer_receiver_completed">Recebido com sucesso</string>
|
||||||
|
<string name="transfer_receiver_failed">A transferência falhou</string>
|
||||||
<string name="transfer_receiver_expired">Pedido expirado</string>
|
<string name="transfer_receiver_expired">Pedido expirado</string>
|
||||||
<string name="transfer_receiver_refused">Pedido recusado</string>
|
<string name="transfer_receiver_refused">Pedido recusado</string>
|
||||||
<string name="transfer_receiver_requested">A aguardar a sua aprovação</string>
|
<string name="transfer_receiver_requested">A aguardar a sua aprovação</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Никто ещё не запросил эту передачу.</string>
|
<string name="transfer_no_receivers">Никто ещё не запросил эту передачу.</string>
|
||||||
<string name="transfer_receiver_accepted">Одобрено — ожидание завершения</string>
|
<string name="transfer_receiver_accepted">Одобрено — ожидание завершения</string>
|
||||||
<string name="transfer_receiver_completed">Успешно получено</string>
|
<string name="transfer_receiver_completed">Успешно получено</string>
|
||||||
|
<string name="transfer_receiver_failed">Передача не удалась</string>
|
||||||
<string name="transfer_receiver_expired">Запрос истёк</string>
|
<string name="transfer_receiver_expired">Запрос истёк</string>
|
||||||
<string name="transfer_receiver_refused">Запрос отклонён</string>
|
<string name="transfer_receiver_refused">Запрос отклонён</string>
|
||||||
<string name="transfer_receiver_requested">Ожидание вашего одобрения</string>
|
<string name="transfer_receiver_requested">Ожидание вашего одобрения</string>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<string name="transfer_no_receivers">Nobody has requested this transfer yet.</string>
|
<string name="transfer_no_receivers">Nobody has requested this transfer yet.</string>
|
||||||
<string name="transfer_receiver_accepted">Approved — waiting for completion</string>
|
<string name="transfer_receiver_accepted">Approved — waiting for completion</string>
|
||||||
<string name="transfer_receiver_completed">Received successfully</string>
|
<string name="transfer_receiver_completed">Received successfully</string>
|
||||||
|
<string name="transfer_receiver_failed">Transfer failed</string>
|
||||||
<string name="transfer_receiver_expired">Request expired</string>
|
<string name="transfer_receiver_expired">Request expired</string>
|
||||||
<string name="transfer_receiver_refused">Request refused</string>
|
<string name="transfer_receiver_refused">Request refused</string>
|
||||||
<string name="transfer_receiver_requested">Waiting for your approval</string>
|
<string name="transfer_receiver_requested">Waiting for your approval</string>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ enum class ReceiverDeliveryStatus {
|
|||||||
Refused,
|
Refused,
|
||||||
Expired,
|
Expired,
|
||||||
Completed,
|
Completed,
|
||||||
|
Failed,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -433,6 +433,7 @@ private fun ReceiverRequest.toModel(): ReceiverRequestModel = ReceiverRequestMod
|
|||||||
"refused" -> ReceiverDeliveryStatus.Refused
|
"refused" -> ReceiverDeliveryStatus.Refused
|
||||||
"expired" -> ReceiverDeliveryStatus.Expired
|
"expired" -> ReceiverDeliveryStatus.Expired
|
||||||
"completed" -> ReceiverDeliveryStatus.Completed
|
"completed" -> ReceiverDeliveryStatus.Completed
|
||||||
|
"failed" -> ReceiverDeliveryStatus.Failed
|
||||||
else -> ReceiverDeliveryStatus.Unknown
|
else -> ReceiverDeliveryStatus.Unknown
|
||||||
},
|
},
|
||||||
reason = reason,
|
reason = reason,
|
||||||
|
|||||||
@@ -409,13 +409,15 @@ private fun receiverStatusText(status: ReceiverDeliveryStatus) = stringResource(
|
|||||||
ReceiverDeliveryStatus.Refused -> Res.string.transfer_receiver_refused
|
ReceiverDeliveryStatus.Refused -> Res.string.transfer_receiver_refused
|
||||||
ReceiverDeliveryStatus.Expired -> Res.string.transfer_receiver_expired
|
ReceiverDeliveryStatus.Expired -> Res.string.transfer_receiver_expired
|
||||||
ReceiverDeliveryStatus.Completed -> Res.string.transfer_receiver_completed
|
ReceiverDeliveryStatus.Completed -> Res.string.transfer_receiver_completed
|
||||||
|
ReceiverDeliveryStatus.Failed -> Res.string.transfer_receiver_failed
|
||||||
ReceiverDeliveryStatus.Unknown -> Res.string.transfer_receiver_unknown
|
ReceiverDeliveryStatus.Unknown -> Res.string.transfer_receiver_unknown
|
||||||
})
|
})
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun receiverStatusColor(status: ReceiverDeliveryStatus) = when (status) {
|
private fun receiverStatusColor(status: ReceiverDeliveryStatus) = when (status) {
|
||||||
ReceiverDeliveryStatus.Completed -> LocalVniDropColors.current.brandDefault
|
ReceiverDeliveryStatus.Completed -> LocalVniDropColors.current.brandDefault
|
||||||
ReceiverDeliveryStatus.Refused, ReceiverDeliveryStatus.Expired -> LocalVniDropColors.current.destructiveDefault
|
ReceiverDeliveryStatus.Refused, ReceiverDeliveryStatus.Expired, ReceiverDeliveryStatus.Failed ->
|
||||||
|
LocalVniDropColors.current.destructiveDefault
|
||||||
else -> LocalVniDropColors.current.foregroundLighter
|
else -> LocalVniDropColors.current.foregroundLighter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user