Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4b79002
feat(send): bind quote freshness and currency checks into confirmation
woahwhattheheck Sep 24, 2026
126479b
fix: validate quote values independently of payload reconciliation
woahwhattheheck Sep 26, 2026
63d50c6
fix: reconcile reviewed quote against serialized transfer payload
woahwhattheheck Sep 26, 2026
2b51ff3
fix(send-money): normalize localized amounts before quote revalidation
woahwhattheheck Oct 3, 2026
fa87f39
fix(send-money): preserve localized amounts when the input blurs
woahwhattheheck Oct 3, 2026
7eb4bcd
fix(send-money): compare localized inputs with canonical balances
woahwhattheheck Oct 3, 2026
e6002ea
style(send-money): apply repository quotes to balance regressions
woahwhattheheck Oct 3, 2026
14c8de4
fix(send-money): validate wallet connection before transfer creation
woahwhattheheck Oct 3, 2026
6fe01b6
fix(send-money): retire invalidated confirmations during wallet conne…
woahwhattheheck Oct 4, 2026
84510d0
fix(send-money): explain missing quotes and restore precision flow ch…
woahwhattheheck Oct 4, 2026
eb89ddb
test(send-money): traverse confirmation in offline recovery checks
woahwhattheheck Oct 4, 2026
9f4f56f
test(send-money): target the pending confirmation button
woahwhattheheck Oct 4, 2026
4f74e1f
test(quote): align strict amount assertions with canonical contract
woahwhattheheck Oct 4, 2026
17dd96c
style: satisfy the existing Prettier CI gate
woahwhattheheck Oct 4, 2026
91347ff
docs(quote): publish passing existing gates and raw evidence [skip ci]
woahwhattheheck Oct 4, 2026
5b36c88
Remove internal bylines from public validation reports [skip ci]
woahwhattheheck Oct 4, 2026
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
Binary file added docs/review/quote-balance-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/quote-balance-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/wallet-connection-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/review/wallet-connection-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions src/components/QuoteCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,56 @@
height: 2px;
}
}

.quote-meta {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin: 0 0 0.85rem;
padding: 0.65rem 0.75rem;
border-radius: 8px;
background: rgba(15, 23, 42, 0.45);
font-size: 0.78rem;
color: var(--color-muted);
}

.quote-meta-line {
display: flex;
justify-content: space-between;
gap: 0.75rem;
}

.quote-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.02em;
}

.quote-card--expired {
border-color: #b45309;
}

.quote-expired-banner {
margin: 0 0 0.85rem;
padding: 0.55rem 0.7rem;
border-radius: 8px;
background: rgba(180, 83, 9, 0.18);
color: #fbbf24;
font-size: 0.85rem;
}

.quote-refresh {
appearance: none;
border: none;
background: transparent;
color: #fde68a;
font: inherit;
font-weight: 600;
text-decoration: underline;
cursor: pointer;
padding: 0;
}

.quote-refresh:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
113 changes: 105 additions & 8 deletions src/components/QuoteCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,107 @@ import { formatRate, formatPercent } from '../utils/format.js';
import { formatMoney } from '../utils/money.js';
import { FEE_PERCENT } from '../constants/fees.js';
import { DEFAULT_LOCALE } from '../constants/locales.js';
import { getCurrency } from '../constants/currencies.js';
import { quoteSecondsRemaining } from '../utils/quoteBinding.js';
import { isQuoteExpired } from '../services/contracts/quote.js';
import './QuoteCard.css';

