diff --git a/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt b/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt index ba24291..2fb80b1 100644 --- a/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt +++ b/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt @@ -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 @@ -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) + } companion object { const val NAME = "BoltGooglePay" @@ -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) } @@ -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 ) @@ -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 diff --git a/src/__tests__/CreditCard.test.tsx b/src/__tests__/CreditCard.test.tsx index fef39d7..caad11c 100644 --- a/src/__tests__/CreditCard.test.tsx +++ b/src/__tests__/CreditCard.test.tsx @@ -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', () => { diff --git a/src/client/Bolt.ts b/src/client/Bolt.ts index e998ea2..414417e 100644 --- a/src/client/Bolt.ts +++ b/src/client/Bolt.ts @@ -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 { + return { + 'X-Publishable-Key': this.publishableKey, + }; + } } diff --git a/src/payments/GoogleWallet.tsx b/src/payments/GoogleWallet.tsx index bc6ed1b..c03f825 100644 --- a/src/payments/GoogleWallet.tsx +++ b/src/payments/GoogleWallet.tsx @@ -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 ): Promise => { const response = await fetch(`${apiUrl}/v1/apm_config/googlepay`, { method: 'GET', - headers: { - merchant_token: publishableKey, - }, + headers, }); if (!response.ok) { @@ -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?.( @@ -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(() => {