Skip to content

Commit e9bb3ef

Browse files
committed
fix: refactor Apple/Google Pay component and fix React hooks violations
- Extract createOrRenderPaymentRequest helper to eliminate ~60 lines duplication - Fix conditional hooks errors by moving early return after all hooks - Add stable dependencies to useCallback/useEffect to prevent infinite loops - Memoize buttonManager config and destructure emitResponse for stability - Add eslint-plugin-react-hooks to enforce hooks rules
1 parent 3c29598 commit e9bb3ef

File tree

5 files changed

+144
-133
lines changed

5 files changed

+144
-133
lines changed

.eslintrc.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ module.exports = {
1010
jsx: true,
1111
},
1212
},
13+
plugins: [ 'react-hooks' ],
1314
rules: {
1415
// Critical rules - only ban console.log, allow warn/error
1516
'no-console': [ 'error', { allow: [ 'warn', 'error' ] } ],
17+
18+
// React Hooks rules
19+
'react-hooks/rules-of-hooks': 'error',
20+
'react-hooks/exhaustive-deps': 'warn',
1621
},
1722
};

assets/js/components/monei-apple-google-component.js

Lines changed: 70 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ export const createAppleGoogleLabel = ( moneiData ) => {
4040
* @return {*} JSX Element
4141
*/
4242
export const MoneiAppleGoogleContent = ( props ) => {
43-
const { useEffect, useRef, useState, createPortal } = wp.element;
43+
const { useEffect, useRef, useState, createPortal, useMemo, useCallback } =
44+
wp.element;
4445
const { useSelect } = wp.data;
4546
const { onPaymentSetup, onCheckoutSuccess } = props.eventRegistration;
46-
const { activePaymentMethod } = props;
47+
const { activePaymentMethod, emitResponse } = props;
48+
const { responseTypes, noticeContexts } = emitResponse;
4749
const moneiData =
4850
props.moneiData ||
4951
// eslint-disable-next-line no-undef
@@ -63,94 +65,25 @@ export const MoneiAppleGoogleContent = ( props ) => {
6365
activePaymentMethod ===
6466
( props.paymentMethodId || 'monei_apple_google' );
6567

66-
const buttonManager = useButtonStateManager( {
67-
isActive,
68-
emitResponse: props.emitResponse,
69-
tokenFieldName: 'monei_payment_request_token',
70-
errorMessage: moneiData.tokenErrorString,
71-
} );
72-
73-
/**
74-
* Initialize MONEI Payment Request
75-
*/
76-
const initPaymentRequest = () => {
77-
// eslint-disable-next-line no-undef
78-
if ( typeof monei === 'undefined' || ! monei.PaymentRequest ) {
79-
console.error( 'MONEI SDK is not available' );
80-
return;
81-
}
82-
83-
const currentTotal = cartTotals?.total_price
84-
? parseInt( cartTotals.total_price )
85-
: Math.round( moneiData.total * 100 );
86-
87-
lastAmountRef.current = currentTotal;
88-
89-
// Clean up existing instance
90-
if ( paymentRequestRef.current?.close ) {
91-
try {
92-
paymentRequestRef.current.close();
93-
} catch ( e ) {
94-
// Silent fail
95-
}
96-
}
97-
98-
const container = document.getElementById(
99-
'payment-request-container'
100-
);
101-
if ( ! container ) {
102-
console.error( 'Payment request container not found' );
103-
return;
104-
}
105-
106-
// Clear container
107-
container.innerHTML = '';
108-
109-
// eslint-disable-next-line no-undef
110-
const paymentRequest = monei.PaymentRequest( {
111-
accountId: moneiData.accountId,
112-
sessionId: moneiData.sessionId,
113-
language: moneiData.language,
114-
amount: currentTotal,
115-
currency: moneiData.currency,
116-
style: moneiData.paymentRequestStyle || {},
117-
onSubmit( result ) {
118-
if ( result.token ) {
119-
setError( '' );
120-
buttonManager.enableCheckout( result.token );
121-
}
122-
},
123-
onError( error ) {
124-
const errorMessage =
125-
error.message ||
126-
`${ error.status || 'Error' } ${
127-
error.statusCode ? `(${ error.statusCode })` : ''
128-
}`;
129-
setError( errorMessage );
130-
console.error( 'Payment Request error:', error );
131-
},
132-
} );
68+
// Memoize buttonManager config to ensure stability
69+
const buttonManagerConfig = useMemo(
70+
() => ( {
71+
isActive,
72+
emitResponse,
73+
tokenFieldName: 'monei_payment_request_token',
74+
errorMessage: moneiData.tokenErrorString,
75+
} ),
76+
[ isActive, emitResponse, moneiData.tokenErrorString ]
77+
);
13378

134-
paymentRequest.render( container );
135-
paymentRequestRef.current = paymentRequest;
136-
};
79+
const buttonManager = useButtonStateManager( buttonManagerConfig );
13780

13881
/**
139-
* Update the amount in the existing Payment Request instance
82+
* Create or re-render MONEI Payment Request with specified amount
83+
* @param {number} amount - Payment amount in cents
14084
*/
141-
const updatePaymentRequestAmount = () => {
142-
const currentTotal = cartTotals?.total_price
143-
? parseInt( cartTotals.total_price )
144-
: Math.round( moneiData.total * 100 );
145-
146-
// Only update if amount actually changed
147-
if ( currentTotal === lastAmountRef.current ) {
148-
return;
149-
}
150-
151-
lastAmountRef.current = currentTotal;
152-
153-
if ( paymentRequestRef.current ) {
85+
const createOrRenderPaymentRequest = useCallback(
86+
( amount ) => {
15487
// Clean up existing instance
15588
if ( paymentRequestRef.current?.close ) {
15689
try {
@@ -164,6 +97,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
16497
'payment-request-container'
16598
);
16699
if ( ! container ) {
100+
console.error( 'Payment request container not found' );
167101
return;
168102
}
169103

@@ -175,7 +109,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
175109
accountId: moneiData.accountId,
176110
sessionId: moneiData.sessionId,
177111
language: moneiData.language,
178-
amount: currentTotal,
112+
amount,
179113
currency: moneiData.currency,
180114
style: moneiData.paymentRequestStyle || {},
181115
onSubmit( result ) {
@@ -197,8 +131,46 @@ export const MoneiAppleGoogleContent = ( props ) => {
197131

198132
paymentRequest.render( container );
199133
paymentRequestRef.current = paymentRequest;
134+
},
135+
[ moneiData, setError, buttonManager ]
136+
);
137+
138+
/**
139+
* Initialize MONEI Payment Request
140+
*/
141+
const initPaymentRequest = useCallback( () => {
142+
// eslint-disable-next-line no-undef
143+
if ( typeof monei === 'undefined' || ! monei.PaymentRequest ) {
144+
console.error( 'MONEI SDK is not available' );
145+
return;
146+
}
147+
148+
const currentTotal = cartTotals?.total_price
149+
? parseInt( cartTotals.total_price )
150+
: Math.round( moneiData.total * 100 );
151+
152+
lastAmountRef.current = currentTotal;
153+
154+
createOrRenderPaymentRequest( currentTotal );
155+
}, [ cartTotals, moneiData.total, createOrRenderPaymentRequest ] );
156+
157+
/**
158+
* Update the amount in the existing Payment Request instance
159+
*/
160+
const updatePaymentRequestAmount = useCallback( () => {
161+
const currentTotal = cartTotals?.total_price
162+
? parseInt( cartTotals.total_price )
163+
: Math.round( moneiData.total * 100 );
164+
165+
// Only update if amount actually changed
166+
if ( currentTotal === lastAmountRef.current ) {
167+
return;
200168
}
201-
};
169+
170+
lastAmountRef.current = currentTotal;
171+
172+
createOrRenderPaymentRequest( currentTotal );
173+
}, [ cartTotals, moneiData.total, createOrRenderPaymentRequest ] );
202174

203175
// Initialize on mount
204176
useEffect( () => {
@@ -213,7 +185,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
213185
} else if ( ! monei || ! monei.PaymentRequest ) {
214186
console.error( 'MONEI SDK is not available' );
215187
}
216-
}, [] );
188+
}, [ initPaymentRequest ] );
217189

218190
// Update amount when cart totals change
219191
useEffect( () => {
@@ -224,7 +196,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
224196
) {
225197
updatePaymentRequestAmount();
226198
}
227-
}, [ cartTotals ] );
199+
}, [ cartTotals, updatePaymentRequestAmount ] );
228200

229201
// Cleanup on unmount
230202
useEffect( () => {
@@ -246,7 +218,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
246218
} );
247219

248220
return () => unsubscribe();
249-
}, [ onPaymentSetup ] );
221+
}, [ onPaymentSetup, buttonManager ] );
250222

