@@ -32,10 +32,8 @@ export interface CoinbaseQuoteInput {
3232 sandbox ?: boolean ;
3333}
3434
35- /** Slow fallback poll interval while popup is open (5 seconds) */
36- const FALLBACK_POLL_INTERVAL_MS = 5_000 ;
37- /** Fast polling interval after popup reports success (1 second) */
38- const CONFIRMATION_POLL_INTERVAL_MS = 1_000 ;
35+ /** Polling interval for order status (1 second) */
36+ const POLL_INTERVAL_MS = 1_000 ;
3937/** Timeout for the fast confirmation poll after popup reports success (15 seconds) */
4038const CONFIRMATION_TIMEOUT_MS = 15_000 ;
4139
@@ -147,13 +145,12 @@ export function useCoinbase({
147145 const popupRef = useRef < Window | null > ( null ) ;
148146 const channelRef = useRef < BroadcastChannel | null > ( null ) ;
149147 const popupCheckRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
150- const fallbackPollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
151- const confirmPollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
148+ const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
152149 const confirmTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
153150 /** Whether a terminal status has been reached (prevents popup-closed from overriding) */
154151 const terminalReachedRef = useRef ( false ) ;
155152
156- /** Clean up BroadcastChannel, popup watcher, and all polls */
153+ /** Clean up BroadcastChannel, popup watcher, and poll */
157154 const cleanup = useCallback ( ( ) => {
158155 if ( channelRef . current ) {
159156 channelRef . current . close ( ) ;
@@ -163,13 +160,9 @@ export function useCoinbase({
163160 clearInterval ( popupCheckRef . current ) ;
164161 popupCheckRef . current = null ;
165162 }
166- if ( fallbackPollRef . current ) {
167- clearInterval ( fallbackPollRef . current ) ;
168- fallbackPollRef . current = null ;
169- }
170- if ( confirmPollRef . current ) {
171- clearInterval ( confirmPollRef . current ) ;
172- confirmPollRef . current = null ;
163+ if ( pollRef . current ) {
164+ clearInterval ( pollRef . current ) ;
165+ pollRef . current = null ;
173166 }
174167 if ( confirmTimeoutRef . current ) {
175168 clearTimeout ( confirmTimeoutRef . current ) ;
@@ -180,23 +173,19 @@ export function useCoinbase({
180173 // Clean up on unmount
181174 useEffect ( ( ) => cleanup , [ cleanup ] ) ;
182175
183- /** Stop all polling (fallback + confirmation) */
184- const stopAllPolls = useCallback ( ( ) => {
185- if ( fallbackPollRef . current ) {
186- clearInterval ( fallbackPollRef . current ) ;
187- fallbackPollRef . current = null ;
188- }
189- if ( confirmPollRef . current ) {
190- clearInterval ( confirmPollRef . current ) ;
191- confirmPollRef . current = null ;
176+ /** Stop polling and clear timeout */
177+ const stopPoll = useCallback ( ( ) => {
178+ if ( pollRef . current ) {
179+ clearInterval ( pollRef . current ) ;
180+ pollRef . current = null ;
192181 }
193182 if ( confirmTimeoutRef . current ) {
194183 clearTimeout ( confirmTimeoutRef . current ) ;
195184 confirmTimeoutRef . current = null ;
196185 }
197186 } , [ ] ) ;
198187
199- /** Shared poll function — queries backend and handles terminal statuses */
188+ /** Poll backend once — handles terminal statuses */
200189 const pollOnce = useCallback (
201190 async ( targetOrderId : string ) => {
202191 try {
@@ -209,80 +198,54 @@ export function useCoinbase({
209198 if ( result . status === CoinbaseOnrampStatus . Completed ) {
210199 setOrderStatus ( CoinbaseOnrampStatus . Completed ) ;
211200 terminalReachedRef . current = true ;
212- stopAllPolls ( ) ;
201+ stopPoll ( ) ;
213202 } else if ( result . status === CoinbaseOnrampStatus . Failed ) {
214203 setOrderStatus ( CoinbaseOnrampStatus . Failed ) ;
215204 terminalReachedRef . current = true ;
216- stopAllPolls ( ) ;
205+ stopPoll ( ) ;
217206 onError ?.( new Error ( "Coinbase order failed." ) ) ;
218207 }
219208 } catch ( err ) {
220209 console . error ( "Failed to poll Coinbase order status:" , err ) ;
221210 // Don't stop on transient errors
222211 }
223212 } ,
224- [ onError , stopAllPolls ] ,
213+ [ onError , stopPoll ] ,
225214 ) ;
226215
227- /**
228- * Slow fallback poll (5s) — starts as soon as the popup opens.
229- * Catches completions even if the BroadcastChannel signal is lost.
230- */
231- const startFallbackPoll = useCallback (
216+ /** Start 1s polling for order status */
217+ const startPoll = useCallback (
232218 ( targetOrderId : string ) => {
233- if ( fallbackPollRef . current ) return ;
219+ if ( pollRef . current ) return ;
234220
235- fallbackPollRef . current = setInterval (
221+ pollRef . current = setInterval (
236222 ( ) => pollOnce ( targetOrderId ) ,
237- FALLBACK_POLL_INTERVAL_MS ,
223+ POLL_INTERVAL_MS ,
238224 ) ;
239225 } ,
240226 [ pollOnce ] ,
241227 ) ;
242228
243229 /**
244- * Fast confirmation poll (1s) — starts when popup reports polling_success.
245- * Replaces the fallback poll. Times out after 15s .
230+ * Start a 15s confirmation timeout. If the backend doesn't confirm
231+ * Completed within this window, treat it as fatal .
246232 */
247- const startConfirmationPoll = useCallback (
248- ( targetOrderId : string ) => {
249- // Stop the slow fallback poll — we're upgrading to fast
250- if ( fallbackPollRef . current ) {
251- clearInterval ( fallbackPollRef . current ) ;
252- fallbackPollRef . current = null ;
233+ const startConfirmationTimeout = useCallback ( ( ) => {
234+ if ( confirmTimeoutRef . current ) return ;
235+
236+ confirmTimeoutRef . current = setTimeout ( ( ) => {
237+ if ( ! terminalReachedRef . current ) {
238+ stopPoll ( ) ;
239+ setOrderStatus ( CoinbaseOnrampStatus . Failed ) ;
240+ terminalReachedRef . current = true ;
241+ onError ?.(
242+ new Error (
243+ "Payment confirmation timed out. Your card may have been charged — please contact support." ,
244+ ) ,
245+ ) ;
253246 }
254-
255- // Avoid duplicate confirmation polls
256- if ( confirmPollRef . current ) return ;
257-
258- // Immediate first poll
259- pollOnce ( targetOrderId ) ;
260-
261- // Subsequent fast polls
262- confirmPollRef . current = setInterval (
263- ( ) => pollOnce ( targetOrderId ) ,
264- CONFIRMATION_POLL_INTERVAL_MS ,
265- ) ;
266-
267- // Timeout: if status never reaches Completed, treat as fatal
268- confirmTimeoutRef . current = setTimeout ( ( ) => {
269- if ( confirmPollRef . current ) {
270- clearInterval ( confirmPollRef . current ) ;
271- confirmPollRef . current = null ;
272- }
273- if ( ! terminalReachedRef . current ) {
274- setOrderStatus ( CoinbaseOnrampStatus . Failed ) ;
275- terminalReachedRef . current = true ;
276- onError ?.(
277- new Error (
278- "Payment confirmation timed out. Your card may have been charged — please contact support." ,
279- ) ,
280- ) ;
281- }
282- } , CONFIRMATION_TIMEOUT_MS ) ;
283- } ,
284- [ onError , pollOnce ] ,
285- ) ;
247+ } , CONFIRMATION_TIMEOUT_MS ) ;
248+ } , [ onError , stopPoll ] ) ;
286249
287250 /** Open the payment link in a popup and listen via BroadcastChannel */
288251 const openPaymentPopup = useCallback (
@@ -319,8 +282,8 @@ export function useCoinbase({
319282 ) ;
320283 popupRef . current = popup ;
321284
322- // Start slow fallback poll immediately (catches completions if signal is lost)
323- startFallbackPoll ( targetOrderId ) ;
285+ // Start 1s poll immediately (catches completions even if BroadcastChannel signal is lost)
286+ startPoll ( targetOrderId ) ;
324287
325288 // Listen for events from the popup via BroadcastChannel
326289 const channel = new BroadcastChannel ( `coinbase-payment-${ targetOrderId } ` ) ;
@@ -338,12 +301,11 @@ export function useCoinbase({
338301 case "onramp_api.polling_success" :
339302 // Mark terminal immediately so popup-close watcher doesn't interfere
340303 terminalReachedRef . current = true ;
341- // Popup reports success — start tight backend poll for txHash
342- startConfirmationPoll ( targetOrderId ) ;
304+ // Add a 15s timeout — poll is already running at 1s
305+ startConfirmationTimeout ( ) ;
343306 break ;
344307
345308 case "onramp_api.polling_error" :
346- case "onramp_api.commit_error" :
347309 case "onramp_api.load_error" :
348310 setOrderStatus ( CoinbaseOnrampStatus . Failed ) ;
349311 terminalReachedRef . current = true ;
@@ -352,6 +314,15 @@ export function useCoinbase({
352314 ) ;
353315 break ;
354316
317+ case "onramp_api.commit_error" :
318+ // Don't treat as terminal — Coinbase falls back to QR code
319+ // when Apple Pay is unsupported or fails.
320+ console . warn (
321+ "[coinbase-hook] commit_error (non-terminal):" ,
322+ data ?. errorMessage ,
323+ ) ;
324+ break ;
325+
355326 case "onramp_api.cancel" :
356327 setOrderStatus ( CoinbaseOnrampStatus . Failed ) ;
357328 terminalReachedRef . current = true ;
@@ -378,8 +349,8 @@ export function useCoinbase({
378349 paymentLink ,
379350 orderId ,
380351 cleanup ,
381- startFallbackPoll ,
382- startConfirmationPoll ,
352+ startPoll ,
353+ startConfirmationTimeout ,
383354 onError ,
384355 ] ,
385356 ) ;
0 commit comments