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)');