251223
// Setup checkout success hook
252224
useEffect( () => {
@@ -257,7 +229,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
257229
// If no paymentId, backend handles everything (redirect flow)
258230
if ( ! paymentDetails?.paymentId ) {
259231
return {
260-
type: props.emitResponse.responseTypes.SUCCESS,
232+
type: responseTypes.SUCCESS,
261233
};
262234
}
263235

@@ -275,15 +247,15 @@ export const MoneiAppleGoogleContent = ( props ) => {
275247

276248
if ( result.nextAction && result.nextAction.mustRedirect ) {
277249
return {
278-
type: props.emitResponse.responseTypes.SUCCESS,
250+
type: responseTypes.SUCCESS,
279251
redirectUrl: result.nextAction.redirectUrl,
280252
};
281253
}
282254
if ( result.status === 'FAILED' ) {
283255
const failUrl = new URL( paymentDetails.failUrl );
284256
failUrl.searchParams.set( 'status', 'FAILED' );
285257
return {
286-
type: props.emitResponse.responseTypes.SUCCESS,
258+
type: responseTypes.SUCCESS,
287259
redirectUrl: failUrl.toString(),
288260
};
289261
} else {
@@ -295,7 +267,7 @@ export const MoneiAppleGoogleContent = ( props ) => {
295267
url.searchParams.set( 'status', result.status );
296268

297269
return {
298-
type: props.emitResponse.responseTypes.SUCCESS,
270+
type: responseTypes.SUCCESS,
299271
redirectUrl: url.toString(),
300272
};
301273
}
@@ -306,17 +278,16 @@ export const MoneiAppleGoogleContent = ( props ) => {
306278
);
307279
setIsConfirming( false );
308280
return {
309-
type: props.emitResponse.responseTypes.ERROR,
281+
type: responseTypes.ERROR,
310282
message: error.message || 'Payment confirmation failed',
311-
messageContext:
312-
props.emitResponse.noticeContexts.PAYMENTS,
283+
messageContext: noticeContexts.PAYMENTS,
313284
};
314285
}
315286
}
316287
);
317288

318289
return () => unsubscribe();
319-
}, [ onCheckoutSuccess ] );
290+
}, [ onCheckoutSuccess, responseTypes, noticeContexts ] );
320291

