fix(android): fix Google Pay crash — Activity context and ActivityEventListener#49
fix(android): fix Google Pay crash — Activity context and ActivityEventListener#49RhysAtBolt wants to merge 4 commits intomainfrom
Conversation
…ng ActivityEventListener
- Pass Activity (not ApplicationContext) to Wallet.getPaymentsClient() so the
payment sheet has a window to attach to, fixing the "not attached to an
Activity" crash on button tap
- Implement ActivityEventListener and register it in init{} so onActivityResult
is forwarded by the RN bridge — previously the pending Promise would hang
forever as the result was never received
- Make handlePaymentResult private now that it is called internally via the
listener rather than from MainActivity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Use the correct x-publishable-key header instead of merchant_token when fetching the Google Pay APM config from Bolt's API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
src/payments/GoogleWallet.tsx
Outdated
| method: 'GET', | ||
| headers: { | ||
| merchant_token: publishableKey, | ||
| 'x-publishable-key': publishableKey, |
There was a problem hiding this comment.
| 'x-publishable-key': publishableKey, | |
| 'X-Publishable-Key': publishableKey, |
There was a problem hiding this comment.
And see if we can generalise this as this is needed for most of the api calls
There was a problem hiding this comment.
Pull request overview
Fixes Android Google Pay crash/hanging flows by ensuring the Google Pay SDK is invoked with an Activity context and by wiring onActivityResult delivery to the native module; also corrects the HTTP header used to fetch the Google Pay APM config on the JS side.
Changes:
- Android:
PaymentsClientis created with anActivitycontext and the module is registered as anActivityEventListenerto receiveonActivityResult. - Android: payment-result handling is routed through the module’s
onActivityResultand internalized (private). - JS: APM config fetch now sends
x-publishable-keyinstead ofmerchant_token.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/payments/GoogleWallet.tsx | Fixes the request header used to fetch Google Pay APM config. |
| android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt | Uses Activity context for Google Pay and registers for activity results to resolve/reject pending promises. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private fun getPaymentsClient(activity: Activity): PaymentsClient { | ||
| if (paymentsClient == null) { | ||
| val walletOptions = Wallet.WalletOptions.Builder() | ||
| .setEnvironment(WalletConstants.ENVIRONMENT_TEST) | ||
| .build() | ||
| paymentsClient = Wallet.getPaymentsClient(reactApplicationContext, walletOptions) | ||
| paymentsClient = Wallet.getPaymentsClient(activity, walletOptions) | ||
| } | ||
| return paymentsClient!! |
There was a problem hiding this comment.
paymentsClient is cached after being created with an Activity context. If the Activity is recreated (rotation, multi-window, process recreation), the cached client can retain a reference to a destroyed Activity and may leak it or misbehave. Consider either (a) not caching and creating a new PaymentsClient per call, or (b) cache keyed by the current Activity (recreate when the passed Activity differs) and avoid holding a strong reference to the old one.
| init { | ||
| reactContext.addActivityEventListener(this) | ||
| } |
There was a problem hiding this comment.
The module registers itself as an ActivityEventListener in init, but never unregisters. When the React instance is torn down/recreated (e.g., Fast Refresh/dev reload or multiple React instances), this can leave stale listeners and cause duplicate callbacks or leaks. Consider overriding invalidate() (or the appropriate teardown hook for your React Native version) to call reactApplicationContext.removeActivityEventListener(this).
| pendingPromise = promise | ||
| pendingPublishableKey = publishableKey | ||
| pendingBaseUrl = baseUrl | ||
|
|
||
| val paymentDataRequest = buildPaymentDataRequest(config) | ||
| val request = PaymentDataRequest.fromJson(paymentDataRequest.toString()) | ||
|
|
||
| val activity = reactApplicationContext.currentActivity | ||
| if (activity == null) { | ||
| promise.reject("NO_ACTIVITY", "No current activity") | ||
| return | ||
| } |
There was a problem hiding this comment.
pendingPromise / pendingPublishableKey / pendingBaseUrl are set before the NO_ACTIVITY early return. If currentActivity is null, the promise is rejected but the pending state remains set, which can retain references longer than needed and complicate subsequent calls. Clear the pending fields before returning in the NO_ACTIVITY branch.
src/payments/GoogleWallet.tsx
Outdated
| const response = await fetch(`${apiUrl}/v1/apm_config/googlepay`, { | ||
| method: 'GET', | ||
| headers: { | ||
| merchant_token: publishableKey, | ||
| 'x-publishable-key': publishableKey, | ||
| }, |
There was a problem hiding this comment.
This change fixes the request header name used for the APM config fetch, but there’s no unit test asserting the request is sent with the expected header. Since this header is required for the config call to succeed, consider adding a Jest test that mocks fetch and verifies the x-publishable-key header is used.
- Capitalize header to 'X-Publishable-Key' per convention (SanthoshCharanBolt) - Add Bolt.apiHeaders() method to centralise the header for all API calls and use it in fetchGooglePayAPMConfig (SanthoshCharanBolt) - Don't cache PaymentsClient — create per-call to avoid holding a stale Activity reference across rotation/recreation (Copilot) - Override invalidate() to call removeActivityEventListener, preventing stale listeners on React instance teardown (Copilot) - Clear pending fields before NO_ACTIVITY early return to avoid retaining references longer than necessary (Copilot) - Add test for Bolt.apiHeaders() returning X-Publishable-Key (Copilot) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Description
Fix two bugs that caused Google Pay to crash and a third that sent the wrong auth header when fetching the Bolt APM config.
1.
PaymentsClientcreated with wrong context (GooglePayModule.kt)Wallet.getPaymentsClient()was called withreactApplicationContext(the Application context). The Google Pay SDK needs anActivitycontext to attach the payment sheet to a window — henceTried to show an alert while not attached to an Activity.2.
onActivityResultnever received (GooglePayModule.kt)GooglePayModulenever registered as anActivityEventListener, so the result from the Google Pay sheet was silently swallowed and the pendingPromisewould hang forever. The module now implementsActivityEventListenerand registers itself ininit{}— no changes toMainActivityrequired.3. Wrong header when fetching APM config (
GoogleWallet.tsx)The
fetchGooglePayAPMConfigrequest usedmerchant_tokenas the header name instead of the correctx-publishable-key, causing the config fetch to fail before the payment sheet could even be shown.Testing
onCompletecallback fires with token + billing addressonErrorcallback fires withCANCELLEDNO_ACTIVITYrejectionSecurity Review
Important
A security review is required for every PR in this repository to comply with PCI requirements.
Security Impact Summary
No new data flows or secrets introduced. The
x-publishable-keyheader fix corrects which key is sent to the Bolt APM config endpoint — the key itself was already present in the original code, just under the wrong header name. The Activity context change only affects where Google Pay's system UI is anchored; no payment data handling is altered.