From f401783d6ce673900ecc67157acf39550ccc974a Mon Sep 17 00:00:00 2001 From: Aryeh Stiefel Date: Tue, 31 Mar 2026 13:59:57 -0400 Subject: [PATCH] feat: fetch Google Pay config from Bolt API, add button theme support --- README.md | 32 +++---- .../boltreactnativesdk/GooglePayButtonView.kt | 13 +++ .../GooglePayButtonViewManager.kt | 5 ++ .../com/boltreactnativesdk/GooglePayModule.kt | 46 ++++++---- example/src/App.tsx | 8 +- src/__tests__/GoogleWallet.test.tsx | 39 ++++++-- src/__tests__/WalletTypes.test.ts | 28 +++--- src/client/Bolt.ts | 13 ++- src/native/NativeGooglePayButton.ts | 1 + src/payments/GoogleWallet.tsx | 90 +++++++++++++++++-- src/payments/index.ts | 1 + src/payments/types.ts | 52 +++++++---- 12 files changed, 246 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 1c5fca8..07676db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # @boltpay/react-native +[![npm version](https://img.shields.io/npm/v/@boltpay/react-native)](https://www.npmjs.com/package/@boltpay/react-native) +[![build](https://img.shields.io/github/actions/workflow/status/BoltApp/bolt-react-native-sdk/ci.yml?branch=main)](https://github.com/BoltApp/bolt-react-native-sdk/actions/workflows/ci.yml) +[![license](https://img.shields.io/npm/l/@boltpay/react-native)](LICENSE) + Bolt React Native SDK for payments. Provides Credit Card tokenization, 3D Secure verification, Apple Pay, and Google Pay — all integrated with the Bolt payment platform. ## Architecture @@ -214,16 +218,7 @@ Then re-run `expo prebuild` and rebuild. ### 5. Google Pay (Android) -#### Prerequisites - -Google Pay requires two merchant identifiers: - -- **`gatewayMerchantId`** — Your Bolt merchant ID from the Bolt dashboard. This is used in the tokenization specification to route the payment through Bolt's gateway. -- **`googleMerchantId`** (optional) — Your Google-assigned merchant ID from the [Google Pay Business Console](https://pay.google.com/business/console/) (format: `BCR2DN...`). Required for production. In the test environment this can be omitted. - -> **Common mistake:** Using your Android application ID (e.g., `com.example.myapp`) for either of these fields will cause an `OR_BIBED_06` error. The `gatewayMerchantId` must be your Bolt merchant ID and the `googleMerchantId` must be the ID from Google's console. - -#### Usage +Merchant and gateway configuration (tokenization spec, merchant IDs) is automatically fetched from Bolt's API using your publishable key — you only need to provide presentation options like currency and amount. ```typescript import { GoogleWallet } from '@boltpay/react-native/payments'; @@ -232,17 +227,15 @@ function CheckoutScreen() { return ( { - // result: { token, bin?, expiration?, billingAddress?, boltReference? } + // result: { token, bin?, expiration?, email?, billingAddress?, boltReference? } }} onError={(error) => console.error(error)} /> @@ -322,10 +315,11 @@ cc.setStyles({ | Prop | Type | Default | Description | | -------------- | --------------------------- | --------- | ----------------------------------------------------------------------------------------------------- | -| `config` | `GooglePayConfig` | required | Gateway/Google merchant IDs, merchant name, country/currency, and total price | +| `config` | `GooglePayConfig` | required | Presentation options: currency, amount, label, billing address format. Merchant config is auto-fetched from Bolt. | | `onComplete` | `(GooglePayResult) => void` | required | Called with token, bin, expiration, and billing address on success | | `onError` | `(Error) => void` | — | Called on payment failure or cancellation | | `buttonType` | `GooglePayButtonType` | `'plain'` | Maps to Google Pay `ButtonConstants.ButtonType`. Button text is rendered natively and auto-localized. | +| `buttonTheme` | `GooglePayButtonTheme` | `'dark'` | Button color theme: `'dark'` or `'light'`. Maps to `ButtonConstants.ButtonTheme`. | | `borderRadius` | `number` | — | Corner radius in dp applied to the Google Pay button | | `style` | `ViewStyle` | — | Container style overrides (height, margin, etc.) | @@ -350,7 +344,9 @@ cc.setStyles({ - `ApplePayButtonType` — Apple-approved button label variants (`'plain'`, `'buy'`, `'checkout'`, `'book'`, `'subscribe'`, `'donate'`, `'order'`, `'setUp'`, `'inStore'`, `'reload'`, `'addMoney'`, `'topUp'`, `'rent'`, `'support'`, `'contribute'`, `'tip'`) - `GooglePayResult` — `{ token, bin?, expiration?, email?, billingAddress?, boltReference? }` - `GooglePayButtonType` — Google-approved button label variants (`'plain'`, `'buy'`, `'pay'`, `'checkout'`, `'subscribe'`, `'donate'`, `'order'`, `'book'`) -- `ApplePayConfig`, `GooglePayConfig` — Configuration for wallet buttons +- `GooglePayButtonTheme` — Button color theme (`'dark'`, `'light'`) +- `ApplePayConfig` — Apple Pay configuration (merchant ID, country/currency, total) +- `GooglePayConfig` — `{ billingAddressCollectionFormat?, currencyCode?, label?, amount? }` (merchant/gateway config auto-fetched from Bolt) ### Error Codes (`ThreeDSError`) diff --git a/android/src/main/java/com/boltreactnativesdk/GooglePayButtonView.kt b/android/src/main/java/com/boltreactnativesdk/GooglePayButtonView.kt index f7e8180..17fca4a 100644 --- a/android/src/main/java/com/boltreactnativesdk/GooglePayButtonView.kt +++ b/android/src/main/java/com/boltreactnativesdk/GooglePayButtonView.kt @@ -19,6 +19,7 @@ import com.google.android.gms.wallet.button.PayButton class GooglePayButtonView(context: Context) : FrameLayout(context) { private var currentButtonType: String = "plain" + private var currentButtonTheme: String = "dark" private var cornerRadiusPx: Float = 0f init { @@ -46,12 +47,19 @@ class GooglePayButtonView(context: Context) : FrameLayout(context) { rebuildButton() } + fun updateButtonTheme(theme: String) { + if (theme == currentButtonTheme) return + currentButtonTheme = theme + rebuildButton() + } + private fun rebuildButton() { removeAllViews() val button = PayButton(context) val options = ButtonOptions.newBuilder() .setButtonType(mapButtonType(currentButtonType)) + .setButtonTheme(mapButtonTheme(currentButtonTheme)) .setAllowedPaymentMethods(ALLOWED_PAYMENT_METHODS) .build() button.initialize(options) @@ -85,6 +93,11 @@ class GooglePayButtonView(context: Context) : FrameLayout(context) { else -> ButtonConstants.ButtonType.PLAIN } + fun mapButtonTheme(theme: String): Int = when (theme) { + "light" -> ButtonConstants.ButtonTheme.LIGHT + else -> ButtonConstants.ButtonTheme.DARK + } + // Minimal allowed payment methods JSON required by PayButton.initialize() private const val ALLOWED_PAYMENT_METHODS = """ [{"type":"CARD","parameters":{"allowedAuthMethods":["PAN_ONLY","CRYPTOGRAM_3DS"],"allowedCardNetworks":["VISA","MASTERCARD","AMEX","DISCOVER"]}}] diff --git a/android/src/main/java/com/boltreactnativesdk/GooglePayButtonViewManager.kt b/android/src/main/java/com/boltreactnativesdk/GooglePayButtonViewManager.kt index 95eceed..fffbb1c 100644 --- a/android/src/main/java/com/boltreactnativesdk/GooglePayButtonViewManager.kt +++ b/android/src/main/java/com/boltreactnativesdk/GooglePayButtonViewManager.kt @@ -37,6 +37,11 @@ class GooglePayButtonViewManager : view.updateButtonType(type ?: "plain") } + @ReactProp(name = "buttonTheme") + override fun setButtonTheme(view: GooglePayButtonView, theme: String?) { + view.updateButtonTheme(theme ?: "dark") + } + @ReactProp(name = "borderRadius", defaultFloat = 0f) override fun setBorderRadius(view: GooglePayButtonView, borderRadius: Float) { view.updateBorderRadius(PixelUtil.toPixelFromDIP(borderRadius)) diff --git a/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt b/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt index c3e76e2..ba24291 100644 --- a/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt +++ b/android/src/main/java/com/boltreactnativesdk/GooglePayModule.kt @@ -21,6 +21,10 @@ import java.net.URL * 1. Checking Google Pay readiness * 2. Presenting the Google Pay payment sheet * 3. Tokenizing the result via Bolt's tokenizer API + * + * The merchant/gateway configuration (tokenization spec, merchant ID, etc.) + * is fetched from Bolt's /v1/apm_config/googlepay endpoint on the JS side + * and passed down in the config JSON. */ class GooglePayModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { @@ -174,18 +178,31 @@ class GooglePayModule(reactContext: ReactApplicationContext) : val cardParams = JSONObject() cardParams.put("allowedAuthMethods", JSONArray(listOf("PAN_ONLY", "CRYPTOGRAM_3DS"))) cardParams.put("allowedCardNetworks", JSONArray(listOf("VISA", "MASTERCARD", "AMEX", "DISCOVER"))) - cardParams.put("billingAddressRequired", true) - val billingAddressParams = JSONObject() - billingAddressParams.put("format", "FULL") - billingAddressParams.put("phoneNumberRequired", true) - cardParams.put("billingAddressParameters", billingAddressParams) - - val tokenSpec = JSONObject() - tokenSpec.put("type", "PAYMENT_GATEWAY") - val tokenParams = JSONObject() - tokenParams.put("gateway", "bolt") - tokenParams.put("gatewayMerchantId", config.optString("gatewayMerchantId", "")) - tokenSpec.put("parameters", tokenParams) + + // Billing address + val billingFormat = config.optString("billingAddressFormat", "FULL") + if (billingFormat != "NONE") { + cardParams.put("billingAddressRequired", true) + val billingAddressParams = JSONObject() + billingAddressParams.put("format", billingFormat) + billingAddressParams.put("phoneNumberRequired", true) + cardParams.put("billingAddressParameters", billingAddressParams) + } + + // Tokenization spec from Bolt API config + val tokenSpecConfig = config.optJSONObject("tokenizationSpecification") + val tokenSpec = if (tokenSpecConfig != null) { + // Use the tokenization spec from Bolt's apm_config API + tokenSpecConfig + } else { + // Fallback: shouldn't happen in normal flow + val spec = JSONObject() + spec.put("type", "PAYMENT_GATEWAY") + val tokenParams = JSONObject() + tokenParams.put("gateway", "bolt") + spec.put("parameters", tokenParams) + spec + } val cardMethod = JSONObject() cardMethod.put("type", "CARD") @@ -198,13 +215,12 @@ class GooglePayModule(reactContext: ReactApplicationContext) : val transactionInfo = JSONObject() transactionInfo.put("totalPrice", config.optString("totalPrice", "0.00")) transactionInfo.put("totalPriceStatus", config.optString("totalPriceStatus", "FINAL")) - transactionInfo.put("countryCode", config.optString("countryCode", "US")) transactionInfo.put("currencyCode", config.optString("currencyCode", "USD")) params.put("transactionInfo", transactionInfo) - // Merchant info + // Merchant info from Bolt API config val merchantInfo = JSONObject() - merchantInfo.put("merchantId", config.optString("googleMerchantId", "")) + merchantInfo.put("merchantId", config.optString("merchantId", "")) merchantInfo.put("merchantName", config.optString("merchantName", "")) params.put("merchantInfo", merchantInfo) params.put("emailRequired", true) diff --git a/example/src/App.tsx b/example/src/App.tsx index 8cd939a..2993c7d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -237,13 +237,9 @@ const AddCardScreen = () => { {Platform.OS === 'android' && ( , [string]>(); @@ -34,6 +35,7 @@ jest.mock('../client/useBolt', () => ({ useBolt: () => ({ publishableKey: 'pk_test_123', baseUrl: 'https://connect.bolt.com', + apiUrl: 'https://api.bolt.com', }), })); @@ -49,13 +51,25 @@ jest.mock('../telemetry/tracer', () => ({ })); const baseConfig: GooglePayConfig = { - gatewayMerchantId: 'BOLT_MERCHANT_ID', - googleMerchantId: 'BCR2DN6T7654321', - merchantName: 'Demo Store', - countryCode: 'US', currencyCode: 'USD', - totalPrice: '10.00', - totalPriceStatus: 'FINAL', + amount: '10.00', + label: 'Test Purchase', + billingAddressCollectionFormat: 'full', +}; + +const mockAPMConfig = { + bolt_config: { + credit_card_processor: 'bolt', + tokenization_specification: { + type: 'PAYMENT_GATEWAY', + parameters: { + gateway: 'bolt', + gatewayMerchantId: 'BOLT_MERCHANT_ID', + }, + }, + merchant_id: 'BCR2DN6T7654321', + merchant_name: 'Demo Store', + }, }; describe('GoogleWallet', () => { @@ -152,6 +166,19 @@ describe('GoogleWallet', () => { }); }); + describe('APM config', () => { + it('should have the expected bolt_config shape', () => { + const config = mockAPMConfig.bolt_config; + expect(config.merchant_id).toBe('BCR2DN6T7654321'); + expect(config.merchant_name).toBe('Demo Store'); + expect(config.tokenization_specification.type).toBe('PAYMENT_GATEWAY'); + expect(config.tokenization_specification.parameters).toEqual({ + gateway: 'bolt', + gatewayMerchantId: 'BOLT_MERCHANT_ID', + }); + }); + }); + describe('buttonType defaults', () => { it('should accept all valid GooglePayButtonType values', () => { const validTypes: GooglePayButtonType[] = [ diff --git a/src/__tests__/WalletTypes.test.ts b/src/__tests__/WalletTypes.test.ts index 07af0ee..18c050e 100644 --- a/src/__tests__/WalletTypes.test.ts +++ b/src/__tests__/WalletTypes.test.ts @@ -98,20 +98,26 @@ describe('GooglePay types', () => { expect(result.boltReference).toBeUndefined(); }); - it('GooglePayConfig should require gatewayMerchantId, merchantName, countryCode, currencyCode, totalPrice', () => { + it('GooglePayConfig should accept presentation options only', () => { const config: GooglePayConfig = { - gatewayMerchantId: 'BOLT_MERCHANT_ID', - googleMerchantId: 'BCR2DN6T7654321', - merchantName: 'Demo Store', - countryCode: 'US', currencyCode: 'USD', - totalPrice: '0.00', - totalPriceStatus: 'ESTIMATED', + amount: '0.00', + label: 'Card Verification', + billingAddressCollectionFormat: 'full', }; - expect(config.gatewayMerchantId).toBe('BOLT_MERCHANT_ID'); - expect(config.googleMerchantId).toBe('BCR2DN6T7654321'); - expect(config.totalPrice).toBe('0.00'); - expect(config.totalPriceStatus).toBe('ESTIMATED'); + expect(config.currencyCode).toBe('USD'); + expect(config.amount).toBe('0.00'); + expect(config.label).toBe('Card Verification'); + expect(config.billingAddressCollectionFormat).toBe('full'); + }); + + it('GooglePayConfig should allow all fields to be optional', () => { + const config: GooglePayConfig = {}; + + expect(config.currencyCode).toBeUndefined(); + expect(config.amount).toBeUndefined(); + expect(config.label).toBeUndefined(); + expect(config.billingAddressCollectionFormat).toBeUndefined(); }); }); diff --git a/src/client/Bolt.ts b/src/client/Bolt.ts index 28d4ff1..e998ea2 100644 --- a/src/client/Bolt.ts +++ b/src/client/Bolt.ts @@ -14,9 +14,16 @@ const ENVIRONMENT_URLS: Record = { staging: 'https://connect-staging.bolt.com', }; +const API_URLS: Record = { + production: 'https://api.bolt.com', + sandbox: 'https://api-sandbox.bolt.com', + staging: 'https://api-staging.bolt.com', +}; + export class Bolt { public readonly publishableKey: string; public readonly baseUrl: string; + public readonly apiUrl: string; public readonly language: string; private onPageStyles?: Styles; @@ -25,10 +32,10 @@ export class Bolt { throw new Error('Bolt: publishableKey is required'); } + const env = config.environment ?? 'production'; this.publishableKey = config.publishableKey; - this.baseUrl = - ENVIRONMENT_URLS[config.environment ?? 'production'] ?? - ENVIRONMENT_URLS.production!; + this.baseUrl = ENVIRONMENT_URLS[env] ?? ENVIRONMENT_URLS.production!; + this.apiUrl = API_URLS[env] ?? API_URLS.production!; this.language = config.language ?? 'en'; initTelemetry(config); diff --git a/src/native/NativeGooglePayButton.ts b/src/native/NativeGooglePayButton.ts index 179bf67..dc60495 100644 --- a/src/native/NativeGooglePayButton.ts +++ b/src/native/NativeGooglePayButton.ts @@ -7,6 +7,7 @@ import { codegenNativeComponent } from 'react-native'; interface NativeProps extends ViewProps { buttonType: string; + buttonTheme?: string; borderRadius?: Float; onPress: BubblingEventHandler<{}>; } diff --git a/src/payments/GoogleWallet.tsx b/src/payments/GoogleWallet.tsx index f7196a0..db4ced7 100644 --- a/src/payments/GoogleWallet.tsx +++ b/src/payments/GoogleWallet.tsx @@ -6,6 +6,9 @@ import type { GooglePayResult, GooglePayConfig, GooglePayButtonType, + GooglePayButtonTheme, + GooglePayAPMConfigResponse, + GooglePayAPMConfig, } from './types'; import { startSpan, SpanStatusCode } from '../telemetry/tracer'; import { BoltAttributes } from '../telemetry/attributes'; @@ -25,14 +28,42 @@ export interface GoogleWalletProps { onError?: (error: Error) => void; style?: ViewStyle; buttonType?: GooglePayButtonType; + buttonTheme?: GooglePayButtonTheme; borderRadius?: number; } +/** + * Fetch Google Pay configuration from Bolt's API. + * The config includes tokenization spec, merchant ID, and merchant name + * so the developer doesn't need to provide them. + */ +const fetchGooglePayAPMConfig = async ( + apiUrl: string, + publishableKey: string +): Promise => { + const response = await fetch(`${apiUrl}/v1/apm_config/googlepay`, { + method: 'GET', + headers: { + merchant_token: publishableKey, + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Google Pay config: ${response.status} ${response.statusText}` + ); + } + + const data: GooglePayAPMConfigResponse = await response.json(); + return data.bolt_config; +}; + /** * — renders a native Google Pay button that triggers the * native PaymentsClient payment sheet via the BoltGooglePay TurboModule. * - * Only renders on Android when Google Pay is available. + * Merchant/gateway configuration is automatically fetched from Bolt's API + * using the publishable key. Only renders on Android when Google Pay is available. */ export const GoogleWallet = ({ config, @@ -40,23 +71,43 @@ export const GoogleWallet = ({ onError, style, buttonType = 'plain', + buttonTheme, borderRadius, }: GoogleWalletProps) => { const bolt = useBolt(); const [available, setAvailable] = useState(false); + const [apmConfig, setApmConfig] = useState(null); + // Fetch Bolt Google Pay config on mount useEffect(() => { - if (Platform.OS !== 'android' || !NativeGooglePay) { + if (Platform.OS !== 'android') return; + + fetchGooglePayAPMConfig(bolt.apiUrl, bolt.publishableKey) + .then(setApmConfig) + .catch((err) => { + onError?.( + err instanceof Error + ? err + : new Error('Failed to fetch Google Pay config') + ); + }); + }, [bolt.apiUrl, bolt.publishableKey, onError]); + + // Check Google Pay readiness once we have the APM config + useEffect(() => { + if (Platform.OS !== 'android' || !NativeGooglePay || !apmConfig) { setAvailable(false); return; } - NativeGooglePay.isReadyToPay(JSON.stringify(config)) + + const nativeConfig = buildNativeConfig(config, apmConfig); + NativeGooglePay.isReadyToPay(JSON.stringify(nativeConfig)) .then(setAvailable) .catch(() => setAvailable(false)); - }, [config]); + }, [config, apmConfig]); const handlePress = useCallback(async () => { - if (!NativeGooglePay) { + if (!NativeGooglePay || !apmConfig) { onError?.(new Error('Google Pay is not available')); return; } @@ -67,8 +118,9 @@ export const GoogleWallet = ({ }); try { + const nativeConfig = buildNativeConfig(config, apmConfig); const resultJson = await NativeGooglePay.requestPayment( - JSON.stringify(config), + JSON.stringify(nativeConfig), bolt.publishableKey, bolt.baseUrl ); @@ -84,7 +136,7 @@ export const GoogleWallet = ({ span.end(); onError?.(error); } - }, [config, bolt, onComplete, onError]); + }, [config, apmConfig, bolt, onComplete, onError]); if (!available || !BoltGooglePayButton) { return null; @@ -93,6 +145,7 @@ export const GoogleWallet = ({ return ( ); }; + +/** + * Build the config object sent to the native module by merging + * the Bolt API config with the developer's presentation options. + */ +const buildNativeConfig = ( + config: GooglePayConfig, + apmConfig: GooglePayAPMConfig +) => { + return { + // From Bolt API + merchantId: apmConfig.merchant_id, + merchantName: apmConfig.merchant_name, + tokenizationSpecification: apmConfig.tokenization_specification, + // From developer + currencyCode: config.currencyCode ?? 'USD', + totalPrice: config.amount ?? '0.00', + totalPriceStatus: 'FINAL', + totalPriceLabel: config.label, + billingAddressFormat: + config.billingAddressCollectionFormat === 'none' ? 'NONE' : 'FULL', + }; +}; diff --git a/src/payments/index.ts b/src/payments/index.ts index bf9075a..e02e0e2 100644 --- a/src/payments/index.ts +++ b/src/payments/index.ts @@ -31,6 +31,7 @@ export type { ApplePayBillingContact, ApplePayConfig, GooglePayButtonType, + GooglePayButtonTheme, GooglePayResult, GooglePayBillingAddress, GooglePayConfig, diff --git a/src/payments/types.ts b/src/payments/types.ts index 1708709..7838bb6 100644 --- a/src/payments/types.ts +++ b/src/payments/types.ts @@ -116,6 +116,11 @@ export type GooglePayButtonType = | 'order' | 'book'; +/** + * Google Pay button color theme. Maps to ButtonConstants.ButtonTheme on Android. + */ +export type GooglePayButtonTheme = 'dark' | 'light'; + // ── Apple Pay Types ───────────────────────────────────────── export interface ApplePayResult { @@ -173,21 +178,36 @@ export interface GooglePayBillingAddress { phoneNumber?: string; } +/** + * Configuration for the Google Pay button. Merchant/gateway config is + * automatically fetched from Bolt's `/v1/apm_config/googlepay` endpoint + * using the publishable key — you only need to provide presentation options. + */ export interface GooglePayConfig { - /** - * Your Bolt merchant ID, used as `gatewayMerchantId` in the tokenization - * specification. This is the merchant identifier from your Bolt dashboard. - */ - gatewayMerchantId: string; - /** - * Your Google-assigned merchant ID from the Google Pay & Wallet Console - * (https://pay.google.com/business/console). Format: `BCR2DN...`. - * Required for production; in TEST environment you may pass an empty string. - */ - googleMerchantId?: string; - merchantName: string; - countryCode: string; - currencyCode: string; - totalPrice: string; - totalPriceStatus?: 'FINAL' | 'ESTIMATED'; + /** Billing address collection: "full" collects all fields, "none" skips. Defaults to "full". */ + billingAddressCollectionFormat?: 'full' | 'none'; + /** ISO 4217 currency code. Defaults to "USD". */ + currencyCode?: string; + /** Label shown in the Google Pay sheet (e.g. "Store card for future charges"). */ + label?: string; + /** Total price as a string (e.g. "10.00"). Defaults to "0.00". */ + amount?: string; +} + +// ── Internal Google Pay APM Config (from Bolt API) ───────── + +/** Shape returned by GET /v1/apm_config/googlepay */ +export interface GooglePayAPMConfigResponse { + merchant_config?: GooglePayAPMConfig; + bolt_config: GooglePayAPMConfig; +} + +export interface GooglePayAPMConfig { + credit_card_processor: string; + tokenization_specification: { + type: string; + parameters: Record; + }; + merchant_id: string; + merchant_name: string; }