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
3 changes: 1 addition & 2 deletions app/features/gift-cards/gift-card-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ export default function GiftCardDetails({ cardId }: GiftCardDetailsProps) {

<div className="mt-6 grid w-72 grid-cols-2 gap-10">
<LinkWithViewTransition
to={buildLinkWithSearchParams('/receive', {
accountId: card.id,
to={buildLinkWithSearchParams(`/gift-cards/${card.id}/transfer`, {
redirectTo: `/gift-cards/${card.id}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude this redirectTo should not have been removed. We still want to reidrect back when the transfer is completed

Copy link

@claude claude bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @gudnuf's task in 54s —— View job


Restored redirectTo on gift card transfer link

  • Read the current file and diff to understand the removed code
  • Restore the redirectTo parameter
  • Push the fix

The "Add" button's link was missing the redirectTo search param. Restored it using buildLinkWithSearchParams (same pattern as the "Send" button), so navigating back after transfer completion works correctly.

Commit: 46c27c4

})}
transition="slideUp"
Expand Down
134 changes: 134 additions & 0 deletions app/features/transfer/transfer-confirmation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { MoneyDisplay } from '~/components/money-display';
import {
PageBackButton,
PageContent,
PageFooter,
PageHeader,
PageHeaderTitle,
} from '~/components/page';
import { Button } from '~/components/ui/button';
import { Card, CardContent } from '~/components/ui/card';
import { MoneyWithConvertedAmount } from '~/features/shared/money-with-converted-amount';
import { useToast } from '~/hooks/use-toast';
import { useNavigateWithViewTransition } from '~/lib/transitions';
import { useAccount } from '../accounts/account-hooks';
import { getDefaultUnit } from '../shared/currencies';
import { DomainError } from '../shared/error';
import { useInitiateTransfer } from './transfer-hooks';
import { useTransferStore } from './transfer-provider';
import type { TransferQuote } from './transfer-service';

const ConfirmationRow = ({
label,
value,
}: { label: string; value: React.ReactNode }) => {
return (
<div className="flex items-center justify-between">
<p className="text-muted-foreground">{label}</p>
<div>{value}</div>
</div>
);
};

type TransferConfirmationProps = {
transferQuote: TransferQuote;
};

export default function TransferConfirmation({
transferQuote,
}: TransferConfirmationProps) {
const { toast } = useToast();
const navigate = useNavigateWithViewTransition();

const destinationAccountId = useTransferStore((s) => s.destinationAccountId);
const sourceAccountId = useTransferStore((s) => s.sourceAccountId);

const destinationAccount = useAccount(destinationAccountId);
const sourceAccount = useAccount(sourceAccountId);

const { mutate: initiateTransfer, status: transferStatus } =
useInitiateTransfer({
onSuccess: ({ receiveTransactionId }) => {
const params = new URLSearchParams({
showOkButton: 'true',
redirectTo: `/gift-cards/${destinationAccount.id}`,
});
navigate(
{
pathname: `/transactions/${receiveTransactionId}`,
search: params.toString(),
},
{
transition: 'slideLeft',
applyTo: 'newView',
},
);
},
onError: (error) => {
if (error instanceof DomainError) {
toast({ description: error.message });
} else {
console.error('Failed to initiate transfer', { cause: error });
toast({
description: 'Transfer failed. Please try again.',
variant: 'destructive',
});
}
},
});

const handleConfirm = () => {
initiateTransfer({
sourceAccount,
destinationAccount,
transferQuote,
});
};

const isPending = ['pending', 'success'].includes(transferStatus);

return (
<>
<PageHeader className="z-10">
<PageBackButton to=".." transition="slideRight" applyTo="oldView" />
<PageHeaderTitle>Confirm</PageHeaderTitle>
</PageHeader>
<PageContent className="flex flex-col items-center gap-4">
<MoneyWithConvertedAmount money={transferQuote.estimatedTotal} />
<div className="absolute top-0 right-0 bottom-0 left-0 mx-auto flex max-w-sm items-center justify-center">
<Card className="m-4 w-full">
<CardContent className="flex flex-col gap-6 pt-6">
<ConfirmationRow
label="Amount"
value={
<MoneyDisplay
size="sm"
money={transferQuote.amount}
unit={getDefaultUnit(transferQuote.amount.currency)}
/>
}
/>
<ConfirmationRow
label="Estimated fee"
value={
<MoneyDisplay
size="sm"
money={transferQuote.estimatedFee}
unit={getDefaultUnit(transferQuote.estimatedFee.currency)}
/>
}
/>
<ConfirmationRow label="From" value={sourceAccount.name} />
<ConfirmationRow label="To" value={destinationAccount.name} />
</CardContent>
</Card>
</div>
</PageContent>
<PageFooter className="pb-14">
<Button onClick={handleConfirm} loading={isPending}>
Confirm
</Button>
</PageFooter>
</>
);
}
78 changes: 78 additions & 0 deletions app/features/transfer/transfer-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useMutation } from '@tanstack/react-query';
import type { Money } from '~/lib/money';
import type { Account } from '../accounts/account';
import { ConcurrencyError, DomainError } from '../shared/error';
import { useUser } from '../user/user-hooks';
import type { TransferQuote } from './transfer-service';
import { useTransferService } from './transfer-service';

export function useCreateTransferQuote() {
const transferService = useTransferService();

return useMutation({
scope: { id: 'create-transfer-quote' },
mutationFn: ({
sourceAccount,
destinationAccount,
amount,
}: {
sourceAccount: Account;
destinationAccount: Account;
amount: Money;
}) =>
transferService.getTransferQuote({
sourceAccount,
destinationAccount,
amount,
}),
retry: (failureCount, error) => {
if (error instanceof DomainError) return false;
return failureCount < 1;
},
});
}

export function useInitiateTransfer({
onSuccess,
onError,
}: {
onSuccess: (data: {
sendTransactionId: string;
receiveTransactionId: string;
}) => void;
onError: (error: Error) => void;
}) {
const userId = useUser((user) => user.id);
const transferService = useTransferService();

return useMutation({
scope: { id: 'initiate-transfer' },
mutationFn: ({
sourceAccount,
destinationAccount,
transferQuote,
}: {
sourceAccount: Account;
destinationAccount: Account;
transferQuote: TransferQuote;
}) =>
transferService.initiateTransfer({
userId,
sourceAccount,
destinationAccount,
transferQuote,
}),
onSuccess: (data) => {
onSuccess({
sendTransactionId: data.sendTransactionId,
receiveTransactionId: data.receiveTransactionId,
});
},
onError,
retry: (failureCount, error) => {
if (error instanceof ConcurrencyError) return true;
if (error instanceof DomainError) return false;
return failureCount < 1;
},
});
}
Loading