fix(android): reject the promise on SDK errors instead of crashing the app - #5
fix(android): reject the promise on SDK errors instead of crashing the app#5tolypash wants to merge 1 commit into
Conversation
…e app `pairTerminal`, `charge` and `refund` each caught a launch failure, rejected the promise and then rethrew the same exception. The rethrow escapes the module's coroutine — on `refund` it escapes on the main queue (`.runOnQueue(Queues.MAIN)`) — so an error the JS side is fully equipped to handle takes the process down instead. Seen in production: on a device where `YocoSDK.initialise()` had failed, every subsequent call threw, and pressing Void in the POS killed the app rather than surfacing "could not start refund". A zero-amount charge did the same via `IllegalArgumentException: Amount provided needs to be a positive number`. Also null the stored promise on the failure path. It is only cleared in the `OnActivityResult` handlers, so a rejected-but-retained promise could later be resolved by an unrelated activity result — settling it twice. `configure` keeps its rethrow: it is a synchronous `Function`, where throwing is how the error reaches JS. Second, unrelated one-liner: `ReceiptInfo.transactionTime` was missing `@Field`, so it never serialised into the JS payment result on Android (iOS exposes it). Consumers reading `receiptInfo.transactionTime` silently got undefined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
📝 WalkthroughWalkthroughThe Android module now resets stored promises when ChangesPayment handling
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
android/src/main/java/expo/modules/yoco/ReactNativeYocoModule.kt (1)
75-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd regression tests for the new failure lifecycle.
Cover each operation with a mocked synchronous SDK exception, asserting that the JS promise rejects, the exception does not escape, and a later activity result cannot settle the cleared promise.
Also applies to: 109-110, 191-192
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/expo/modules/yoco/ReactNativeYocoModule.kt` around lines 75 - 76, Add regression coverage for the failure paths in the affected Yoco module operations, including the handlers around pairTerminalPromise and the corresponding lines noted in the comment. Mock synchronous SDK exceptions and assert each operation rejects its JS promise without propagating the exception, clears the pending promise, and ignores any later activity result rather than settling it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@android/src/main/java/expo/modules/yoco/ReactNativeYocoModule.kt`:
- Around line 75-76: Add regression coverage for the failure paths in the
affected Yoco module operations, including the handlers around
pairTerminalPromise and the corresponding lines noted in the comment. Mock
synchronous SDK exceptions and assert each operation rejects its JS promise
without propagating the exception, clears the pending promise, and ignores any
later activity result rather than settling it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 339f3ddf-0af8-4e65-bd50-c4b9f5c59444
📒 Files selected for processing (2)
android/src/main/java/expo/modules/yoco/ReactNativeYocoModule.ktandroid/src/main/java/expo/modules/yoco/data/result/ReactNativeYocoPaymentResult.kt
The bug
pairTerminal,chargeandrefundall do this:Rejecting is correct. The rethrow then sends the same exception out of the module's coroutine — and for
refund, which is.runOnQueue(Queues.MAIN), out on the main queue. So an error the JS side is fully equipped to handle instead takes the process down.Seen in production (Sally POS, Yoco terminals):
YocoSDK.initialise()had failed (its Koin graph couldn't buildBaseURLsConfigProvider), every later SDK call threw — pressing Void killed the app instead of showing "could not start refund";amountInCents: 0hitIllegalArgumentException: Amount provided needs to be a positive numberand did the same.Both are ordinary, recoverable errors on the JS side. Neither should be fatal.
The change
throw ein the threeAsyncFunctioncatch blocks — the promise rejection is the error channel;chargePromise/refundPromise/pairTerminalPromiseare otherwise only cleared in theOnActivityResulthandlers, so a rejected-but-retained promise could later be resolved by an unrelated activity result, settling it twice (a crash of its own under the New Architecture);configurekeeps its rethrow — it's a synchronousFunction, where throwing is how the error reaches JS. That path is what surfaces the init failure to the app, and it should stay.Second, unrelated one-liner
ReceiptInfo.transactionTimeis missing@Field, so it never serialises into the JS payment result on Android — anything readingreceiptInfo.transactionTimesilently getsundefined. iOS exposes it. Added the annotation.Verified against the SDK
Decompiled
com.yoco:payment-ui:2.6.9from the Gradle cache to confirm the surrounding assumptions (Yoco's docs site currently 404s on everydeveloper.yoco.com/in-person/android/*page through its redirect toyoco.docs.buildwithfern.com):public void refund(Context, String transactionId, RefundParameters, PrintParameters, ActivityResultLauncher)— the parameter is namedtransactionId, and the method throwsIllegalArgumentException("An empty transaction cannot be refunded")on a blank id. It is not theclientTransactionId;PaymentResultcarries both separately, and this module already surfaces both.Testing
Not covered by automated tests (the module has none, and this is native error-path behaviour). Reviewed by inspection; the intended result is that a failing
charge/refund/pairTerminalarrives in JS as a rejected promise carrying the SDK's message, with the app still running.🤖 Generated with Claude Code
Summary by CodeRabbit