Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.boltreactnativesdk

import android.app.Activity
import android.content.Intent
import com.facebook.react.bridge.ActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
Expand All @@ -27,7 +29,12 @@ import java.net.URL
* and passed down in the config JSON.
*/
class GooglePayModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
ReactContextBaseJavaModule(reactContext),
ActivityEventListener {

init {
reactContext.addActivityEventListener(this)
}
Comment on lines +35 to +37
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.

companion object {
const val NAME = "BoltGooglePay"
Expand All @@ -36,26 +43,32 @@ class GooglePayModule(reactContext: ReactApplicationContext) :

override fun getName(): String = NAME

private var paymentsClient: PaymentsClient? = null
private var pendingPromise: Promise? = null
private var pendingPublishableKey: String = ""
private var pendingBaseUrl: String = ""

private fun getPaymentsClient(): PaymentsClient {
if (paymentsClient == null) {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_TEST)
.build()
paymentsClient = Wallet.getPaymentsClient(reactApplicationContext, walletOptions)
}
return paymentsClient!!
private fun getPaymentsClient(activity: Activity): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_TEST)
.build()
return Wallet.getPaymentsClient(activity, walletOptions)
}

override fun invalidate() {
reactApplicationContext.removeActivityEventListener(this)
super.invalidate()
}

@ReactMethod
fun isReadyToPay(configJson: String, promise: Promise) {
val activity = reactApplicationContext.currentActivity
if (activity == null) {
promise.resolve(false)
return
}
try {
val isReadyToPayRequest = IsReadyToPayRequest.fromJson(buildIsReadyToPayRequest().toString())
getPaymentsClient().isReadyToPay(isReadyToPayRequest)
getPaymentsClient(activity).isReadyToPay(isReadyToPayRequest)
.addOnCompleteListener { task ->
promise.resolve(task.isSuccessful && task.result == true)
}
Expand All @@ -77,12 +90,15 @@ class GooglePayModule(reactContext: ReactApplicationContext) :

val activity = reactApplicationContext.currentActivity
if (activity == null) {
pendingPromise = null
pendingPublishableKey = ""
pendingBaseUrl = ""
promise.reject("NO_ACTIVITY", "No current activity")
return
}

AutoResolveHelper.resolveTask(
getPaymentsClient().loadPaymentData(request),
getPaymentsClient(activity).loadPaymentData(request),
activity,
LOAD_PAYMENT_DATA_REQUEST_CODE
)
Expand All @@ -91,11 +107,20 @@ class GooglePayModule(reactContext: ReactApplicationContext) :
}
}

// ActivityEventListener — receives onActivityResult forwarded by React Native
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == LOAD_PAYMENT_DATA_REQUEST_CODE) {
val paymentData = data?.let { PaymentData.getFromIntent(it) }
handlePaymentResult(resultCode, paymentData)
}
}

override fun onNewIntent(intent: Intent) {}

/**
* Called from the Activity's onActivityResult.
* Processes the Google Pay payment data and tokenizes via Bolt.
*/
fun handlePaymentResult(resultCode: Int, paymentData: PaymentData?) {
private fun handlePaymentResult(resultCode: Int, paymentData: PaymentData?) {
val promise = pendingPromise ?: return
pendingPromise = null

Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/CreditCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ describe('Bolt', () => {
bolt.configureOnPageStyles(newStyles);
expect(bolt.getOnPageStyles()).toEqual(newStyles);
});

it('should return X-Publishable-Key header from apiHeaders()', () => {
const bolt = new Bolt({ publishableKey: 'pk_test_abc' });
expect(bolt.apiHeaders()).toEqual({ 'X-Publishable-Key': 'pk_test_abc' });
});
});

describe('CreditCard', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/client/Bolt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ export class Bolt {
getOnPageStyles(): Styles | undefined {
return this.onPageStyles;
}

/**
* Returns the standard HTTP headers required for Bolt REST API calls.
* Centralised here so every API caller uses a consistent header name.
*/
apiHeaders(): Record<string, string> {
return {
'X-Publishable-Key': this.publishableKey,
};
}
}
12 changes: 5 additions & 7 deletions src/payments/GoogleWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,13 @@ export interface GoogleWalletProps {
* The config includes tokenization spec, merchant ID, and merchant name
* so the developer doesn't need to provide them.
*/
const fetchGooglePayAPMConfig = async (
export const fetchGooglePayAPMConfig = async (
apiUrl: string,
publishableKey: string
headers: Record<string, string>
): Promise<GooglePayAPMConfigResponse> => {
const response = await fetch(`${apiUrl}/v1/apm_config/googlepay`, {
method: 'GET',
headers: {
merchant_token: publishableKey,
},
headers,
});

if (!response.ok) {
Expand Down Expand Up @@ -82,7 +80,7 @@ export const GoogleWallet = ({
useEffect(() => {
if (Platform.OS !== 'android') return;

fetchGooglePayAPMConfig(bolt.apiUrl, bolt.publishableKey)
fetchGooglePayAPMConfig(bolt.apiUrl, bolt.apiHeaders())
.then(setApmConfigResponse)
.catch((err) => {
onError?.(
Expand All @@ -91,7 +89,7 @@ export const GoogleWallet = ({
: new Error('Failed to fetch Google Pay config')
);
});
}, [bolt.apiUrl, bolt.publishableKey, onError]);
}, [bolt, onError]);

// Check Google Pay readiness once we have the APM config
useEffect(() => {
Expand Down
Loading