Skip to content
Open
75 changes: 75 additions & 0 deletions client/components/bank/oneTimePayment/html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Bank ACH One-time Payment Sample Integration

This is a demo of how to integration Bank ACH One-time payment via PayPal Web SDK v6. Paypal SDK lets merchants provide Bank ACH as a payment method via plain HTML and javascript.

## ️Architecture Overview

This sample demonstrates a complete Bank ACH integration flow:

1. Initialize PayPal Web SDK with Bank ACH component with a sample order creation.
2. Opens popup for payer bank authentication and account selection when Bank ACH button is clicked.
3. OnSuccess callback passed to SDK initialization captures order with returned order ID.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
3. OnSuccess callback passed to SDK initialization captures order with returned order ID.
3. `onApprove` callback passed to SDK initialization for capturing the order with the returned order ID.


### Server Setup

1. **Navigate to the server directory:**

```bash
cd server/node
```

2. **Install dependencies:**

```bash
npm install
```

3. **Configure environment variables:**
Create a `.env` file in the root directory:

```env
PAYPAL_SANDBOX_CLIENT_ID=your_paypal_sandbox_client_id
PAYPAL_SANDBOX_CLIENT_SECRET=your_paypal_sandbox_client_secret
```

4. **Start the server:**
```bash
npm start
```
The server will run on `https://localhost:8080`

### Client Setup

1. **Navigate to the Bank ACH One Time Payment demo directory:**

```bash
cd client/components/bank/oneTimePayment/html
```

2. **Install dependencies:**

```bash
npm install
```

3. **Start the development server:**
```bash
npm start
```
The demo will be available at `http://localhost:3000`


## 🧪 Testing the Integration

1. **Visit http://localhost:3000**
- Enter your Merchant ID
- Click the Bank Payment button
- The bank login popup will be displayed

3. **Complete bank login and account selection**
- Enter your credentials to get authenticated
- Select the bank and account you would like to use for testing

4. **Verify Results**
- Check the browser console logs for order capture details
- Check Event Logs -> API Calls at [developer.paypal.com](https://developer.paypal.com)
19 changes: 19 additions & 0 deletions client/components/bank/oneTimePayment/html/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "v6-web-sdk-sample-integration-client-html",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"preview": "vite preview",
"start": "vite",
"format": "prettier . --write",
"format:check": "prettier . --check",
"lint": "echo \"eslint is not set up\""
},
"license": "Apache-2.0",
"devDependencies": {
"prettier": "^3.6.2",
"vite": "^7.0.4"
}
}
131 changes: 131 additions & 0 deletions client/components/bank/oneTimePayment/html/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const getMerchantId = () => {
return document.getElementById('merchant-id-input').value || "7NU2UD5KWFN6U" // placeholder value. only for SB testing
}

const getOrderAmount = () => {
return document.getElementById('order-amount-input').value || "100.00"
}

const orderPayload = {
intent: 'CAPTURE',
purchaseUnits: [
{
amount: {
currencyCode: "USD",
value: getOrderAmount(),
},
payee: {
merchantId: getMerchantId()
}
},
]
}

async function onPayPalWebSdkLoaded() {
try {
const clientToken = await getBrowserSafeClientToken();
const sdkInstance = await window.paypal.createInstance({
clientToken,
components: ["bank-ach-payments"],
pageType: "checkout",
});

const paymentMethods = await sdkInstance.findEligibleMethods({
currencyCode: "USD",
paymentFlow: "ONE_TIME_PAYMENT",
merchantId: getMerchantId(),
amount: getOrderAmount()
});

if (paymentMethods.isEligible("ach")) {
setupAchButton(sdkInstance);
}
console.log('sdk loaded')
} catch (error) {
console.error(error);
}
}

const paymentSessionOptions = {
async onApprove(data) {
console.log("onApprove", data);
const orderData = await captureOrder({
orderId: data.orderId,
});
console.log("Capture order", orderData);
},
onCancel(data) {
console.log("onCancel", data);
},
onError(error) {
console.log("onError", error);
},
};


async function setupAchButton(sdkInstance) {
const achPaymentSession = sdkInstance.createBankAchOneTimePaymentSession(
paymentSessionOptions,
);
document.getElementById('ach-button-container').innerHTML = '<bank-ach-button id="ach-button">';
const onClick = async () => {
const startOptions = {
presentationMode: "popup",
}

const checkoutOptionsPromise = createOrder(orderPayload).then((orderId) => {
console.log("Created order", orderId);
return orderId
});
try {
await achPaymentSession.start(startOptions, checkoutOptionsPromise)
} catch(e) {
console.error(e)
}
}
const achButton = document.querySelector("#ach-button");

achButton.removeAttribute("hidden");
achButton.addEventListener("click", onClick);
}

async function getBrowserSafeClientToken() {
const response = await fetch("/paypal-api/auth/browser-safe-client-token", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const { accessToken } = await response.json();

return accessToken;
}

async function createOrder(orderPayload) {
const response = await fetch("/paypal-api/checkout/orders/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(orderPayload),
});
const { id } = await response.json();

return {orderId: id};
}


async function captureOrder({ orderId }) {
const response = await fetch(
`/paypal-api/checkout/orders/${orderId}/capture`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
);
const data = await response.json();

return data;
}
49 changes: 49 additions & 0 deletions client/components/bank/oneTimePayment/html/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>One-Time Payment - Recommended Integration - PayPal Web SDK</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
#ach-button-container {
display: flex;
flex-direction: column;
gap: 12px;
}
#merchant-id-input {
margin-top: 12px;
padding: 8px;
font-size: 16px;
width: 300px;
}
#order-amount-input {
margin-top: 12px;
padding: 8px;
font-size: 16px;
width: 300px;
}
</style>
</head>
<body>
<h1>One-Time Payment Recommended Integration</h1>

<div id="ach-button-container">
<button id="ach-button" hidden>Pay with Bank Account</button>
</div>

<label for="merchant-id-input">Merchant ID:</label>
<input type="text" id="merchant-id-input" placeholder="Enter your merchant ID" />
Copy link
Contributor

Choose a reason for hiding this comment

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

My initial thought is to remove this merchant id option. It will be confusing for direct merchants using these examples. This merchant id only applies to the partner use case.

Suggested change
<input type="text" id="merchant-id-input" placeholder="Enter your merchant ID" />


<label for="order-amount-input">Order Amount (USD):</label>
<input type="text" id="order-amount-input" placeholder="100.00"/>
<script src="app.js"></script>

<script type="module">
const script = document.createElement('script');
script.async = true;
script.src = 'https://www.sandbox.paypal.com/web-sdk/v6/core';
script.onload = () => window.onPayPalWebSdkLoaded();
document.head.appendChild(script);
</script>
</body>
</html>
19 changes: 19 additions & 0 deletions client/components/bank/oneTimePayment/html/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig(() => {
return {
plugins: [],
root: "src",
server: {
port: 3000,
proxy: {
"/paypal-api": {
target: "http://localhost:8080",
changeOrigin: true,
secure: false,
},
},
},
};
});
Loading