Skip to content
Merged
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
25 changes: 25 additions & 0 deletions src/__tests__/BuyPolicyModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ describe('BuyPolicyModal', () => {
expect(html).toContain('Max 32 chars');
});

// #543: the oracle key is derived during render (useMemo) rather than synced
// into state from a useEffect, so typing in a builder field updates it in
// the same render instead of triggering a second setOracleKey render.
it('updates the crop oracle key as latitude is typed', () => {
render(<BuyPolicyModal product={makeProduct({ category: 'crop' })} onClose={vi.fn()} />);
const lat = screen.getByPlaceholderText('e.g. -0.0917') as HTMLInputElement;
fireEvent.change(lat, { target: { value: '1.5' } });
expect(screen.getByText(/^rainfall:1\.5/)).toBeInTheDocument();
});

it('updates the flight oracle key as the flight number is typed', () => {
render(<BuyPolicyModal product={makeProduct({ category: 'flight', name: 'Flight Delay' })} onClose={vi.fn()} />);
const input = screen.getByPlaceholderText(/KQ/i) as HTMLInputElement;
fireEvent.change(input, { target: { value: 'KQ100' } });
expect(screen.getByText(/^flight:KQ100:/)).toBeInTheDocument();
});

it('keeps manual oracle key entry working for categories without a builder', () => {
render(<BuyPolicyModal product={makeProduct({ category: 'health', name: 'Health' })} onClose={vi.fn()} />);
const input = screen.getByPlaceholderText('e.g. rainfall:1.5,36.8:2026-06') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'health:ke:2026' } });
expect(input.value).toBe('health:ke:2026');
expect(screen.getByText('health:ke:2026', { selector: 'span' })).toBeInTheDocument();
});

it('renders the configure step with coverage and duration inputs', () => {
const html = renderToStaticMarkup(<BuyPolicyModal product={makeProduct()} onClose={vi.fn()} />);
expect(html).toContain('Coverage Amount (USDC)');
Expand Down
25 changes: 14 additions & 11 deletions src/components/BuyPolicyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import type { Product } from '@/types';
import { useWallet } from '@/hooks/useWallet';
import { displayToStroops, stroopsToDisplay, estimatePremium, basisPointsToPercent } from '@/lib/format';
Expand Down Expand Up @@ -37,7 +37,8 @@ export function BuyPolicyModal({ product, onClose }: Props) {

const [coverage, setCoverage] = useState('');
const [duration, setDuration] = useState(String(Math.min(30, product.maxDuration)));
const [oracleKey, setOracleKey] = useState('');
// Only used for categories without a key builder (disaster, health, ...).
const [manualOracleKey, setManualOracleKey] = useState('');
const [step, setStep] = useState(0);

// Reset to step 0 when the product changes to avoid showing stale data
Expand Down Expand Up @@ -71,18 +72,20 @@ export function BuyPolicyModal({ product, onClose }: Props) {
const coverageNum = parseFloat(coverage) || 0;
const estimatedPrem = safeEstimatePremium(coverage, product.premiumRate);

// Automatically build oracle key based on inputs
useEffect(() => {
// Derive the oracle key during render (#543). It used to be synced into
// state from a useEffect, so every keystroke in lat/lng/flight fields
// rendered twice (input change, then setOracleKey) and the first render
// showed a stale key.
const oracleKey = useMemo(() => {
if (product.category === 'crop') {
const latNum = parseFloat(lat) || 0;
const lngNum = parseFloat(lng) || 0;
setOracleKey(buildRainfallKey(latNum, lngNum, year, month));
} else if (product.category === 'flight') {
setOracleKey(buildFlightKey(flightNumber.trim(), flightDate));
} else if (product.category === 'defi') {
setOracleKey('defi');
return buildRainfallKey(latNum, lngNum, year, month);
}
}, [product.category, lat, lng, year, month, flightNumber, flightDate]);
if (product.category === 'flight') return buildFlightKey(flightNumber.trim(), flightDate);
if (product.category === 'defi') return 'defi';
return manualOracleKey;
}, [product.category, lat, lng, year, month, flightNumber, flightDate, manualOracleKey]);

function validateFields(): boolean {
let isValid = true;
Expand Down Expand Up @@ -383,7 +386,7 @@ export function BuyPolicyModal({ product, onClose }: Props) {
<input
type="text"
value={oracleKey}
onChange={(e) => { setOracleKey(e.target.value); setOracleKeyError(''); }}
onChange={(e) => { setManualOracleKey(e.target.value); setOracleKeyError(''); }}
placeholder='e.g. rainfall:1.5,36.8:2026-06'
maxLength={32}
className={`${INPUT_CLASS} ${oracleKeyError ? 'border-red-500' : ''}`}
Expand Down
Loading