Skip to content

fix(android): fix Google Pay crash — Activity context and ActivityEventListener#49

Open
RhysAtBolt wants to merge 4 commits intomainfrom
fix/google-pay-activity-context
Open

fix(android): fix Google Pay crash — Activity context and ActivityEventListener#49
RhysAtBolt wants to merge 4 commits intomainfrom
fix/google-pay-activity-context

Conversation

@RhysAtBolt
Copy link
Copy Markdown
Contributor

@RhysAtBolt RhysAtBolt commented Apr 2, 2026

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. PaymentsClient created with wrong context (GooglePayModule.kt)
Wallet.getPaymentsClient() was called with reactApplicationContext (the Application context). The Google Pay SDK needs an Activity context to attach the payment sheet to a window — hence Tried to show an alert while not attached to an Activity.

2. onActivityResult never received (GooglePayModule.kt)
GooglePayModule never registered as an ActivityEventListener, so the result from the Google Pay sheet was silently swallowed and the pending Promise would hang forever. The module now implements ActivityEventListener and registers itself in init{} — no changes to MainActivity required.

3. Wrong header when fetching APM config (GoogleWallet.tsx)
The fetchGooglePayAPMConfig request used merchant_token as the header name instead of the correct x-publishable-key, causing the config fetch to fail before the payment sheet could even be shown.

Testing

  • Tap Google Pay button on Android — payment sheet opens without crash
  • Complete a Google Pay payment — onComplete callback fires with token + billing address
  • Cancel/dismiss the Google Pay sheet — onError callback fires with CANCELLED
  • Cold-start the app and tap Google Pay immediately — no NO_ACTIVITY rejection

Security Review

Important

A security review is required for every PR in this repository to comply with PCI requirements.

  • I have considered and reviewed security implications of this PR and included the summary below.

Security Impact Summary

No new data flows or secrets introduced. The x-publishable-key header 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.

…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>
@RhysAtBolt RhysAtBolt requested review from a team as code owners April 2, 2026 12:51
@snyk-io
Copy link
Copy Markdown

snyk-io bot commented Apr 2, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

RhysAtBolt and others added 2 commits April 2, 2026 13:54
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>
Copy link
Copy Markdown

@SanthoshCharanBolt SanthoshCharanBolt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Added a comment

method: 'GET',
headers: {
merchant_token: publishableKey,
'x-publishable-key': publishableKey,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'x-publishable-key': publishableKey,
'X-Publishable-Key': publishableKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And see if we can generalise this as this is needed for most of the api calls

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: PaymentsClient is created with an Activity context and the module is registered as an ActivityEventListener to receive onActivityResult.
  • Android: payment-result handling is routed through the module’s onActivityResult and internalized (private).
  • JS: APM config fetch now sends x-publishable-key instead of merchant_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.

Comment on lines 51 to 58
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!!
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +37
init {
reactContext.addActivityEventListener(this)
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 94
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
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 48
const response = await fetch(`${apiUrl}/v1/apm_config/googlepay`, {
method: 'GET',
headers: {
merchant_token: publishableKey,
'x-publishable-key': publishableKey,
},
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants