This use case explains how to request a custom payment (e.g., for physical goods, services, or dynamically priced items) using the Braintree platform via cordova-plugin-purchase v13+ and its required Braintree extension (cordova-plugin-purchase-braintree).
Unlike store products, payment requests typically involve a custom amount and require server-side processing of a payment method nonce generated by the Braintree SDK.
1. Platform Setup
First, ensure your Braintree account (Sandbox and Production), Cordova project, and native platforms (iOS/Android) are correctly configured.
See [Setup Braintree][/setup/setup-braintree]
2. Code Implementation
Implement the JavaScript code to initialize the Braintree adapter, display payment details, request the payment using store.requestPayment(), and handle the resulting events.
This section details the code implementation steps for processing a custom payment using the Braintree platform via cordova-plugin-purchase and its Braintree extension.
1. Base Framework
First, ensure you have the basic HTML structure and initial JavaScript setup as outlined in the . This includes waiting for deviceready and basic plugin checks.
Next, implement the initializeStoreAndSetupListeners function. This involves:
Configuring Braintree options, crucially providing a clientTokenProvider (recommended) or a tokenizationKey (for testing).
Setting up the mandatorystore.validator. Your validator endpoint must be capable of receiving a Braintree payment method nonce and using the Braintree Server SDK to create a transaction.sale.
Setting up store.when() listeners to handle the approved (nonce received), verified (server processed nonce successfully), and finished (acknowledged) states.
Calling store.initialize() with the Braintree platform and options.
Lines 21-25: (Optional) Instantiate Iaptic helper if using it for token/validation.
Lines 28-60: Define braintreeOptions. The clientTokenProvider (Lines 30-57) is the recommended way to authorize the client SDK. It fetches a short-lived token from your server. Alternatively, uncomment and use tokenizationKey (Line 59) for sandbox testing. Optional Apple Pay/Google Pay/3DS settings can be added here.
Lines 63-66:Crucially, set store.validator to your backend endpoint that processes Braintree nonces. Without this, payments cannot be completed.
Lines 69-91: Set up store.when() listeners.
.approved(): Triggered when the Braintree SDK successfully generates a nonce. You must call transaction.verify() here to send the nonce to your validator.
.verified(): Triggered after your validator successfully processes the nonce (calls Braintree's transaction.sale) and returns a success response. Call receipt.finish() here and fulfill the order.
.unverified(): Handles validation failures reported by your server.
.finished(): Confirms the transaction is fully acknowledged by the plugin.
.cancelled(): Handles cancellations from the Drop-in UI (though the requestPayment promise .cancelled() is often more direct).
Lines 94-110: Call store.initialize() to activate the Braintree adapter. Update UI state based on success or failure.
3. User Interface (refreshUI)
Implement the refreshUI function to display the payment details and update the UI based on the payment state (LOADING, BASKET, IN_PROGRESS, PAYMENT_INITIATED, PAYMENT_APPROVED, PAYMENT_FINISHED).
www/js/index.js (refreshUI and state helpers)
// Global state variables (consider a more robust state management approach for larger apps)
let appState = 'LOADING'; // Initial state: LOADING, BASKET, IN_PROGRESS, PAYMENT_INITIATED, PAYMENT_APPROVED, PAYMENT_FINISHED
let appMessage = 'Initializing Payment...';
// Function to update the application's UI based on the current state
function refreshUI() {
const appEl = document.getElementById('app'); // Main container element
const messagesEl = document.getElementById('messages'); // Element for status messages
const paymentSectionEl = document.getElementById('payment-section'); // Payment specific section
const payButtonEl = document.getElementById('pay-button'); // The pay button
if (!appEl || !messagesEl || !paymentSectionEl || !payButtonEl) {
console.error('Required UI elements not found in index.html for refreshUI!');
// Attempt to create elements if missing (basic fallback for snippets)
if (!messagesEl && appEl) appEl.insertAdjacentHTML('afterbegin', '<div id="messages"></div>');
if (!paymentSectionEl && appEl) appEl.insertAdjacentHTML('beforeend', '<div id="payment-section"><button id="pay-button">Pay</button></div>');
// Re-query after potential creation
messagesEl = document.getElementById('messages');
paymentSectionEl = document.getElementById('payment-section');
payButtonEl = document.getElementById('pay-button');
if (!messagesEl || !paymentSectionEl || !payButtonEl) return; // Still missing, give up
}
console.log(`Refreshing UI - State: 
{appMessage}`);
// Update status message display
messagesEl.textContent = appMessage;
messagesEl.style.color = appState.startsWith('Error') || appState.includes('Failed') ? 'red' : '#555';
// Show/hide/update elements based on state
switch (appState) {
case 'LOADING':
paymentSectionEl.style.display = 'none'; // Hide payment section while loading
payButtonEl.disabled = true;
payButtonEl.textContent = 'Loading...';
break;
case 'BASKET':
paymentSectionEl.style.display = 'block'; // Show payment section
payButtonEl.disabled = false; // Enable pay button
payButtonEl.textContent = 'Pay Now';
break;
case 'IN_PROGRESS': // General processing state
case 'PAYMENT_INITIATED': // Drop-in UI shown
case 'PAYMENT_APPROVED': // Nonce received, verifying server-side
paymentSectionEl.style.display = 'block';
payButtonEl.disabled = true; // Disable button during processing
payButtonEl.textContent = 'Processing...';
break;
case 'PAYMENT_FINISHED':
paymentSectionEl.style.display = 'block';
payButtonEl.disabled = true; // Payment complete, disable button
payButtonEl.textContent = 'Payment Complete!';
// Optionally hide the payment section or show a different success message
// paymentSectionEl.innerHTML = '<p>Thank you for your purchase!</p>';
break;
default: // Includes error states if message indicates error
paymentSectionEl.style.display = 'block';
payButtonEl.disabled = true; // Keep disabled on error until resolved
payButtonEl.textContent = 'Pay Now';
break;
}
}
// Helper function to update state and trigger UI refresh
function setAppState(newState, message) {
console.log(`Setting state from ${appState} to ${newState}`);
appState = newState;
appMessage = message || ''; // Use provided message or clear it
refreshUI(); // Update the UI immediately after state change
}
// Initial UI state setup on load
document.addEventListener('deviceready', () => {
// Set initial state before store initialization might finish
setAppState('LOADING', 'Initializing Payment...');
}, false);
// Ensure refreshUI is called initially if needed elsewhere
// refreshUI(); // Call if needed outside of setAppState
Explanation:
This function manages showing/hiding elements and enabling/disabling the "Pay Now" button based on the appState variable.
The setAppState helper updates the state and calls refreshUI.
4. Payment Request (requestBraintreePayment)
Implement the function triggered by your "Pay Now" button. This function uses store.requestPayment() to initiate the Braintree flow.
www/js/index.js (requestBraintreePayment)
// Make this function globally accessible if called from HTML onclick
window.requestBraintreePayment = function() {
// Ensure CdvPurchase and its members are available
if (!window.CdvPurchase || !window.CdvPurchase.store) {
console.error('CdvPurchase is not defined.');
setAppState('BASKET', 'Error: Payment plugin not loaded.');
return;
}
const { store, Platform, ErrorCode } = CdvPurchase;
// --- Payment Details (Example) ---
const GADGET_ID = 'awesome_gadget_01';
const GADGET_TITLE = 'Awesome Gadget';
const GADGET_PRICE_MICROS = 19990000; // $19.99
const CURRENCY = 'USD';
// --- Optional User/Billing Info ---
const billingInfo = {
givenName: 'John',
surname: 'Doe',
streetAddress1: '123 Braintree St',
locality: 'Chicago',
region: 'IL', // State/Province code
postalCode: '60654',
countryCode: 'US', // 2-letter ISO code
// phoneNumber: '15551234567', // Optional
};
const userEmail = 'john.doe@example.com'; // Optional
// --- Update UI ---
setAppState('IN_PROGRESS', 'Initiating payment...'); // Update UI state
// --- Create and Execute Payment Request ---
store.requestPayment({
// Required fields
platform: Platform.BRAINTREE,
items: [{
id: GADGET_ID,
title: GADGET_TITLE,
pricing: {
priceMicros: GADGET_PRICE_MICROS,
// Currency can be inferred from top level if items don't specify
}
}],
amountMicros: GADGET_PRICE_MICROS, // Total amount for the request
currency: CURRENCY,
// Optional fields
description: `Payment for ${GADGET_TITLE}`, // Shown in some payment flows
billingAddress: billingInfo,
email: userEmail,
} /*, additionalData can go here if needed */)
.cancelled(() => {
console.log('User cancelled Braintree payment flow.');
setAppState('BASKET', 'Payment cancelled.');
})
.failed(error => {
console.error('Braintree payment request failed:', error);
setAppState('BASKET', `Payment failed: ${error.message}`);
})
.initiated(transaction => {
// Braintree Drop-In UI is likely presented now, or payment flow started.
console.log(`Transaction initiated (UI shown?): ${transaction.transactionId}`);
setAppState('PAYMENT_INITIATED', 'Please complete payment...');
})
.approved(transaction => {
// Nonce received from Braintree SDK, needs server processing.
// The 'initializeStoreAndSetupListeners' function should have registered
// a store.when().approved() listener that calls transaction.verify().
// This promise chain primarily handles UI flow and initiation errors.
console.log(`Payment approved by Braintree SDK (Nonce: ${transaction.transactionId}). Verification should be in progress...`);
setAppState('PAYMENT_APPROVED', 'Payment approved. Verifying with server...');
})
.finished(transaction => {
// This is called AFTER successful verification AND finish() in the event listener.
// The UI state should already be 'PAYMENT_FINISHED' set by the .verified listener.
console.log(`Payment finished and acknowledged (from promise): ${transaction.transactionId}`);
});
}
// Ensure setAppState is defined globally or accessible
if (typeof setAppState !== 'function') { setAppState = (state, message) => { console.log(`[State] 
{message}`); refreshUI(); }; }
Explanation:
Lines 10-29: Define payment details (items, total amount, currency) and optional billing/user info.
Line 32: Update UI state to show processing.
Lines 35-54: Call store.requestPayment() with platform: Platform.BRAINTREE and the payment details.
Lines 55-end: Chain promise handlers to manage the UI state during the payment flow:
.cancelled(): User closed the Drop-in UI.
.failed(): An error occurred initiating the payment request.
.initiated(): The Braintree Drop-in UI has likely been presented.
.approved(): The Braintree SDK returned a nonce; verification is now happening via the store.when().approved() listener setup earlier.
.finished(): Called after the entire flow (including verification and finish()) is complete.
With these pieces in place, your app can initialize Braintree, display payment options, request a payment, and handle the nonce processing via your backend validator.
3. Server-Side Nonce Processing (Mandatory)
The client-side flow only generates a payment method nonce. This nonce is temporary and represents the user's authorized payment method (e.g., card details, PayPal account).
Your backend server MUST receive this nonce (typically sent from the .approved() event via your store.validator endpoint).
Your server then uses the Braintree Server SDK (Node.js, PHP, Python, etc.) along with your Braintree Private Key to call the Braintree API (e.g., gateway.transaction.sale()) with the nonce and the payment amount.
This server-side call creates the actual charge on the payment method.
Your store.validator should return a success response only after the Braintree transaction is successfully created.
Never trust the client-side approved event alone to fulfill an order.
4. Server-to-Server Webhooks (Recommended)
For robust fulfillment, especially for asynchronous payment methods or post-settlement events, configure webhooks in your Braintree control panel to notify your server about transaction status changes (e.g., settlement confirmation, disputes).
When using a validation service like Iaptic to process the Braintree payment nonce, the service typically handles the communication with the Braintree gateway (e.g., calling transaction.sale). After a successful transaction on Braintree's side, Iaptic (or your custom validator) will notify your application backend via a server-to-server webhook.
This webhook informs your server that a specific payment (often linked to your internal user ID if provided during validation) has been successfully completed. Your server should then:
Verify the webhook's authenticity (e.g., check a secret signature provided by Iaptic).
Update the user's account status in your database (e.g., mark order as paid, grant access, credit virtual currency).
Respond to the webhook request with a success status (e.g., HTTP 200 OK) so the service knows it was received.
Here's an example structure of what a webhook payload might look like (actual format depends on your validator service):
Handling this webhook correctly on your server is crucial for reliable order fulfillment after a Braintree payment processed via a validator. Consult your chosen validation service's documentation for specific details on their webhook format and security recommendations.
(Note: This section uses Iaptic as an example validator service; adapt if using your own backend).
5. Testing
Follow the specific testing procedures using your Braintree Sandbox account and Braintree's test card numbers.
After implementing the code from the previous sections, you can test the Braintree payment flow.
Prerequisites:
Braintree Sandbox Account: Ensure you have set up a Braintree Sandbox account.
Client Token/Tokenization Key: Your app must be initialized with a valid Sandbox Client Token or Tokenization Key. Using production keys will result in real charges or errors.
Validator Endpoint (Mock or Real): You need a backend endpoint configured as store.validator that can receive the payment nonce from the .approved() step. For initial testing, this endpoint could simply log the received nonce and return a successful validation response to allow the .verified() and .finished() steps to proceed in the app. Do not fulfill orders based on this mock validation. Later, implement the actual Braintree transaction.sale call on your server.
Platform Setup: Build and run the app on a device or emulator (Android or iOS).
Testing Steps:
Launch App: Start your application. Check the console logs to ensure the Braintree platform initializes successfully and the UI reaches the 'Ready to pay' state.
Initiate Payment: Tap the "Pay Now" button in your app.
Braintree Drop-in UI: The Braintree Drop-in UI should appear, presenting payment options.
Enter Test Card Details: Select the "Card" option (or another if configured). Use one of Braintree's official test card numbers. A common one for success is:
Card Number:4111 1111 1111 1111
Expiry Date: Any date in the future (e.g., 12/2025)
CVV: Any 3 digits (e.g., 123)
Postal Code: Any 5 digits (e.g., 12345)
Confirm Payment: Tap the button to submit the payment (e.g., "Pay $XX.XX").
Observe App & Logs:
The Drop-in UI should close.
Your app's status message should update (e.g., "Payment approved. Verifying with server...").
Check console logs for the approved event, which includes the payment method nonce (transaction.transactionId).
Your validator endpoint should receive a request containing this nonce.
(If Validator Mocked/Successful): Your validator returns success. The app logs the verified event, calls receipt.finish(), logs the finished event, and updates the UI to "Payment Successful!".
Verify Server (Real Validator): If using a real validator, check your Braintree Sandbox control panel to confirm that a transaction corresponding to the nonce was successfully created (e.g., status "Submitted for Settlement"). Check your server logs to ensure fulfillment logic was triggered.
Testing Failures:
Use Braintree's specific test card numbers designed to trigger processor declines (e.g., 4242... often works, check Braintree docs for current decline cards).
Test cancelling the Drop-in UI (should trigger the .cancelled() callback in requestPayment).
Simulate failures in your validator endpoint to test the .unverified() handler.
This process allows you to verify the client-side flow and the crucial interaction with your backend for processing the payment nonce.