/**
* Displays the breakdown of an FX quote: rate, fee and amount received.
* Displays the breakdown of an FX quote: rate, fee and amount received,
* plus freshness metadata (source, timestamps, currency precision).
* @param {object} props
* @param {object} props.quote - quote object from buildQuote()
* @param {string} [props.locale] - locale used for currency formatting
* @param {number|Date} [props.now] - injectable clock for expiry display
* @param {Function} [props.onRefresh] - optional refresh handler when expired
*/
export default function QuoteCard({ quote, locale = DEFAULT_LOCALE }) {
export default function QuoteCard({
quote,
locale = DEFAULT_LOCALE,
now = Date.now(),
onRefresh,
}) {
if (!quote) return null;

const { from, to, rate, sendAmount, fee, receiveAmount } = quote;
const { from, to, rate, sendAmount, fee, receiveAmount, source, id } = quote;
const fromMeta = getCurrency(from);
const toMeta = getCurrency(to);
const expired = isQuoteExpired(quote, now);
const secondsLeft = quoteSecondsRemaining(quote, now);
const createdLabel = formatTimestamp(quote.createdAt, locale);
const expiresLabel = formatTimestamp(quote.expiresAt, locale);

return (
<div className="quote-card">
<div
className={`quote-card${expired ? ' quote-card--expired' : ''}`}
data-quote-id={id || undefined}
data-quote-expired={expired ? 'true' : 'false'}
>
<h3 className="quote-title">Transfer summary</h3>

<div className="quote-meta" aria-label="Quote details">
<div className="quote-meta-line">
<span>Source</span>
<span>{source || 'unknown'}</span>
</div>
{id && (
<div className="quote-meta-line">
<span>Quote id</span>
<span className="quote-mono">{shortId(id)}</span>
</div>
)}
<div className="quote-meta-line">
<span>Priced at</span>
<span>{createdLabel}</span>
</div>
<div className="quote-meta-line">
<span>Expires</span>
<span>
{expiresLabel}
{!expired && secondsLeft > 0 ? ` · ${secondsLeft}s left` : ''}
</span>
</div>
<div className="quote-meta-line">
<span>Currencies</span>
<span>
{fromMeta
? `${fromMeta.flag} ${from} (${fromMeta.minorUnits}dp)`
: from}{' '}
→ {toMeta ? `${toMeta.flag} ${to} (${toMeta.minorUnits}dp)` : to}
</span>
</div>
</div>

{expired && (
<div className="quote-expired-banner" role="status">
This quote has expired.
{onRefresh ? (
<>
{' '}
<button
type="button"
className="quote-refresh"
onClick={onRefresh}
>
Refresh quote
</button>
</>
) : (
' Refresh to continue.'
)}
</div>
)}

<div className="quote-line">
<span>You send</span>
<span>{formatMoney(sendAmount, from, locale)}</span>
<span data-testid="quote-send-amount">
{formatMoney(sendAmount, from, locale)}
</span>
</div>

<div className="quote-line quote-muted">
<span>RemitFlow fee ({formatPercent(FEE_PERCENT, 1)} + flat)</span>
<span>- {formatMoney(fee, from, locale)}</span>
<span data-testid="quote-fee">- {formatMoney(fee, from, locale)}</span>
</div>

<div className="quote-line quote-muted">
Expand All @@ -38,13 +114,34 @@ export default function QuoteCard({ quote, locale = DEFAULT_LOCALE }) {

<div className="quote-line quote-total">
<span>Recipient gets</span>
<span>{formatMoney(receiveAmount, to, locale)}</span>
<span data-testid="quote-receive-amount">
{formatMoney(receiveAmount, to, locale)}
</span>
</div>

<p className="quote-note">
Fees cover the RemitFlow service and Stellar network cost. Rates are
indicative and update at confirmation.
indicative; confirmation uses the bound quote id until it expires.
</p>
</div>
);
}

function formatTimestamp(iso, locale) {
if (!iso) return '—';
const ms = Date.parse(iso);
if (Number.isNaN(ms)) return '—';
try {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'medium',
}).format(new Date(ms));
} catch {
return new Date(ms).toISOString();
}
}

function shortId(id) {
if (!id || id.length <= 16) return id;
return `${id.slice(0, 10)}…${id.slice(-4)}`;
}
1 change: 0 additions & 1 deletion src/context/AppContext.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { createContext, useContext, useEffect, useState } from 'react';
import {
connectWallet,
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useTransfers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { useCallback, useEffect, useState } from 'react';
import { listTransfers, createTransfer } from '../services/api.js';
import { ContractViolationError } from '../services/contracts/schema.js';
Expand Down
6 changes: 6 additions & 0 deletions src/pages/SendMoney.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,9 @@
color: #86efac;
font-size: 0.875rem;
}

.send-quote-id {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.85em;
word-break: break-all;
}
Loading