diff --git a/app/api/aggregator.ts b/app/api/aggregator.ts index 1c1b949b..8fea7853 100644 --- a/app/api/aggregator.ts +++ b/app/api/aggregator.ts @@ -461,6 +461,7 @@ export async function updateTransactionDetails({ status, txHash, timeSpent, + refundReason, accessToken, walletAddress, }: UpdateTransactionDetailsPayload): Promise { @@ -476,6 +477,9 @@ export async function updateTransactionDetails({ if (timeSpent !== undefined && timeSpent !== null && timeSpent !== "") { data.timeSpent = timeSpent; } + if (refundReason !== undefined && refundReason !== null && refundReason !== "") { + data.refundReason = refundReason; + } const response = await axios.put( `/api/v1/transactions/${transactionId}`, diff --git a/app/api/v1/transactions/[id]/route.ts b/app/api/v1/transactions/[id]/route.ts index 2bced3f5..b1d9c8e8 100644 --- a/app/api/v1/transactions/[id]/route.ts +++ b/app/api/v1/transactions/[id]/route.ts @@ -39,7 +39,7 @@ export const PUT = withRateLimit( new_status: body.status, }); - const { txHash, timeSpent, status } = body; + const { txHash, timeSpent, status, refundReason } = body; // First verify that the transaction belongs to the wallet const { data: existingTransaction, error: fetchError } = @@ -64,15 +64,19 @@ export const PUT = withRateLimit( ); } - // Update transaction + // Update transaction (only include refund_reason when provided, e.g. for refunded status) + const updatePayload: Record = { + tx_hash: txHash, + time_spent: timeSpent, + status: status, + updated_at: new Date().toISOString(), + }; + if (refundReason !== undefined) { + updatePayload.refund_reason = refundReason; + } const { data, error } = await supabaseAdmin .from("transactions") - .update({ - tx_hash: txHash, - time_spent: timeSpent, - status: status, - updated_at: new Date().toISOString(), - }) + .update(updatePayload) .eq("id", id) .eq("wallet_address", walletAddress) .select() diff --git a/app/api/v1/transactions/route.ts b/app/api/v1/transactions/route.ts index d7f28be6..3774375d 100644 --- a/app/api/v1/transactions/route.ts +++ b/app/api/v1/transactions/route.ts @@ -157,6 +157,7 @@ export const POST = withRateLimit(async (request: NextRequest) => { time_spent: body.time_spent, tx_hash: body.txHash, order_id: body.orderId, + refund_reason: body.refundReason ?? null, }) .select() .single(); diff --git a/app/components/transaction/TransactionDetails.tsx b/app/components/transaction/TransactionDetails.tsx index e9d53568..9907218a 100644 --- a/app/components/transaction/TransactionDetails.tsx +++ b/app/components/transaction/TransactionDetails.tsx @@ -373,6 +373,18 @@ export function TransactionDetails({ transaction }: TransactionDetailsProps) { } /> + {transaction.status === "refunded" && + transaction.refund_reason && + transaction.refund_reason.trim() !== "" && ( + + {transaction.refund_reason} + + } + /> + )} -
- - {new Date(transaction.created_at).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} - - - +
+ + {new Date(transaction.created_at).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + + {transaction.status} + +
+ {transaction.status === "refunded" && + transaction.refund_reason && + transaction.refund_reason.trim() !== "" && ( + + {transaction.refund_reason} + )} - > - {transaction.status} -
diff --git a/app/context/TransactionsContext.tsx b/app/context/TransactionsContext.tsx index 4d40a798..1b005825 100644 --- a/app/context/TransactionsContext.tsx +++ b/app/context/TransactionsContext.tsx @@ -88,11 +88,17 @@ export function TransactionsProvider({ tx.order_id!, ); - // Determine new txHash + // Determine new txHash and refund reason let newTxHash: string | undefined; + let refundReason: string | undefined; const orderData = res.data; if (orderData.status === "refunded") { newTxHash = orderData.txHash; + refundReason = + orderData.cancellationReasons?.length && + orderData.cancellationReasons[0] + ? orderData.cancellationReasons.join(", ") + : undefined; } else if (Array.isArray(orderData.txReceipts)) { // Prefer validated, settled, then pending const relevantReceipt = orderData.txReceipts.find( @@ -130,15 +136,18 @@ export function TransactionsProvider({ } } - // update transaction status or txHash if changed + // update transaction status or txHash or refund_reason if changed const statusChanged = orderData.status !== tx.status; const hashChanged = newTxHash && newTxHash !== tx.tx_hash; - if (statusChanged || hashChanged) { + const refundReasonChanged = + refundReason !== undefined && refundReason !== tx.refund_reason; + if (statusChanged || hashChanged || refundReasonChanged) { // Update backend await updateTransactionDetails({ transactionId: tx.id, status: orderData.status, txHash: newTxHash, + refundReason: refundReason ?? undefined, accessToken, walletAddress, }); @@ -149,6 +158,7 @@ export function TransactionsProvider({ ? "completed" : (orderData.status as TransactionStatus), tx_hash: newTxHash ?? tx.tx_hash, + refund_reason: refundReason ?? tx.refund_reason, }; // Update local state diff --git a/app/pages/TransactionStatus.tsx b/app/pages/TransactionStatus.tsx index a76c1729..6719d60a 100644 --- a/app/pages/TransactionStatus.tsx +++ b/app/pages/TransactionStatus.tsx @@ -222,12 +222,18 @@ export function TransactionStatus({ return; } + const refundReason = + transactionStatus === "refunded" && orderDetails?.cancellationReasons?.length + ? orderDetails.cancellationReasons.join(", ") + : undefined; + const response = await updateTransactionDetails({ transactionId, status: transactionStatus, txHash: transactionStatus !== "refunded" ? createdHash : orderDetails?.txHash, timeSpent, + refundReason, accessToken, walletAddress: embeddedWallet.address, }); @@ -363,9 +369,14 @@ export function TransactionStatus({ trackEvent("Swap completed", eventData); setIsTracked(true); } else if (transactionStatus === "refunded") { + const reason = + orderDetails?.cancellationReasons?.length && + orderDetails.cancellationReasons[0] + ? orderDetails.cancellationReasons[0] + : "Transaction failed and refunded"; trackEvent("Swap failed", { ...eventData, - "Reason for failure": "Transaction failed and refunded", + "Reason for failure": reason, }); setIsTracked(true); } @@ -642,6 +653,11 @@ export function TransactionStatus({ : ""; if (transactionStatus === "refunded") { + const refundReason = + orderDetails?.cancellationReasons?.length && + orderDetails.cancellationReasons[0] + ? orderDetails.cancellationReasons[0] + : null; return ( <> Your transfer of{" "} @@ -650,6 +666,14 @@ export function TransactionStatus({ {formatCurrency(fiat ?? 0, currency, `en-${currency.slice(0, 2)}`)}) {" "} to {formattedRecipientName} was unsuccessful. + {refundReason && ( + <> +
+ + {refundReason} + + + )}

The stablecoin has been refunded to your account. diff --git a/app/types.ts b/app/types.ts index 78f59334..239bae5e 100644 --- a/app/types.ts +++ b/app/types.ts @@ -150,6 +150,7 @@ export type OrderDetailsData = { settlements: Settlement[]; txReceipts: TxReceipt[]; updatedAt: string; + cancellationReasons?: string[]; }; type Settlement = { @@ -321,6 +322,7 @@ export interface TransactionHistory { created_at: string; updated_at: string; order_id?: string; + refund_reason?: string | null; } export interface TransactionCreateInput { @@ -337,12 +339,14 @@ export interface TransactionCreateInput { txHash?: string; timeSpent?: string; orderId?: string; + refundReason?: string | null; } export interface TransactionUpdateInput { status: TransactionStatus; timeSpent?: string; txHash?: string; + refundReason?: string | null; } export type JWTProvider = "privy" | "thirdweb"; @@ -381,6 +385,7 @@ export interface UpdateTransactionDetailsPayload extends UpdateTransactionStatusPayload { txHash?: string; timeSpent?: string; + refundReason?: string | null; } export type Currency = { diff --git a/supabase/migrations/20250204000000_add_refund_reason_to_transactions.sql b/supabase/migrations/20250204000000_add_refund_reason_to_transactions.sql new file mode 100644 index 00000000..f013e6c8 --- /dev/null +++ b/supabase/migrations/20250204000000_add_refund_reason_to_transactions.sql @@ -0,0 +1,5 @@ +-- Add refund_reason column to store cancellation/refund reason from aggregator when status is refunded +ALTER TABLE transactions +ADD COLUMN IF NOT EXISTS refund_reason TEXT NULL; + +COMMENT ON COLUMN transactions.refund_reason IS 'Reason for refund when status is refunded (from aggregator cancellationReasons)';