Skip to content

Commit b4cd5b8

Browse files
committed
fix: drill down on edge cases including the ability to re-check a token on success & fail - e.g. cancel auction / check / transfer. refactors each tokenId with its own loading state (so a user can potentially pile up transactions without waiting for other to complete)
1 parent f107bd0 commit b4cd5b8

2 files changed

Lines changed: 73 additions & 62 deletions

File tree

src/components/CryptoKitties/CryptoKitties.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ test('handles batch transfer of multiple kitties', async () => {
398398
})
399399

400400
// Error handling
401-
test('shows error alert when ownership check fails for kitty ID', async () => {
401+
test('shows error when ownership check fails for kitty ID', async () => {
402402
const { getByLabelText, getByText } = render(<CryptoKitties walletAddress={ETH_WALLET} dapperWalletAddress={OTHER_DAPPERWALLET} invokeTx={mockInvokeTx} {...contracts} />)
403403

404404
// Wait for total supply to be set
@@ -422,7 +422,7 @@ test('shows error alert when ownership check fails for kitty ID', async () => {
422422
expect(getByText('An error occurred while checking ownership.')).toBeTruthy()
423423
})
424424

425-
test('shows error alert when auction cancellation fails', async () => {
425+
test('shows error when auction cancellation fails', async () => {
426426
const { getByLabelText, getByText } = render(<CryptoKitties walletAddress={ETH_WALLET} dapperWalletAddress={DAPPERWALLET} invokeTx={mockInvokeTx} {...contracts} />)
427427

428428
// Wait for total supply to be set
@@ -448,7 +448,7 @@ test('shows error alert when auction cancellation fails', async () => {
448448
fireEvent.click(getByText(/Cancel Sire Auction/i))
449449
})
450450

451-
expect(window.alert).toHaveBeenCalledWith('Failed to cancel auction. Please try again.')
451+
expect(getByText('Failed to cancel auction. Please try again.')).toBeTruthy()
452452
})
453453

454454
test('shows alert when user enters non-numeric or out-of-range kitty ID', async () => {

src/components/CryptoKitties/CryptoKitties.tsx

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,11 @@ const CryptoKitties: React.FC<{
6161
sire: Contract<AbiFragment[]>,
6262
}> = ({ walletAddress, dapperWalletAddress, invokeTx, core, sale, sire }) => {
6363

64-
const initFormState: FormDetails = {
65-
kittyId: '',
66-
loading: false,
67-
}
68-
6964
// Component state
7065
const [kittyStatuses, setKittyStatuses] = useState<KittyStatus[]>([]) // Status for multiple kitties
7166
const [balance, setBalance] = useState<number>(0) // User's CryptoKitties balance
7267
const [totalSupply, setTotalSupply] = useState<number>(0) // Total number of CryptoKitties
73-
const [formDetails, setFormDetails] = useState<FormDetails>(initFormState) // Form state
68+
const [kittyId, setKittyId] = useState('') // Input value
7469

7570
useEffect(() => {
7671
const init = async () => {
@@ -119,23 +114,22 @@ const CryptoKitties: React.FC<{
119114
* @throws {Error} If auction cancellation fails
120115
*/
121116
const handleCancelAuction = async (isSaleAuction: boolean, tokenId: string) => {
122-
setFormDetails(prevState => ({ ...prevState, loading: true }))
117+
setKittyStatuses(prev => prev.map(s =>
118+
s.id === tokenId ? { ...s, loading: true } : s
119+
));
123120
const contract = isSaleAuction ? sale : sire
124121
const address = isSaleAuction ? Contracts['Sale'].addr : Contracts['Sire'].addr
125122
const methodCall = contract.methods.cancelAuction(tokenId)
126123
try {
127124
await invokeTx(address, methodCall, '0')
128125
setKittyStatuses(prev => prev.map(s =>
129-
s.id === tokenId ? { ...s, auctionCancelled: true } : s
126+
s.id === tokenId ? { ...s, auctionCancelled: true, loading: false } : s
130127
));
131128
} catch (e) {
132129
const errorMessage = 'Failed to cancel auction. Please try again.';
133-
alert(errorMessage);
134130
setKittyStatuses(prev => prev.map(s =>
135-
s.id === tokenId ? { ...s, error: errorMessage } : s
131+
s.id === tokenId ? { ...s, error: errorMessage, loading: false } : s
136132
));
137-
} finally {
138-
setFormDetails(prevState => ({ ...prevState, loading: false }))
139133
}
140134
}
141135

@@ -147,22 +141,21 @@ const CryptoKitties: React.FC<{
147141
* @throws {Error} If transfer fails
148142
*/
149143
const handleTransfer = async (kittyId: string) => {
150-
setFormDetails(prevState => ({ ...prevState, loading: true }))
144+
setKittyStatuses(prev => prev.map(s =>
145+
s.id === kittyId ? { ...s, loading: true } : s
146+
));
151147
const address = Contracts['Core'].addr
152148
const methodCall = core.methods.transfer(walletAddress, kittyId)
153149
try {
154150
await invokeTx(address, methodCall, '0')
155151
setKittyStatuses(prev => prev.map(s =>
156-
s.id === kittyId ? { ...s, transferSuccess: true } : s
152+
s.id === kittyId ? { ...s, transferSuccess: true, loading: false } : s
157153
))
158154
} catch (e) {
159155
const errorMessage = 'Failed to transfer. Please try again.';
160-
alert(errorMessage);
161156
setKittyStatuses(prev => prev.map(s =>
162-
s.id === kittyId ? { ...s, error: errorMessage } : s
157+
s.id === kittyId ? { ...s, error: errorMessage, loading: false } : s
163158
));
164-
} finally {
165-
setFormDetails(prevState => ({ ...prevState, loading: false }))
166159
}
167160
}
168161

@@ -171,30 +164,26 @@ const CryptoKitties: React.FC<{
171164
* @param {React.ChangeEvent<HTMLInputElement>} e - Change event
172165
* @param {keyof FormDetails} changeParam - Form field to update
173166
*/
174-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, changeParam: keyof FormDetails) => {
175-
const { value } = e.target
176-
const newState = { ...formDetails }
177-
if (changeParam === 'kittyId') {
178-
newState.kittyId = value
167+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
168+
const value = e.target.value;
169+
setKittyId(value);
179170

180-
// Clear existing statuses when input changes
181-
setKittyStatuses([])
171+
// Clear existing statuses when input changes
172+
setKittyStatuses([]);
182173

183-
// Show all IDs immediately
184-
const ids = value.split(',')
185-
.map(id => id.trim())
186-
.filter(id => id !== '');
174+
// Show all IDs immediately
175+
const ids = value.split(',')
176+
.map(id => id.trim())
177+
.filter(id => id !== '');
187178

188-
setKittyStatuses(ids.map(id => ({
189-
id,
190-
transferrable: false,
191-
forSale: false,
192-
forSire: false,
193-
loading: false,
194-
error: !isValidKittyId(id) ? 'Invalid kitty ID' : undefined
195-
})));
196-
}
197-
setFormDetails(newState)
179+
setKittyStatuses(ids.map(id => ({
180+
id,
181+
transferrable: false,
182+
forSale: false,
183+
forSire: false,
184+
loading: false,
185+
error: !isValidKittyId(id) ? 'Invalid kitty ID' : undefined
186+
})));
198187
}
199188

200189
/**
@@ -205,46 +194,69 @@ const CryptoKitties: React.FC<{
205194
const checkAllKitties = async () => {
206195
// Get valid IDs
207196
const ids = kittyStatuses
208-
.filter(status => !status.error)
197+
.filter(status => !status.error || (
198+
status.error !== 'Invalid kitty ID'
199+
))
209200
.map(status => status.id);
201+
202+
// Create a copy of current statuses to update
203+
let updatedStatuses = [...kittyStatuses];
204+
210205
for (let i = 0; i < ids.length; i++) {
211206
const kittyId = ids[i];
212-
setKittyStatuses(prev => prev.map(status =>
213-
status.id === kittyId ? { ...status, loading: true, error: undefined } : status
214-
));
207+
208+
// Update loading state for current kitty
209+
updatedStatuses = updatedStatuses.map(status =>
210+
status.id === kittyId ? {
211+
id: status.id,
212+
transferrable: false,
213+
forSale: false,
214+
forSire: false,
215+
loading: true,
216+
error: undefined,
217+
transferSuccess: undefined,
218+
auctionCancelled: undefined
219+
} : status
220+
);
221+
setKittyStatuses(updatedStatuses);
215222

216223
try {
217224
const owner = await core.methods.ownerOf(kittyId).call();
218225
if (owner && owner.toString().toLowerCase() === dapperWalletAddress.toLowerCase()) {
219-
setKittyStatuses(prev => prev.map(status =>
226+
updatedStatuses = updatedStatuses.map(status =>
220227
status.id === kittyId ? { ...status, transferrable: true, loading: false } : status
221-
));
228+
);
229+
setKittyStatuses(updatedStatuses);
222230
continue;
223231
}
224232

225233
const isInSaleAuction = await checkAuction(sale, kittyId);
226234

227235
if (isInSaleAuction) {
228-
setKittyStatuses(prev => prev.map(status =>
236+
updatedStatuses = updatedStatuses.map(status =>
229237
status.id === kittyId ? { ...status, forSale: true, loading: false } : status
230-
));
238+
);
239+
setKittyStatuses(updatedStatuses);
231240
} else {
232241
const isInSireAuction = await checkAuction(sire, kittyId);
233242
if (isInSireAuction) {
234-
setKittyStatuses(prev => prev.map(status =>
243+
updatedStatuses = updatedStatuses.map(status =>
235244
status.id === kittyId ? { ...status, forSire: true, loading: false } : status
236-
));
245+
);
246+
setKittyStatuses(updatedStatuses);
237247
} else {
238-
setKittyStatuses(prev => prev.map(status =>
248+
updatedStatuses = updatedStatuses.map(status =>
239249
status.id === kittyId ? { ...status, loading: false, error: 'Not owned by this Dapper Wallet' } : status
240-
));
250+
);
251+
setKittyStatuses(updatedStatuses);
241252
}
242253
}
243254
} catch (error) {
244255
const errorMessage = 'An error occurred while checking ownership.';
245-
setKittyStatuses(prev => prev.map(status =>
256+
updatedStatuses = updatedStatuses.map(status =>
246257
status.id === kittyId ? { ...status, loading: false, error: errorMessage } : status
247-
));
258+
);
259+
setKittyStatuses(updatedStatuses);
248260
}
249261
}
250262
}
@@ -271,15 +283,14 @@ const CryptoKitties: React.FC<{
271283
<input
272284
id={'tokenIds'}
273285
type={'text'}
274-
value={formDetails.kittyId}
275-
onChange={e => handleChange(e, 'kittyId')}
276-
disabled={formDetails.loading}
286+
value={kittyId}
287+
onChange={handleChange}
277288
placeholder="Example: 123 or 123,456,789"
278289
/>
279290
</label>
280291
<button
281292
onClick={checkAllKitties}
282-
disabled={formDetails.loading}
293+
disabled={kittyStatuses.some(status => status.loading)}
283294
>
284295
{'Check Kitties'}
285296
</button>
@@ -303,15 +314,15 @@ const CryptoKitties: React.FC<{
303314
{status.transferrable && (
304315
<button
305316
onClick={async () => await handleTransfer(status.id)}
306-
disabled={formDetails.loading}
317+
disabled={status.loading}
307318
>
308319
Transfer Kitty
309320
</button>
310321
)}
311322
{(status.forSale || status.forSire) && (
312323
<button
313324
onClick={async () => await handleCancelAuction(status.forSale, status.id)}
314-
disabled={formDetails.loading}
325+
disabled={status.loading}
315326
>
316327
{`Cancel ${status.forSale ? 'Sale' : 'Sire'} Auction`}
317328
</button>

0 commit comments

Comments
 (0)