Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/api/aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ export async function updateTransactionDetails({
status,
txHash,
timeSpent,
refundReason,
accessToken,
walletAddress,
}: UpdateTransactionDetailsPayload): Promise<SaveTransactionResponse> {
Expand All @@ -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}`,
Expand Down
20 changes: 12 additions & 8 deletions app/api/v1/transactions/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand All @@ -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<string, unknown> = {
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()
Expand Down
1 change: 1 addition & 0 deletions app/api/v1/transactions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
12 changes: 12 additions & 0 deletions app/components/transaction/TransactionDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,18 @@ export function TransactionDetails({ transaction }: TransactionDetailsProps) {
</span>
}
/>
{transaction.status === "refunded" &&
transaction.refund_reason &&
transaction.refund_reason.trim() !== "" && (
<DetailRow
label="Refund reason"
value={
<span className="text-text-accent-gray dark:text-white/80">
{transaction.refund_reason}
</span>
}
/>
)}
<DetailRow
label="Transaction status"
value={
Expand Down
39 changes: 24 additions & 15 deletions app/components/transaction/TransactionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,31 @@ export const TransactionListItem = ({
{transaction.from_currency}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-text-disabled dark:text-white/30">
{new Date(transaction.created_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="size-1 bg-icon-outline-disabled dark:bg-white/5"></span>
<span
className={classNames(
STATUS_COLOR_MAP[transaction.status] ||
"text-text-secondary dark:text-white/50",
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
<span className="text-text-disabled dark:text-white/30">
{new Date(transaction.created_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="size-1 bg-icon-outline-disabled dark:bg-white/5"></span>
<span
className={classNames(
STATUS_COLOR_MAP[transaction.status] ||
"text-text-secondary dark:text-white/50",
)}
>
{transaction.status}
</span>
</div>
{transaction.status === "refunded" &&
transaction.refund_reason &&
transaction.refund_reason.trim() !== "" && (
<span className="text-xs text-text-secondary dark:text-white/50 line-clamp-1">
{transaction.refund_reason}
</span>
)}
>
{transaction.status}
</span>
</div>
</div>
</div>
Expand Down
16 changes: 13 additions & 3 deletions app/context/TransactionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
});
Expand All @@ -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
Expand Down
26 changes: 25 additions & 1 deletion app/pages/TransactionStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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{" "}
Expand All @@ -650,6 +666,14 @@ export function TransactionStatus({
{formatCurrency(fiat ?? 0, currency, `en-${currency.slice(0, 2)}`)})
</span>{" "}
to {formattedRecipientName} was unsuccessful.
{refundReason && (
<>
<br />
<span className="text-text-secondary dark:text-white/70">
{refundReason}
</span>
</>
)}
<br />
<br />
The stablecoin has been refunded to your account.
Expand Down
5 changes: 5 additions & 0 deletions app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export type OrderDetailsData = {
settlements: Settlement[];
txReceipts: TxReceipt[];
updatedAt: string;
cancellationReasons?: string[];
};

type Settlement = {
Expand Down Expand Up @@ -321,6 +322,7 @@ export interface TransactionHistory {
created_at: string;
updated_at: string;
order_id?: string;
refund_reason?: string | null;
}

export interface TransactionCreateInput {
Expand All @@ -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";
Expand Down Expand Up @@ -381,6 +385,7 @@ export interface UpdateTransactionDetailsPayload
extends UpdateTransactionStatusPayload {
txHash?: string;
timeSpent?: string;
refundReason?: string | null;
}

export type Currency = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)';