321292
return (
322293
<fieldset className="monei-fieldset monei-payment-request-fieldset">

assets/js/components/monei-cc-component.js

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { useEffect, useState, useRef, useCallback, useMemo, createPortal } =
1313
* @return {React.Element}
1414
*/
1515
export const MoneiCCContent = ( props ) => {
16-
const { responseTypes } = props.emitResponse;
16+
const { responseTypes, noticeContexts } = props.emitResponse;
1717
const { onPaymentSetup, onCheckoutValidation, onCheckoutSuccess } =
1818
props.eventRegistration;
1919

@@ -62,14 +62,6 @@ export const MoneiCCContent = ( props ) => {
6262

6363
// Card input management
6464
const cardInput = useMoneiCardInput( cardInputConfig );
65-
// If hosted workflow, show redirect message
66-
if ( isHostedWorkflow ) {
67-
return (
68-
<div className="monei-redirect-description">
69-
{ moneiData.description }
70-
</div>
71-
);
72-
}
7365

7466
/**
7567
* Create payment token
@@ -94,7 +86,7 @@ export const MoneiCCContent = ( props ) => {
9486
} );
9587

9688
return tokenPromiseRef.current;
97-
}, [ cardInput.createToken ] );
89+
}, [ cardInput ] );
9890

9991
/**
10092
* Validate form
@@ -116,13 +108,7 @@ export const MoneiCCContent = ( props ) => {
116108
}
117109

118110
return isValid;
119-
}, [
120-
cardholderName.validate,
121-
cardInput.isValid,
122-
formErrors.setError,
123-
formErrors.clearError,
124-
moneiData.cardErrorString,
125-
] );
111+
}, [ cardholderName, cardInput, formErrors, moneiData.cardErrorString ] );
126112

127113
// Setup validation hook
128114
useEffect( () => {
@@ -164,11 +150,8 @@ export const MoneiCCContent = ( props ) => {
164150
return unsubscribe;
165151
}, [
166152
onCheckoutValidation,
167-
cardholderName.validate,
168-
cardholderName.error,
169-
cardInput.error,
170-
cardInput.isValid,
171-
cardInput.token,
153+
cardholderName,
154+
cardInput,
172155
createPaymentToken,
173156
moneiData.cardErrorString,
174157
moneiData.tokenErrorString,
@@ -218,8 +201,8 @@ export const MoneiCCContent = ( props ) => {
218201
return unsubscribe;
219202
}, [
220203
onPaymentSetup,
221-
cardholderName.value,
222-
cardInput.token,
204+
cardholderName,
205+
cardInput,
223206
createPaymentToken,
224207
responseTypes,
225208
moneiData.tokenErrorString,
@@ -281,20 +264,23 @@ export const MoneiCCContent = ( props ) => {
281264
return {
282265
type: responseTypes.ERROR,
283266
message: error.message || 'Payment confirmation failed',
284-
messageContext:
285-
props.emitResponse.noticeContexts.PAYMENTS,
267+
messageContext: noticeContexts.PAYMENTS,
286268
};
287269
}
288270
}
289271
);
290272

291273
return unsubscribe;
292-
}, [
293-
onCheckoutSuccess,
294-
cardholderName.value,
295-
responseTypes,
296-
props.emitResponse.noticeContexts,
297-
] );
274+
}, [ onCheckoutSuccess, cardholderName, responseTypes, noticeContexts ] );
275+
276+
// If hosted workflow, show redirect message only
277+
if ( isHostedWorkflow ) {
278+
return (
279+
<div className="monei-redirect-description">
280+
{ moneiData.description }
281+
</div>
282+
);
283+
}
298284

299285
return (
300286
<fieldset className="monei-fieldset monei-card-fieldset wc-block-components-form">

0 commit comments

Comments
 (0)