From af21f497e3c8f85a0bf89784ae2ffb39882f3f91 Mon Sep 17 00:00:00 2001 From: presidojay1 <88345996+presidojay1@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:23:22 +0100 Subject: [PATCH 1/2] perf(buy-modal): derive oracle key with useMemo instead of effect+state BuyPolicyModal synced the oracle key into state from a useEffect, so every keystroke in the lat/lng/year/month/flight fields rendered twice (input change, then setOracleKey) and the first render showed a stale key. Compute the key during render with useMemo from the builder inputs. Only categories without a key builder (disaster, health, ...) keep state, now named manualOracleKey, for the free-text input. Closes #543 --- src/components/BuyPolicyModal.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/BuyPolicyModal.tsx b/src/components/BuyPolicyModal.tsx index 0afea67..b6a146d 100644 --- a/src/components/BuyPolicyModal.tsx +++ b/src/components/BuyPolicyModal.tsx @@ -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'; @@ -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 @@ -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; @@ -383,7 +386,7 @@ export function BuyPolicyModal({ product, onClose }: Props) { { 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' : ''}`} From 0cebccecfb81392bed9f554716d0e7f5088c43cb Mon Sep 17 00:00:00 2001 From: presidojay1 <88345996+presidojay1@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:23:23 +0100 Subject: [PATCH 2/2] test(buy-modal): cover derived and manual oracle keys Assert the computed key updates as crop latitude and flight number are typed, and that manual oracle-key entry still works for categories without a builder. Refs #543 --- src/__tests__/BuyPolicyModal.test.tsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/__tests__/BuyPolicyModal.test.tsx b/src/__tests__/BuyPolicyModal.test.tsx index 33cbdd9..a1f3da4 100644 --- a/src/__tests__/BuyPolicyModal.test.tsx +++ b/src/__tests__/BuyPolicyModal.test.tsx @@ -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(); + 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(); + 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(); + 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(); expect(html).toContain('Coverage Amount (USDC)');