From 300bc7c0a91c698b556db406b6c3682df84c2913 Mon Sep 17 00:00:00 2001 From: raha Date: Wed, 18 Feb 2026 16:07:43 +0530 Subject: [PATCH 1/7] Updated the dev docs for scalapay and pixInternational --- .../pixInternationalPayments/html/src/app.js | 366 ++++++++++++++++++ .../html/src/index.html | 133 +++++++ .../scalapayPayments/html/README.md | 163 ++++++++ .../scalapayPayments/html/src/app.js | 327 ++++++++++++++++ .../scalapayPayments/html/src/index.html | 136 +++++++ client/index.html | 20 + server/node/src/paypalServerSdkClient.ts | 5 + server/node/src/routes/index.ts | 6 + server/node/src/routes/ordersRouteHandler.ts | 173 ++++++++- 9 files changed, 1308 insertions(+), 21 deletions(-) create mode 100644 client/components/pixInternationalPayments/html/src/app.js create mode 100644 client/components/pixInternationalPayments/html/src/index.html create mode 100644 client/components/scalapayPayments/html/README.md create mode 100644 client/components/scalapayPayments/html/src/app.js create mode 100644 client/components/scalapayPayments/html/src/index.html diff --git a/client/components/pixInternationalPayments/html/src/app.js b/client/components/pixInternationalPayments/html/src/app.js new file mode 100644 index 00000000..65ae9a07 --- /dev/null +++ b/client/components/pixInternationalPayments/html/src/app.js @@ -0,0 +1,366 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "BR", // Brazil for testing + components: ["pix-international-payments"], + }); + + // Check if Pix International is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "BRL", + }); + + const isPixInternationalEligible = paymentMethods.isEligible("pix_international"); + + if (isPixInternationalEligible) { + setupPixInternationalPayment(sdkInstance); + } else { + showMessage({ + text: "Pix International is not eligible. Please ensure your configuration is correct.", + type: "error", + }); + console.error("Pix International is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupPixInternationalPayment(sdkInstance) { + try { + // Create Pix International checkout session + const pixInternationalCheckout = sdkInstance.createPixInternationalOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(pixInternationalCheckout); + + // Setup button click handler + setupButtonHandler(pixInternationalCheckout); + } catch (error) { + console.error("Error setting up Pix International payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(pixInternationalCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = pixInternationalCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = pixInternationalCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#pix-international-full-name").appendChild(fullNameField); + document.querySelector("#pix-international-email").appendChild(emailField); +} + +function setupButtonHandler(pixInternationalCheckout) { + const pixInternationalButton = document.querySelector("#pix-international-button"); + pixInternationalButton.removeAttribute("hidden"); + + pixInternationalButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate tax info first + const taxInfo = validateTaxInfo(); + + // Validate the payment fields + const isValid = await pixInternationalCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrderWithTaxInfo(taxInfo); + + // Start payment flow with popup mode + await pixInternationalCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +function validateTaxInfo() { + const taxId = document.querySelector("#tax-id").value.trim(); + const taxIdType = document.querySelector("#tax-id-type").value.trim(); + const errors = []; + + if (!taxId) errors.push("Tax ID"); + if (!taxIdType) errors.push("Tax ID Type"); + + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(", ")}`; + throw new Error(errorMessage); + } + + return { + taxId, + taxIdType, + }; +} + +// Create PayPal order with tax info +async function createOrderWithTaxInfo(taxInfo) { + try { + console.log("Creating PayPal order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + reference_id: "Reference ID 2", + description: "Description of PU", + custom_id: "Custom-ID1", + soft_descriptor: "Purchase Descriptor", + amount: { + currency_code: "BRL", + value: "230.05", + breakdown: { + item_total: { + currency_code: "BRL", + value: "180.05", + }, + shipping: { + currency_code: "BRL", + value: "20.00", + }, + handling: { + currency_code: "BRL", + value: "10.00", + }, + tax_total: { + currency_code: "BRL", + value: "20.00", + }, + insurance: { + currency_code: "BRL", + value: "10.00", + }, + shipping_discount: { + currency_code: "BRL", + value: "10.00", + }, + }, + }, + items: [ + { + name: "Item 0", + description: "Description 0", + sku: "SKU - 0", + url: "www.example.com", + unit_amount: { + currency_code: "BRL", + value: "90.05", + }, + tax: { + currency_code: "BRL", + value: "10.00", + }, + quantity: "1", + category: "PHYSICAL_GOODS", + }, + { + name: "Item 1", + description: "Description 1", + sku: "SKU 1", + url: "www.example1.com", + unit_amount: { + currency_code: "BRL", + value: "45.00", + }, + tax: { + currency_code: "BRL", + value: "5.00", + }, + quantity: "2", + category: "PHYSICAL_GOODS", + }, + ], + shipping: { + method: "Postal Service", + }, + }, + ], + }; + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(orderPayload), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID with tax info for the payment session + return { + orderId: id, + taxInfo: { + taxId: taxInfo.taxId, + taxIdType: taxInfo.taxIdType, + }, + }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/pixInternationalPayments/html/src/index.html b/client/components/pixInternationalPayments/html/src/index.html new file mode 100644 index 00000000..38be51d6 --- /dev/null +++ b/client/components/pixInternationalPayments/html/src/index.html @@ -0,0 +1,133 @@ + + + + + Pix International - PayPal Web SDK + + + + + +

Pix International One-Time Payment Integration

+ +
+
+
+ +
+ +
+ + +
+
+ + +
+ +
+ + + + + diff --git a/client/components/scalapayPayments/html/README.md b/client/components/scalapayPayments/html/README.md new file mode 100644 index 00000000..6b9987dc --- /dev/null +++ b/client/components/scalapayPayments/html/README.md @@ -0,0 +1,163 @@ +# Scalapay One-Time Payment Integration + +This example demonstrates how to integrate Scalapay payments using PayPal's v6 Web SDK. Scalapay is a popular Buy Now, Pay Later payment method that allows customers to split their purchase into interest-free installments. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Scalapay integration flow: + +1. Initialize PayPal Web SDK with the Scalapay component +2. Check eligibility for Scalapay payment method +3. Create Scalapay payment session with required payment fields +4. Validate customer information (name, email, phone) before initiating payment +5. Authorize the payment through Scalapay popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Scalapay one-time payment integration +- Full name, email, and phone number field validation +- Popup payment flow with Scalapay authorization +- Eligibility checking for Scalapay +- Error handling and user feedback +- EUR (Euro) currency support +- Custom order creation with processing instruction + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Scalapay Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Scalapay** in the payment methods and enable it + - Ensure your account is configured to accept **EUR (Euro)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the main page:** + Open your browser and go to `http://localhost:8080` + +2. **Select Scalapay Payments:** + Click on the Scalapay Payments link in the Static Examples section + +## How It Works + +### Geographic Availability + +Scalapay is available in France and other European countries + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Scalapay components using a client ID fetched from the server's `/paypal-api/auth/browser-safe-client-id` endpoint +2. **Eligibility Check**: Verifies Scalapay is eligible for the merchant with EUR currency and FR buyer country +3. **Session Creation**: Creates Scalapay payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields, plus custom phone number input +5. **Validation**: Validates all fields (name, email, phone) before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authorize the payment with Scalapay. The order is created server-side via `/paypal-api/checkout/orders/create-order-for-scalapay` with custom processing instruction before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-id` - Generate client ID +- `POST /paypal-api/checkout/orders/create-order-for-scalapay` - Create order with custom processing instruction (must use EUR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +### Special Order Configuration + +Scalapay requires a special order creation payload with: +- `processingInstruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL"` +- Full breakdown of amounts including `itemTotal` +- Phone number passed during checkout session start + +## Troubleshooting + +### Scalapay not eligible + +- Verify `testBuyerCountry` is set to "FR" +- Check that `currencyCode` is set to "EUR" (Euro) +- Ensure Scalapay is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept EUR currency + +### Validation fails + +- Ensure all fields (name, email, phone) are properly filled +- Check that email format is correct +- Verify phone country code and national number are provided +- Ensure SDK fields (name, email) are mounted correctly + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with EUR currency +- Ensure phone data is passed correctly to the session options + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload includes `processingInstruction` +- **Ensure currency_code is set to "EUR"** - this is critical for Scalapay +- Verify breakdown includes `itemTotal` + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/scalapayPayments/html/src/app.js b/client/components/scalapayPayments/html/src/app.js new file mode 100644 index 00000000..a3ba7726 --- /dev/null +++ b/client/components/scalapayPayments/html/src/app.js @@ -0,0 +1,327 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "FR", // France for Scalapay testing + components: ["scalapay-payments"], + }); + + // Check if Scalapay is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "EUR", + }); + + const isScalapayEligible = paymentMethods.isEligible("scalapay"); + + if (isScalapayEligible) { + setupScalapayPayment(sdkInstance); + } else { + showMessage({ + text: "Scalapay is not eligible. Please ensure your buyer country is France and currency is EUR.", + type: "error", + }); + console.error("Scalapay is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupScalapayPayment(sdkInstance) { + try { + // Create Scalapay checkout session + const scalapayCheckout = sdkInstance.createScalapayOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(scalapayCheckout); + + // Setup button click handler + setupButtonHandler(scalapayCheckout); + } catch (error) { + console.error("Error setting up Scalapay payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(scalapayCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = scalapayCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create payment field for email with optional prefill + const emailField = scalapayCheckout.createPaymentFields({ + type: "email", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#scalapay-full-name").appendChild(fullNameField); + document.querySelector("#scalapay-email").appendChild(emailField); +} + +function setupButtonHandler(scalapayCheckout) { + const scalapayButton = document.querySelector("#scalapay-button"); + scalapayButton.removeAttribute("hidden"); + + scalapayButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate phone fields first + const phoneData = validatePhoneFields(); + + // Validate the payment fields (name and email) + const isValid = await scalapayCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode and phone data + await scalapayCheckout.start( + { presentationMode: "popup" }, + createOrderWithPhone(phoneData), + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: + error.message || + "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +function validatePhoneFields() { + const phoneCountryCode = document + .querySelector("#phone-country-code") + .value.trim(); + const phoneNationalNumber = document + .querySelector("#phone-national-number") + .value.trim(); + + const errors = []; + + if (!phoneCountryCode) errors.push("Phone Country Code"); + if (!phoneNationalNumber) errors.push("Phone National Number"); + + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(", ")}`; + throw new Error(errorMessage); + } + + return { + phoneCountryCode, + phoneNationalNumber, + }; +} + +// Create PayPal order with phone data +async function createOrderWithPhone(phoneData) { + try { + console.log("Creating PayPal order for Scalapay..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + amount: { + breakdown: { + item_total: { + currency_code: "EUR", + value: "24.10" + }, + tax_total: { + currency_code: "EUR", + value: "1.00" + } + }, + currency_code: "EUR", + value: "25.10" + }, + items: [ + { + name: "Shirt", + quantity: "1", + unit_amount: { + currency_code: "EUR", + value: "24.10" + }, + tax: { + currency_code: "EUR", + value: "1.00" + } + } + ] + } + ] + }; + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(orderPayload), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return session options with orderId and phone data + return { + orderId: id, + phone: { + nationalNumber: phoneData.phoneNationalNumber, + countryCode: phoneData.phoneCountryCode, + }, + }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/scalapayPayments/html/src/index.html b/client/components/scalapayPayments/html/src/index.html new file mode 100644 index 00000000..f200da9a --- /dev/null +++ b/client/components/scalapayPayments/html/src/index.html @@ -0,0 +1,136 @@ + + + + + Scalapay - PayPal Web SDK + + + + + +

Scalapay One-Time Payment Integration

+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ + + + + diff --git a/client/index.html b/client/index.html index d301e5ee..5490ad74 100644 --- a/client/index.html +++ b/client/index.html @@ -242,6 +242,26 @@

Static Examples

+
  • + Scalapay Payments + +
  • +
  • + Pix International Payments + +
  • Dynamic Examples

    diff --git a/server/node/src/paypalServerSdkClient.ts b/server/node/src/paypalServerSdkClient.ts index 05375962..12f2479f 100644 --- a/server/node/src/paypalServerSdkClient.ts +++ b/server/node/src/paypalServerSdkClient.ts @@ -6,6 +6,11 @@ if (!PAYPAL_SANDBOX_CLIENT_ID || !PAYPAL_SANDBOX_CLIENT_SECRET) { throw new Error("Missing API credentials"); } +export const PAYPAL_BASE_URL = + process.env.PAYPAL_ENVIRONMENT === "production" + ? "https://api-m.paypal.com" + : "https://api-m.sandbox.paypal.com"; + export const client = new Client({ clientCredentialsAuthCredentials: { oAuthClientId: PAYPAL_SANDBOX_CLIENT_ID, diff --git a/server/node/src/routes/index.ts b/server/node/src/routes/index.ts index 2db69fcf..d3f3b2b2 100644 --- a/server/node/src/routes/index.ts +++ b/server/node/src/routes/index.ts @@ -12,6 +12,7 @@ import { createOrderForOneTimePaymentWithShippingRouteHandler, createOrderForCardWithSingleUseTokenRouteHandler, createOrderForCardWithThreeDSecureRouteHandler, + getOrderRouteHandler, captureOrderRouteHandler, } from "./ordersRouteHandler"; @@ -64,6 +65,11 @@ router.post( createOrderForCardWithThreeDSecureRouteHandler, ); +router.get( + "/paypal-api/checkout/orders/:orderId", + getOrderRouteHandler, +); + router.post( "/paypal-api/checkout/orders/:orderId/capture", captureOrderRouteHandler, diff --git a/server/node/src/routes/ordersRouteHandler.ts b/server/node/src/routes/ordersRouteHandler.ts index bb85a584..68510076 100644 --- a/server/node/src/routes/ordersRouteHandler.ts +++ b/server/node/src/routes/ordersRouteHandler.ts @@ -13,11 +13,67 @@ import { z } from "zod/v4"; import { randomUUID } from "node:crypto"; import type { Request, Response } from "express"; -import { client } from "../paypalServerSdkClient"; +import { client, PAYPAL_BASE_URL } from "../paypalServerSdkClient"; import { getAllProducts, getProduct } from "../productCatalog"; const ordersController = new OrdersController(client); +const MoneySchema = z.object({ + currency_code: z.string(), + value: z.string(), +}); + +const ClientOrderPayloadSchema = z.object({ + intent: z.string(), + processing_instruction: z.string().optional(), + purchase_units: z.array( + z.object({ + reference_id: z.string().optional(), + description: z.string().optional(), + custom_id: z.string().optional(), + soft_descriptor: z.string().optional(), + amount: z.object({ + currency_code: z.string(), + value: z.string(), + breakdown: z + .object({ + item_total: MoneySchema.optional(), + tax_total: MoneySchema.optional(), + shipping: MoneySchema.optional(), + handling: MoneySchema.optional(), + insurance: MoneySchema.optional(), + shipping_discount: MoneySchema.optional(), + }) + .optional(), + }), + payee: z + .object({ + merchant_id: z.string().optional(), + }) + .optional(), + items: z + .array( + z.object({ + name: z.string(), + quantity: z.string(), + description: z.string().optional(), + url: z.string().optional(), + category: z.string().optional(), + sku: z.string().optional(), + unit_amount: MoneySchema, + tax: MoneySchema.optional(), + }), + ) + .optional(), + shipping: z + .object({ + method: z.string().optional(), + }) + .optional(), + }), + ), +}); + const OneTimePaymentSchema = z .object({ cart: z @@ -88,28 +144,86 @@ export async function createOrderForOneTimePaymentRouteHandler( request: Request, response: Response, ) { - const { currencyCode, totalAmount, items } = OneTimePaymentSchema.parse( - request.body, - ); - - const orderRequestBody = { - intent: CheckoutPaymentIntent.Capture, - purchaseUnits: [ - { - amount: { - currencyCode, - value: totalAmount, - breakdown: { - itemTotal: { - currencyCode: currencyCode, - value: totalAmount, + let orderRequestBody; + + if (request.body.purchase_units) { + // Direct API call path for APM-specific payloads (e.g., Scalapay, PIX) + // that require fields not fully supported by the SDK + try { + const parsed = ClientOrderPayloadSchema.parse(request.body); + + // console.log( + // "[createOrderForOneTimePayment] Orders payload sent to v2/checkout/orders:", + // JSON.stringify(parsed, null, 2), + // ); + + const token = + await client.clientCredentialsAuthManager.fetchToken(); + + const rawResponse = await fetch( + `${PAYPAL_BASE_URL}/v2/checkout/orders`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token.accessToken}`, + "PayPal-Request-Id": randomUUID(), + Prefer: "return=minimal", + }, + body: JSON.stringify(parsed), + }, + ); + + // Handle non-JSON responses + const contentType = rawResponse.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + const result = await rawResponse.json(); + // console.log( + // "[createOrderForOneTimePayment] Response:", + // JSON.stringify(result, null, 2), + // ); + return response.status(rawResponse.status).json(result); + } else { + const text = await rawResponse.text(); + console.error( + "[createOrderForOneTimePayment] Non-JSON response:", + text, + ); + return response.status(rawResponse.status).send(text); + } + } catch (error) { + console.error( + "[createOrderForOneTimePayment] Error creating order:", + error, + ); + return response.status(500).json({ + error: "Failed to create order", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + } else { + const { currencyCode, totalAmount, items } = OneTimePaymentSchema.parse( + request.body, + ); + orderRequestBody = { + intent: CheckoutPaymentIntent.Capture, + purchaseUnits: [ + { + amount: { + currencyCode, + value: totalAmount, + breakdown: { + itemTotal: { + currencyCode: currencyCode, + value: totalAmount, + }, }, }, + items, }, - items, - }, - ], - }; + ], + }; + } const { result, statusCode } = await ordersController.createOrder({ body: orderRequestBody, @@ -417,6 +531,23 @@ export async function createOrderForCardWithThreeDSecureRouteHandler( response.status(statusCode).json(result); } +export async function getOrderRouteHandler( + request: Request, + response: Response, +) { + const schema = z.object({ + orderId: z.string(), + }); + + const { orderId } = schema.parse(request.params); + + const { result, statusCode } = await ordersController.getOrder({ + id: orderId, + }); + + response.status(statusCode).json(result); +} + export async function captureOrderRouteHandler( request: Request, response: Response, @@ -438,4 +569,4 @@ export async function captureOrderRouteHandler( }); response.status(statusCode).json(result); -} +} \ No newline at end of file From 85978bcecc10d0e11e5d58a26afa02b3001b2d98 Mon Sep 17 00:00:00 2001 From: raha Date: Wed, 18 Feb 2026 16:24:57 +0530 Subject: [PATCH 2/7] Added Readme file for pix International --- .../pixInternationalPayments/html/README.md | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 client/components/pixInternationalPayments/html/README.md diff --git a/client/components/pixInternationalPayments/html/README.md b/client/components/pixInternationalPayments/html/README.md new file mode 100644 index 00000000..cee1ea90 --- /dev/null +++ b/client/components/pixInternationalPayments/html/README.md @@ -0,0 +1,238 @@ +# PIX International One-Time Payment Integration + +This example demonstrates how to integrate PIX International payments using PayPal's v6 Web SDK. PIX is Brazil's instant payment system, allowing customers to make real-time payments using their Brazilian bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete PIX International integration flow: + +1. Initialize PayPal Web SDK with the PIX International component +2. Check eligibility for PIX International payment method +3. Create PIX International payment session with required payment fields +4. Validate customer information (name, email, tax ID) before initiating payment +5. Authorize the payment through PIX International popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- PIX International one-time payment integration +- Full name, email, and tax ID field validation +- Popup payment flow with PIX International authorization +- Eligibility checking for PIX International +- Error handling and user feedback +- BRL (Brazilian Real) currency support +- Custom order creation with processing instruction +- Support for CPF (individual) and CNPJ (business) tax IDs +- Comprehensive amount breakdown with shipping, handling, insurance, and discounts + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable PIX International Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **PIX International** in the payment methods and enable it + - Ensure your account is configured to accept **BRL (Brazilian Real)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the main page:** + Open your browser and go to `http://localhost:8080` + +2. **Select PIX International Payments:** + Click on the PIX International Payments link in the Static Examples section + +## How It Works + +### Geographic Availability + +PIX International is available for payments in Brazil (BR) using Brazilian Real (BRL) currency. + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with PIX International components using a client ID fetched from the server's `/paypal-api/auth/browser-safe-client-id` endpoint +2. **Eligibility Check**: Verifies PIX International is eligible for the merchant with BRL currency and BR buyer country +3. **Session Creation**: Creates PIX International payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields, plus custom tax ID input fields (CPF/CNPJ) +5. **Validation**: Validates all fields (name, email, tax ID, tax ID type) before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authorize the payment with PIX International. The order is created server-side via `/paypal-api/checkout/orders/create-order-for-one-time-payment` with custom processing instruction before displaying the popup +7. **Completion**: Processes the payment result by fetching the order details via `/paypal-api/checkout/orders/:orderId` (order is auto-completed due to processing instruction), or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-id` - Generate client ID +- `POST /paypal-api/checkout/orders/create-order-for-one-time-payment` - Create order with custom processing instruction (must use BRL currency) +- `GET /paypal-api/checkout/orders/:orderId` - Fetch order details after approval + +### Special Order Configuration + +PIX International requires a special order creation payload with: +- `processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL"` - This auto-completes the payment on approval, so no capture is needed +- Full breakdown of amounts including: + - `item_total` - Sum of all item unit amounts + - `tax_total` - Sum of all item taxes (must equal `sum of (tax Γ— quantity)` for all items) + - `shipping` - Shipping cost (optional) + - `handling` - Handling fee (optional) + - `insurance` - Insurance cost (optional) + - `shipping_discount` - Shipping discount (optional) +- Tax ID information passed during checkout session start +- Currency must be BRL (Brazilian Real) + +### Important Amount Calculation Rules + +The total amount must equal: +``` +total = item_total + tax_total + shipping + handling + insurance - shipping_discount +``` + +**Tax Calculation:** +- Each item can have a `tax` field (per unit) +- The `tax_total` in breakdown must equal `sum of (item.tax Γ— item.quantity)` for all items +- Example: + - Item 1: tax = 10.00, quantity = 1 β†’ contributes 10.00 to tax_total + - Item 2: tax = 5.00, quantity = 2 β†’ contributes 10.00 to tax_total + - tax_total = 20.00 + +## Troubleshooting + +### PIX International not eligible + +- Verify `testBuyerCountry` is set to "BR" +- Check that `currencyCode` is set to "BRL" (Brazilian Real) +- Ensure PIX International is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept BRL currency + +### Validation fails + +- Ensure all fields (name, email, tax ID, tax ID type) are properly filled +- Check that email format is correct +- Verify tax ID is provided (CPF or CNPJ format) +- Select the correct tax ID type from the dropdown (BR_CPF or BR_CNPJ) +- Ensure SDK fields (name, email) are mounted correctly + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with BRL currency +- Ensure tax ID data is passed correctly to the session options + +### Order creation fails with AMOUNT_MISMATCH error + +- Verify that `total = item_total + tax_total + shipping + handling + insurance - shipping_discount` +- Check that all amounts use string values with proper decimal formatting (e.g., "230.05") +- Ensure all currency codes match (BRL) + +### Order creation fails with TAX_TOTAL_MISMATCH error + +- Verify `tax_total` equals the sum of `(item.tax Γ— item.quantity)` for all items +- Example calculation: + ``` + Item 0: tax 10.00 Γ— quantity 1 = 10.00 + Item 1: tax 5.00 Γ— quantity 2 = 10.00 + tax_total must be 20.00 + ``` +- Ensure each item's tax field is properly formatted as a string + +### Order creation fails with schema validation error + +- Verify all required fields are present in the order payload +- Check that the server-side schema (`ClientOrderPayloadSchema`) supports all fields: + - `reference_id`, `description`, `custom_id`, `soft_descriptor` + - `payee` object with `merchant_id` + - `shipping` object with `method` + - Item fields: `description`, `url`, `category`, `sku` (all optional) + +### "onPayPalWebSdkLoaded is not defined" error + +- Ensure `app.js` is loaded **before** the PayPal SDK script +- The correct script order in `index.html` should be: + ```html + + + ``` + +### API server issues + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload includes `processing_instruction` +- **Ensure currency_code is set to "BRL"** - this is critical for PIX International +- Verify breakdown includes all required fields + +## Tax ID Requirements + +PIX International requires Brazilian tax identification: + +- **CPF (Cadastro de Pessoas FΓ­sicas)**: Individual tax ID + - Format: 11 digits + - Type value: `BR_CPF` + +- **CNPJ (Cadastro Nacional da Pessoa JurΓ­dica)**: Business tax ID + - Format: 14 digits + - Type value: `BR_CNPJ` + +These are collected through custom input fields and passed to the payment session during checkout. + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) +- [PIX Payment System Overview](https://www.bcb.gov.br/en/financialstability/pix_en) From dd64eec781096a75a396d94383da51f1f2462aa9 Mon Sep 17 00:00:00 2001 From: "Reegan Gunasekaran (CW)" Date: Wed, 11 Feb 2026 10:44:52 +0530 Subject: [PATCH 3/7] adding devdocs to 12 APM --- .../afterpayPayments/html/README.md | 162 +++++++++++ .../afterpayPayments/html/src/app.js | 241 ++++++++++++++++ .../afterpayPayments/html/src/index.html | 84 ++++++ .../components/bizumPayments/html/README.md | 162 +++++++++++ .../components/bizumPayments/html/src/app.js | 143 ++++++++++ .../bizumPayments/html/src/index.html | 60 ++++ .../components/klarnaPayments/html/README.md | 163 +++++++++++ .../components/klarnaPayments/html/src/app.js | 257 ++++++++++++++++++ .../klarnaPayments/html/src/index.html | 97 +++++++ .../multibancoPayments/html/README.md | 162 +++++++++++ .../multibancoPayments/html/src/app.js | 140 ++++++++++ .../multibancoPayments/html/src/index.html | 60 ++++ .../components/mybankPayments/html/README.md | 162 +++++++++++ .../components/mybankPayments/html/src/app.js | 141 ++++++++++ .../mybankPayments/html/src/index.html | 60 ++++ client/components/payuPayments/html/README.md | 162 +++++++++++ .../components/payuPayments/html/src/app.js | 144 ++++++++++ .../payuPayments/html/src/index.html | 60 ++++ .../components/swishPayments/html/README.md | 162 +++++++++++ .../components/swishPayments/html/src/app.js | 169 ++++++++++++ .../swishPayments/html/src/index.html | 62 +++++ .../components/trustlyPayments/html/README.md | 162 +++++++++++ .../trustlyPayments/html/src/app.js | 158 +++++++++++ .../trustlyPayments/html/src/index.html | 65 +++++ .../components/twintPayments/html/README.md | 162 +++++++++++ .../components/twintPayments/html/src/app.js | 144 ++++++++++ .../twintPayments/html/src/index.html | 60 ++++ .../verkkopankkiPayments/html/README.md | 162 +++++++++++ .../verkkopankkiPayments/html/src/app.js | 158 +++++++++++ .../verkkopankkiPayments/html/src/index.html | 65 +++++ .../wechatpayPayments/html/README.md | 162 +++++++++++ .../wechatpayPayments/html/src/app.js | 143 ++++++++++ .../wechatpayPayments/html/src/index.html | 60 ++++ client/components/zipPayments/html/README.md | 162 +++++++++++ client/components/zipPayments/html/src/app.js | 237 ++++++++++++++++ .../zipPayments/html/src/index.html | 97 +++++++ 36 files changed, 4850 insertions(+) create mode 100644 client/components/afterpayPayments/html/README.md create mode 100644 client/components/afterpayPayments/html/src/app.js create mode 100644 client/components/afterpayPayments/html/src/index.html create mode 100644 client/components/bizumPayments/html/README.md create mode 100644 client/components/bizumPayments/html/src/app.js create mode 100644 client/components/bizumPayments/html/src/index.html create mode 100644 client/components/klarnaPayments/html/README.md create mode 100644 client/components/klarnaPayments/html/src/app.js create mode 100644 client/components/klarnaPayments/html/src/index.html create mode 100644 client/components/multibancoPayments/html/README.md create mode 100644 client/components/multibancoPayments/html/src/app.js create mode 100644 client/components/multibancoPayments/html/src/index.html create mode 100644 client/components/mybankPayments/html/README.md create mode 100644 client/components/mybankPayments/html/src/app.js create mode 100644 client/components/mybankPayments/html/src/index.html create mode 100644 client/components/payuPayments/html/README.md create mode 100644 client/components/payuPayments/html/src/app.js create mode 100644 client/components/payuPayments/html/src/index.html create mode 100644 client/components/swishPayments/html/README.md create mode 100644 client/components/swishPayments/html/src/app.js create mode 100644 client/components/swishPayments/html/src/index.html create mode 100644 client/components/trustlyPayments/html/README.md create mode 100644 client/components/trustlyPayments/html/src/app.js create mode 100644 client/components/trustlyPayments/html/src/index.html create mode 100644 client/components/twintPayments/html/README.md create mode 100644 client/components/twintPayments/html/src/app.js create mode 100644 client/components/twintPayments/html/src/index.html create mode 100644 client/components/verkkopankkiPayments/html/README.md create mode 100644 client/components/verkkopankkiPayments/html/src/app.js create mode 100644 client/components/verkkopankkiPayments/html/src/index.html create mode 100644 client/components/wechatpayPayments/html/README.md create mode 100644 client/components/wechatpayPayments/html/src/app.js create mode 100644 client/components/wechatpayPayments/html/src/index.html create mode 100644 client/components/zipPayments/html/README.md create mode 100644 client/components/zipPayments/html/src/app.js create mode 100644 client/components/zipPayments/html/src/index.html diff --git a/client/components/afterpayPayments/html/README.md b/client/components/afterpayPayments/html/README.md new file mode 100644 index 00000000..d4a1bd36 --- /dev/null +++ b/client/components/afterpayPayments/html/README.md @@ -0,0 +1,162 @@ +# Afterpay One-Time Payment Integration + +This example demonstrates how to integrate Afterpay payments using PayPal's v6 Web SDK. Afterpay is a popular buy-now-pay-later payment method that allows customers to split purchases into installments. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Afterpay integration flow: + +1. Initialize PayPal Web SDK with the Afterpay component +2. Check eligibility for Afterpay payment method +3. Create Afterpay payment session with required payment and billing fields +4. Validate customer and billing information before initiating payment +5. Process payment through Afterpay popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Afterpay one-time payment integration +- Full name, email, and billing address field validation +- Popup payment flow +- Eligibility checking for Afterpay +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Afterpay Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Afterpay** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Afterpay demo directory:** + + ```bash + cd client/components/afterpayPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Afterpay components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Afterpay is eligible for the merchant +3. **Session Creation**: Creates Afterpay payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name, email, and billing address fields +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Afterpay not eligible + +- Verify `testBuyerCountry` is set to "US" +- Check that Afterpay is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name, email, and billing address fields are properly mounted +- Check that fields have valid input +- Verify fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/afterpayPayments/html/src/app.js b/client/components/afterpayPayments/html/src/app.js new file mode 100644 index 00000000..4d4e1838 --- /dev/null +++ b/client/components/afterpayPayments/html/src/app.js @@ -0,0 +1,241 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("AFTERPAY"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + reference_id: "default", + amount: { + breakdown: { + item_total: { + currency_code: currencyCode, + value: "24.10" + }, + tax_total: { + currency_code: currencyCode, + value: "2.00" + } + }, + currency_code: currencyCode, + value: "26.10" + }, + invoice_id: "Invoice-12345", + payee: { + merchant_id: "M683SLY6MTM78", + }, + items: [ + { + name: "Shirt", + quantity: "1", + unit_amount: { + currency_code: currencyCode, + value: "12.05" + }, + tax: { + currency_code: currencyCode, + value: "1.00" + } + }, + { + name: "Trouser", + quantity: "1", + unit_amount: { + currency_code: currencyCode, + value: "12.05" + }, + tax: { + currency_code: currencyCode, + value: "1.00" + } + } + ] + } + ] + }; + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +function validateBillingAddress() { + const addressLine1 = document.querySelector("#address-line-1").value.trim(); + const adminArea1 = document.querySelector("#admin-area-1").value.trim(); + const postalCode = document.querySelector("#postal-code").value.trim(); + const countryCode = document.querySelector("#country-code").value.trim(); + const errors = []; + if (!addressLine1) errors.push('Address Line 1'); + if (!adminArea1) errors.push('Admin Area 1'); + if (!postalCode) errors.push('Postal Code'); + if (!countryCode) errors.push('Country Code'); + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(', ')}`; + setMessage(errorMessage); + throw new Error(errorMessage); + } + return { + addressLine1, + adminArea1, + postalCode, + countryCode + }; +} + +function afterpayCheckoutSessionOptionsPromiseFactory(createOrder) { + return async function afterpayCheckoutSessionOptionsPromise(billingData) { + const orderResult = await createOrder(); + const { addressLine1, adminArea1, postalCode, countryCode } = billingData; + return { + orderId: orderResult.orderId, + billingAddress: { + addressLine1, + adminArea1, + postalCode, + countryCode + } + }; + }; +} + +(async () => { + let fullnameValue; + let emailValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const prefillEmailCheckbox = document.querySelector("#prefill-email"); + prefillEmailCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillEmailCheckbox.checked) { + searchParams.append("prefillEmail", "true"); + } else { + searchParams.delete("prefillEmail"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "US", + defaultCurrencyCode: "USD", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["afterpay-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isAfterpayEligible = paymentMethods.isEligible("afterpay"); + + if (isAfterpayEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const afterpayCheckout = sdkInstance.createAfterpayOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const fullnameField = afterpayCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#afterpay-full-name").appendChild(fullnameField); + const emailField = afterpayCheckout.createPaymentFields({ + type: "email", + value: emailValue, + }); + document.querySelector("#afterpay-email").appendChild(emailField); + const afterpayButton = document.querySelector("#afterpay-button"); + afterpayButton.removeAttribute("hidden"); + document.querySelector("#custom-fields").removeAttribute("hidden"); + async function onClick() { + try { + const billingData = validateBillingAddress(); + const valid = await afterpayCheckout.validate(); + if(valid) { + await afterpayCheckout.start( + { presentationMode: "popup" }, + afterpayCheckoutSessionOptionsPromiseFactory(createOrder)(billingData) + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + setMessage(e.message || "Validation failed"); + } + } + afterpayButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/afterpayPayments/html/src/index.html b/client/components/afterpayPayments/html/src/index.html new file mode 100644 index 00000000..4b964e6a --- /dev/null +++ b/client/components/afterpayPayments/html/src/index.html @@ -0,0 +1,84 @@ + + + + + + Afterpay Button Popup Flow + + + +
    +
    +
    +

    Afterpay Button Popup Flow

    +
    +
    + Controls for "Afterpay" payment flow +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +

    Unbranded Afterpay Payment Button

    +

    + + This example uses the afterpay-button web component to launch the popup flow. +

    +
    +
    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/bizumPayments/html/README.md b/client/components/bizumPayments/html/README.md new file mode 100644 index 00000000..804a7dd2 --- /dev/null +++ b/client/components/bizumPayments/html/README.md @@ -0,0 +1,162 @@ +# Bizum One-Time Payment Integration + +This example demonstrates how to integrate Bizum payments using PayPal's v6 Web SDK. Bizum is a popular payment method in Spain that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Bizum integration flow: + +1. Initialize PayPal Web SDK with the Bizum component +2. Check eligibility for Bizum payment method +3. Create Bizum payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Bizum popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Bizum one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for Bizum +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Bizum Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Bizum** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Bizum demo directory:** + + ```bash + cd client/components/bizumPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Bizum components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Bizum is eligible for the merchant +3. **Session Creation**: Creates Bizum payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Bizum not eligible + +- Verify `testBuyerCountry` is set to "ES" +- Check that Bizum is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/bizumPayments/html/src/app.js b/client/components/bizumPayments/html/src/app.js new file mode 100644 index 00000000..1482e7a0 --- /dev/null +++ b/client/components/bizumPayments/html/src/app.js @@ -0,0 +1,143 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage("Transaction Successful"); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "ES", + defaultCurrencyCode: "EUR", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["bizum-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isBizumEligible = paymentMethods.isEligible("bizum"); + if (isBizumEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const bizumCheckout = sdkInstance.createBizumOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullnameField = bizumCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#bizum-full-name").appendChild(fullnameField); + + async function onClick() { + try { + const valid = await bizumCheckout.validate(); + if(valid) { + await bizumCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + + const bizumButton = document.querySelector("#bizum-button"); + bizumButton.removeAttribute("hidden"); + bizumButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/bizumPayments/html/src/index.html b/client/components/bizumPayments/html/src/index.html new file mode 100644 index 00000000..0d432f35 --- /dev/null +++ b/client/components/bizumPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + Bizum Button Popup Flow + + + +
    +
    +
    +

    Bizum Button Popup Flow

    +
    +
    + Controls for "Bizum" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Bizum Payment Button

    +

    + + This example uses the bizum-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/klarnaPayments/html/README.md b/client/components/klarnaPayments/html/README.md new file mode 100644 index 00000000..101b9be6 --- /dev/null +++ b/client/components/klarnaPayments/html/README.md @@ -0,0 +1,163 @@ + +# Klarna One-Time Payment Integration + +This example demonstrates how to integrate Klarna payments using PayPal's v6 Web SDK. Klarna is a popular payment method that allows customers to pay directly from their bank accounts or using flexible payment options. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Klarna integration flow: + +1. Initialize PayPal Web SDK with the Klarna component +2. Check eligibility for Klarna payment method +3. Create Klarna payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Klarna popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Klarna one-time payment integration +- Full name and email field validation +- Popup payment flow +- Eligibility checking for Klarna +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Klarna Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Klarna** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Klarna demo directory:** + + ```bash + cd client/components/klarnaPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Klarna components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Klarna is eligible for the merchant +3. **Session Creation**: Creates Klarna payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Klarna not eligible + +- Verify `testBuyerCountry` is set to a supported country +- Check that Klarna is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name and email fields are properly mounted +- Check that fields have valid input +- Verify fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) \ No newline at end of file diff --git a/client/components/klarnaPayments/html/src/app.js b/client/components/klarnaPayments/html/src/app.js new file mode 100644 index 00000000..a459ad8a --- /dev/null +++ b/client/components/klarnaPayments/html/src/app.js @@ -0,0 +1,257 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "AUTHORIZE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + payee: { merchant_id: "U83XRVL67BW2J" }, + amount: { + currency_code: currencyCode, + value: 30, + breakdown: { + item_total: { currency_code: currencyCode, value: 30 }, + shipping: { currency_code: currencyCode, value: "0.00" }, + handling: { currency_code: currencyCode, value: "0.00" }, + tax_total: { currency_code: currencyCode, value: "0.00" }, + shipping_discount: { currency_code: currencyCode, value: "0.00" } + } + }, + items: [ + { + name: "jersey", + description: "jersey", + sku: "", + unit_amount: { currency_code: currencyCode, value: "15.00" }, + tax: { currency_code: currencyCode, value: "0.00" }, + category: "PHYSICAL_GOODS", + quantity: "1" + }, + { + name: "jersey", + description: "jersey", + sku: "", + unit_amount: { currency_code: currencyCode, value: "15.00" }, + tax: { currency_code: currencyCode, value: "0.00" }, + category: "PHYSICAL_GOODS", + quantity: "1" + } + ] + } + ] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return orderId; + }; +} + +function klarnaCheckoutSessionOptionsPromiseFactory(createOrder) { + return async function klarnaCheckoutSessionOptionsPromise(billingData) { + const orderId = await createOrder(); + const { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode, + phoneCountryCode, + phoneNationalNumber + } = billingData; + return { + orderId, + billingAddress: { + addressLine1, + adminArea2, + adminArea1, + postalCode, + countryCode + }, + phone: { + nationalNumber: phoneNationalNumber, + countryCode: phoneCountryCode, + }, + }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullName; + let email; + + const prefillCheckbox = document.querySelector("#prefill-full-name"); + prefillCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const prefillEmailCheckbox = document.querySelector("#prefill-email"); + prefillEmailCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillEmailCheckbox.checked) { + searchParams.append("prefillEmail", "true"); + } else { + searchParams.delete("prefillEmail"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "GB", + defaultCurrencyCode: "GBP", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["klarna-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isKlarnaEligible = paymentMethods.isEligible("klarna"); + + if (isKlarnaEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const klarnaCheckoutSessionOptionsPromise = klarnaCheckoutSessionOptionsPromiseFactory(createOrder); + const klarnaCheckout = sdkInstance.createKlarnaOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullNameField = klarnaCheckout.createPaymentFields({ + type: "name", + value: fullName, + }); + document.querySelector("#klarna-full-name").appendChild(fullNameField); + + const emailField = klarnaCheckout.createPaymentFields({ + type: "email", + value: email, + }); + document.querySelector("#klarna-email").appendChild(emailField); + + function validateBillingAddress() { + const addressLine1 = document.querySelector("#address-line-1").value.trim(); + const adminArea1 = document.querySelector("#admin-area-1").value.trim(); + const adminArea2 = document.querySelector("#admin-area-2").value.trim(); + const postalCode = document.querySelector("#postal-code").value.trim(); + const countryCode = document.querySelector("#country-code").value.trim(); + const phoneCountryCode = document.querySelector("#phone-country-code").value.trim(); + const phoneNationalNumber = document.querySelector("#phone-national-number").value.trim(); + const errors = []; + if (!addressLine1) errors.push("Address Line 1"); + if (!adminArea1) errors.push("Admin Area 1"); + if (!adminArea2) errors.push("Admin Area 2"); + if (!postalCode) errors.push("Postal Code"); + if (!countryCode) errors.push("Country Code"); + if (!phoneCountryCode) errors.push("Phone Country Code"); + if (!phoneNationalNumber) errors.push("Phone National Number"); + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(", ")}`; + setMessage(errorMessage); + throw new Error(errorMessage); + } + return { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode, + phoneCountryCode, + phoneNationalNumber + }; + } + + async function onClick() { + try { + const billingData = validateBillingAddress(); + const valid = await klarnaCheckout.validate(); + if(valid) { + await klarnaCheckout.start( + { presentationMode: "popup" }, + klarnaCheckoutSessionOptionsPromise(billingData) + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + setMessage(e.message || "Validation failed"); + } + } + + const klarnaButton = document.querySelector("#klarna-button"); + klarnaButton.removeAttribute("hidden"); + klarnaButton.addEventListener("click", onClick); + document.querySelector("#custom-fields").removeAttribute("hidden"); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/klarnaPayments/html/src/index.html b/client/components/klarnaPayments/html/src/index.html new file mode 100644 index 00000000..f54b34b3 --- /dev/null +++ b/client/components/klarnaPayments/html/src/index.html @@ -0,0 +1,97 @@ + + + + + + Klarna Button Popup Flow + + + +
    +
    +
    +

    Klarna Button Popup Flow

    +
    +
    + Controls for "Klarna" payment flow +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Klarna Payment Button

    +

    + + This example uses the klarna-button web component to launch the popup flow. +

    +
    +
    + +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/multibancoPayments/html/README.md b/client/components/multibancoPayments/html/README.md new file mode 100644 index 00000000..6a743907 --- /dev/null +++ b/client/components/multibancoPayments/html/README.md @@ -0,0 +1,162 @@ +# Multibanco One-Time Payment Integration + +This example demonstrates how to integrate Multibanco payments using PayPal's v6 Web SDK. Multibanco is a popular payment method in Portugal that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Multibanco integration flow: + +1. Initialize PayPal Web SDK with the Multibanco component +2. Check eligibility for Multibanco payment method +3. Create Multibanco payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Multibanco popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Multibanco one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for Multibanco +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Multibanco Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Multibanco** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Multibanco demo directory:** + + ```bash + cd client/components/multibancoPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Multibanco components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Multibanco is eligible for the merchant +3. **Session Creation**: Creates Multibanco payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Multibanco not eligible + +- Verify `testBuyerCountry` is set to "PT" +- Check that Multibanco is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/multibancoPayments/html/src/app.js b/client/components/multibancoPayments/html/src/app.js new file mode 100644 index 00000000..2748a84b --- /dev/null +++ b/client/components/multibancoPayments/html/src/app.js @@ -0,0 +1,140 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "PT", + defaultCurrencyCode: "EUR", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["multibanco-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isMultibancoEligible = paymentMethods.isEligible("multibanco"); + + if (isMultibancoEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const multibancoCheckout = sdkInstance.createMultibancoOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const fullnameField = multibancoCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#multibanco-full-name").appendChild(fullnameField); + async function onClick() { + try { + const valid = await multibancoCheckout.validate(); + if(valid) { + await multibancoCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + const multiBancoButton = document.querySelector("#multibanco-button"); + multiBancoButton.removeAttribute("hidden"); + multiBancoButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/multibancoPayments/html/src/index.html b/client/components/multibancoPayments/html/src/index.html new file mode 100644 index 00000000..77c6fbcc --- /dev/null +++ b/client/components/multibancoPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + multibanco Button Popup Flow + + + +
    +
    +
    +

    multibanco Button Popup Flow

    +
    +
    + Controls for "multibanco" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded multibanco Payment Button

    +

    + + This example uses the multibanco-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/mybankPayments/html/README.md b/client/components/mybankPayments/html/README.md new file mode 100644 index 00000000..baf34909 --- /dev/null +++ b/client/components/mybankPayments/html/README.md @@ -0,0 +1,162 @@ +# myBank One-Time Payment Integration + +This example demonstrates how to integrate myBank payments using PayPal's v6 Web SDK. myBank is a popular payment method in Italy that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete myBank integration flow: + +1. Initialize PayPal Web SDK with the myBank component +2. Check eligibility for myBank payment method +3. Create myBank payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through myBank popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- myBank one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for myBank +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable myBank Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **myBank** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the myBank demo directory:** + + ```bash + cd client/components/mybankPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with myBank components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies myBank is eligible for the merchant +3. **Session Creation**: Creates myBank payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### myBank not eligible + +- Verify `testBuyerCountry` is set to "IT" +- Check that myBank is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/mybankPayments/html/src/app.js b/client/components/mybankPayments/html/src/app.js new file mode 100644 index 00000000..7eba1df1 --- /dev/null +++ b/client/components/mybankPayments/html/src/app.js @@ -0,0 +1,141 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "IT", + defaultCurrencyCode: "EUR", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["mybank-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isMyBankEligible = paymentMethods.isEligible("mybank"); + + if (isMyBankEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const mybankCheckout = sdkInstance.createMyBankOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const fullnameField = mybankCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#mybank-full-name").appendChild(fullnameField); + async function onClick() { + try { + const valid = await mybankCheckout.validate(); + if(valid) { + await mybankCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + const myBankButton = document.querySelector("#mybank-button"); + myBankButton.removeAttribute("hidden"); + myBankButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/mybankPayments/html/src/index.html b/client/components/mybankPayments/html/src/index.html new file mode 100644 index 00000000..978efddd --- /dev/null +++ b/client/components/mybankPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + myBank Button Popup Flow + + + +
    +
    +
    +

    myBank Button Popup Flow

    +
    +
    + Controls for "myBank" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded myBank Payment Button

    +

    + + This example uses the mybank-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/payuPayments/html/README.md b/client/components/payuPayments/html/README.md new file mode 100644 index 00000000..2eaa49b3 --- /dev/null +++ b/client/components/payuPayments/html/README.md @@ -0,0 +1,162 @@ +# PayU One-Time Payment Integration + +This example demonstrates how to integrate PayU payments using PayPal's v6 Web SDK. PayU is a popular payment method in Central and Eastern Europe that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete PayU integration flow: + +1. Initialize PayPal Web SDK with the PayU component +2. Check eligibility for PayU payment method +3. Create PayU payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through PayU popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- PayU one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for PayU +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable PayU Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **PayU** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the PayU demo directory:** + + ```bash + cd client/components/payuPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with PayU components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies PayU is eligible for the merchant +3. **Session Creation**: Creates PayU payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### PayU not eligible + +- Verify `testBuyerCountry` is set to "PL" or a supported country +- Check that PayU is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/payuPayments/html/src/app.js b/client/components/payuPayments/html/src/app.js new file mode 100644 index 00000000..e94a27d4 --- /dev/null +++ b/client/components/payuPayments/html/src/app.js @@ -0,0 +1,144 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "PL", + defaultCurrencyCode: "PLN", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["payu-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isPayuEligible = paymentMethods.isEligible("payu"); + + if (isPayuEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const payuCheckout = sdkInstance.createPayuOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullnameField = payuCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#payu-full-name").appendChild(fullnameField); + + async function onClick() { + try { + const valid = await payuCheckout.validate(); + if(valid) { + await payuCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + + const payuButton = document.querySelector("#payu-button"); + payuButton.removeAttribute("hidden"); + payuButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/payuPayments/html/src/index.html b/client/components/payuPayments/html/src/index.html new file mode 100644 index 00000000..1b9abb2c --- /dev/null +++ b/client/components/payuPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + PayU Button Popup Flow + + + +
    +
    +
    +

    PayU Button Popup Flow

    +
    +
    + Controls for "PayU" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded PayU Payment Button

    +

    + + This example uses the payu-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/swishPayments/html/README.md b/client/components/swishPayments/html/README.md new file mode 100644 index 00000000..556fe117 --- /dev/null +++ b/client/components/swishPayments/html/README.md @@ -0,0 +1,162 @@ +# Swish One-Time Payment Integration + +This example demonstrates how to integrate Swish payments using PayPal's v6 Web SDK. Swish is a popular payment method in Sweden that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Swish integration flow: + +1. Initialize PayPal Web SDK with the Swish component +2. Check eligibility for Swish payment method +3. Create Swish payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Swish popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Swish one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for Swish +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Swish Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Swish** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Swish demo directory:** + + ```bash + cd client/components/swishPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Swish components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Swish is eligible for the merchant +3. **Session Creation**: Creates Swish payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Swish not eligible + +- Verify `testBuyerCountry` is set to "SE" +- Check that Swish is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/swishPayments/html/src/app.js b/client/components/swishPayments/html/src/app.js new file mode 100644 index 00000000..247de5d9 --- /dev/null +++ b/client/components/swishPayments/html/src/app.js @@ -0,0 +1,169 @@ +import { + createOrder as createOrderHelper, + captureOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode, countryCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + payer: { + name: { + given_name: "Carlos", + surname: "Abejundio" + } + }, + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + reference_id: "#0000", + description: "Items bought at Hemm Store", + custom_id: "#1111", + soft_descriptor: "ABC", + amount: { + currency_code: currencyCode, + value: "1000", + breakdown: { + item_total: { + currency_code: currencyCode, + value: "980" + }, + tax_total: { + currency_code: currencyCode, + value: "20" + } + } + }, + shipping: { + address: { + address_line_1: "595343 / 86693 West Elm", + admin_area_2: "Stockholm", + postal_code: "SE-113 49", + country_code: countryCode + } + } + } + ] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Capturing order ${data.orderId}...`); + const orderData = await captureOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Capture result", orderData); + setMessage(JSON.stringify(orderData, null, 2)); +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullName; + + const prefillCheckbox = document.querySelector("#prefill-full-name"); + prefillCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "SE", + defaultCurrencyCode: "SEK", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["swish-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isSwishEligible = paymentMethods.isEligible("swish"); + + if (isSwishEligible) { + const createOrder = buildCreateOrderFunction(currencyCode, countryCode); + const swishCheckout = sdkInstance.createSwishOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullNameField = swishCheckout.createPaymentFields({ + type: "name", + value: fullName, + }); + document.querySelector("#swish-full-name").appendChild(fullNameField); + + async function onClick() { + try { + const valid = await swishCheckout.validate(); + if (valid) { + await swishCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + + const swishButton = document.querySelector("#swish-button"); + swishButton.removeAttribute("hidden"); + swishButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/swishPayments/html/src/index.html b/client/components/swishPayments/html/src/index.html new file mode 100644 index 00000000..ee5067f7 --- /dev/null +++ b/client/components/swishPayments/html/src/index.html @@ -0,0 +1,62 @@ + + + + + + Swish Button Popup Flow + + + +
    +
    +
    +

    Swish Button Popup Flow

    +
    +
    + Controls for "Swish" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Swish Payment Button

    +

    + + This example uses the swish-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/trustlyPayments/html/README.md b/client/components/trustlyPayments/html/README.md new file mode 100644 index 00000000..d63bd013 --- /dev/null +++ b/client/components/trustlyPayments/html/README.md @@ -0,0 +1,162 @@ +# Trustly One-Time Payment Integration + +This example demonstrates how to integrate Trustly payments using PayPal's v6 Web SDK. Trustly is a popular payment method in Europe that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Trustly integration flow: + +1. Initialize PayPal Web SDK with the Trustly component +2. Check eligibility for Trustly payment method +3. Create Trustly payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Trustly popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Trustly one-time payment integration +- Full name and email field validation +- Popup payment flow +- Eligibility checking for Trustly +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Trustly Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Trustly** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Trustly demo directory:** + + ```bash + cd client/components/trustlyPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Trustly components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Trustly is eligible for the merchant +3. **Session Creation**: Creates Trustly payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Trustly not eligible + +- Verify `testBuyerCountry` is set to "SE" or a supported country +- Check that Trustly is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name and email fields are properly mounted +- Check that fields have valid input +- Verify fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/trustlyPayments/html/src/app.js b/client/components/trustlyPayments/html/src/app.js new file mode 100644 index 00000000..a4f978ac --- /dev/null +++ b/client/components/trustlyPayments/html/src/app.js @@ -0,0 +1,158 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + let emailValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const prefillEmailCheckbox = document.querySelector("#prefill-email"); + prefillEmailCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillEmailCheckbox.checked) { + searchParams.append("prefillEmail", "true"); + } else { + searchParams.delete("prefillEmail"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "SE", + defaultCurrencyCode: "SEK", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["trustly-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isTrustlyEligible = paymentMethods.isEligible("trustly"); + + if (isTrustlyEligible) { + const trustlyCheckout = sdkInstance.createTrustlyOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const createOrder = buildCreateOrderFunction(currencyCode); + const fullnameField = trustlyCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#trustly-full-name").appendChild(fullnameField); + const emailField = trustlyCheckout.createPaymentFields({ + type: "email", + value: emailValue, + }); + document.querySelector("#trustly-email").appendChild(emailField); + async function onClick() { + try { + const valid = await trustlyCheckout.validate(); + if(valid) { + await trustlyCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + const trustlyButton = document.querySelector("#trustly-button"); + trustlyButton.removeAttribute("hidden"); + trustlyButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/trustlyPayments/html/src/index.html b/client/components/trustlyPayments/html/src/index.html new file mode 100644 index 00000000..edb6e1b1 --- /dev/null +++ b/client/components/trustlyPayments/html/src/index.html @@ -0,0 +1,65 @@ + + + + + + Trustly Button Popup Flow + + + +
    +
    +
    +

    Trustly Button Popup Flow

    +
    +
    + Controls for "Trustly" payment flow +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Trustly Payment Button

    +

    + + This example uses the trustly-button web component to launch the popup flow. +

    +
    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/twintPayments/html/README.md b/client/components/twintPayments/html/README.md new file mode 100644 index 00000000..3a61852d --- /dev/null +++ b/client/components/twintPayments/html/README.md @@ -0,0 +1,162 @@ +# Twint One-Time Payment Integration + +This example demonstrates how to integrate Twint payments using PayPal's v6 Web SDK. Twint is a popular payment method in Switzerland that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Twint integration flow: + +1. Initialize PayPal Web SDK with the Twint component +2. Check eligibility for Twint payment method +3. Create Twint payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Twint popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Twint one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for Twint +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Twint Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Twint** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Twint demo directory:** + + ```bash + cd client/components/twintPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Twint components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Twint is eligible for the merchant +3. **Session Creation**: Creates Twint payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Twint not eligible + +- Verify `testBuyerCountry` is set to "CH" +- Check that Twint is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/twintPayments/html/src/app.js b/client/components/twintPayments/html/src/app.js new file mode 100644 index 00000000..9f36f0f1 --- /dev/null +++ b/client/components/twintPayments/html/src/app.js @@ -0,0 +1,144 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullName; + + const prefillCheckbox = document.querySelector("#prefill-full-name"); + prefillCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "CH", + defaultCurrencyCode: "CHF", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["twint-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isTwintEligible = paymentMethods.isEligible("twint"); + + if (isTwintEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const twintCheckout = sdkInstance.createTwintOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullNameField = twintCheckout.createPaymentFields({ + type: "name", + value: fullName, + }); + document.querySelector("#twint-full-name").appendChild(fullNameField); + + async function onClick() { + try { + const valid = await twintCheckout.validate(); + if(valid) { + await twintCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + + const twintButton = document.querySelector("#twint-button"); + twintButton.removeAttribute("hidden"); + twintButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/twintPayments/html/src/index.html b/client/components/twintPayments/html/src/index.html new file mode 100644 index 00000000..553e743c --- /dev/null +++ b/client/components/twintPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + Twint Button Popup Flow + + + +
    +
    +
    +

    Twint Button Popup Flow

    +
    +
    + Controls for "Twint" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Twint Payment Button

    +

    + + This example uses the twint-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/verkkopankkiPayments/html/README.md b/client/components/verkkopankkiPayments/html/README.md new file mode 100644 index 00000000..bdc58bd1 --- /dev/null +++ b/client/components/verkkopankkiPayments/html/README.md @@ -0,0 +1,162 @@ +# Verkkopankki One-Time Payment Integration + +This example demonstrates how to integrate Verkkopankki payments using PayPal's v6 Web SDK. Verkkopankki is a popular payment method in Finland that allows customers to pay directly from their bank accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Verkkopankki integration flow: + +1. Initialize PayPal Web SDK with the Verkkopankki component +2. Check eligibility for Verkkopankki payment method +3. Create Verkkopankki payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through Verkkopankki popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Verkkopankki one-time payment integration +- Full name and email field validation +- Popup payment flow +- Eligibility checking for Verkkopankki +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Verkkopankki Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Verkkopankki** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Verkkopankki demo directory:** + + ```bash + cd client/components/verkkopankkiPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Verkkopankki components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Verkkopankki is eligible for the merchant +3. **Session Creation**: Creates Verkkopankki payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Verkkopankki not eligible + +- Verify `testBuyerCountry` is set to "FI" +- Check that Verkkopankki is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name and email fields are properly mounted +- Check that fields have valid input +- Verify fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/verkkopankkiPayments/html/src/app.js b/client/components/verkkopankkiPayments/html/src/app.js new file mode 100644 index 00000000..230869ae --- /dev/null +++ b/client/components/verkkopankkiPayments/html/src/app.js @@ -0,0 +1,158 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + let emailValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const prefillEmailCheckbox = document.querySelector("#prefill-email"); + prefillEmailCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillEmailCheckbox.checked) { + searchParams.append("prefillEmail", "true"); + } else { + searchParams.delete("prefillEmail"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "FI", + defaultCurrencyCode: "EUR", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["verkkopankki-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isVerkkopankkiEligible = paymentMethods.isEligible("verkkopankki"); + + if (isVerkkopankkiEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const verkkopankkiCheckout = sdkInstance.createVerkkopankkiOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const fullnameField = verkkopankkiCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#verkkopankki-full-name").appendChild(fullnameField); + const emailField = verkkopankkiCheckout.createPaymentFields({ + type: "email", + value: emailValue, + }); + document.querySelector("#verkkopankki-email").appendChild(emailField); + async function onClick() { + try { + const valid = await verkkopankkiCheckout.validate(); + if(valid) { + await verkkopankkiCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + const verkkopankkiButton = document.querySelector("#verkkopankki-button"); + verkkopankkiButton.removeAttribute("hidden"); + verkkopankkiButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/verkkopankkiPayments/html/src/index.html b/client/components/verkkopankkiPayments/html/src/index.html new file mode 100644 index 00000000..544d805e --- /dev/null +++ b/client/components/verkkopankkiPayments/html/src/index.html @@ -0,0 +1,65 @@ + + + + + + Verkkopankki Button Popup Flow + + + +
    +
    +
    +

    Verkkopankki Button Popup Flow

    +
    +
    + Controls for "Verkkopankki" payment flow +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Verkkopankki Payment Button

    +

    + + This example uses the verkkopankki-button web component to launch the popup flow. +

    +
    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/wechatpayPayments/html/README.md b/client/components/wechatpayPayments/html/README.md new file mode 100644 index 00000000..f18a85bd --- /dev/null +++ b/client/components/wechatpayPayments/html/README.md @@ -0,0 +1,162 @@ +# WeChat Pay One-Time Payment Integration + +This example demonstrates how to integrate WeChat Pay payments using PayPal's v6 Web SDK. WeChat Pay is a popular payment method in China that allows customers to pay directly from their WeChat accounts. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete WeChat Pay integration flow: + +1. Initialize PayPal Web SDK with the WeChat Pay component +2. Check eligibility for WeChat Pay payment method +3. Create WeChat Pay payment session with required payment fields +4. Validate customer information before initiating payment +5. Process payment through WeChat Pay popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- WeChat Pay one-time payment integration +- Full name field validation +- Popup payment flow +- Eligibility checking for WeChat Pay +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable WeChat Pay Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **WeChat Pay** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the WeChat Pay demo directory:** + + ```bash + cd client/components/wechatpayPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with WeChat Pay components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies WeChat Pay is eligible for the merchant +3. **Session Creation**: Creates WeChat Pay payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name field +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### WeChat Pay not eligible + +- Verify `testBuyerCountry` is set to "CN" +- Check that WeChat Pay is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name field is properly mounted +- Check that field has valid input +- Verify field is visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/wechatpayPayments/html/src/app.js b/client/components/wechatpayPayments/html/src/app.js new file mode 100644 index 00000000..223085e4 --- /dev/null +++ b/client/components/wechatpayPayments/html/src/app.js @@ -0,0 +1,143 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("LPM"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function buildCreateOrderFunction(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + purchase_units: [{ + amount: { + currency_code: currencyCode, + value: "10.00" + } + }] + }; + + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +(async () => { + let fullnameValue; + + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "CN", + defaultCurrencyCode: "CNY", + }); + + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["wechatpay-payments"], + testBuyerCountry: countryCode, + }); + + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + + const isWechatpayEligible = paymentMethods.isEligible("wechatpay"); + + if (isWechatpayEligible) { + const createOrder = buildCreateOrderFunction(currencyCode); + const wechatpayCheckout = sdkInstance.createWechatpayOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + + const fullnameField = wechatpayCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#wechatpay-full-name").appendChild(fullnameField); + + async function onClick() { + try { + const valid = await wechatpayCheckout.validate(); + if(valid) { + await wechatpayCheckout.start( + { presentationMode: "popup" }, + createOrder() + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + } + } + + const wechatpayButton = document.querySelector("#wechatpay-button"); + wechatpayButton.removeAttribute("hidden"); + wechatpayButton.addEventListener("click", onClick); + } + + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/wechatpayPayments/html/src/index.html b/client/components/wechatpayPayments/html/src/index.html new file mode 100644 index 00000000..c173357e --- /dev/null +++ b/client/components/wechatpayPayments/html/src/index.html @@ -0,0 +1,60 @@ + + + + + + WeChat Pay Button Popup Flow + + + +
    +
    +
    +

    WeChat Pay Button Popup Flow

    +
    +
    + Controls for "WeChat Pay" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded WeChat Pay Payment Button

    +

    + + This example uses the wechatpay-button web component to launch the popup flow. +

    +
    + +
    +
    + +
    +
    + + + + diff --git a/client/components/zipPayments/html/README.md b/client/components/zipPayments/html/README.md new file mode 100644 index 00000000..fd37c65f --- /dev/null +++ b/client/components/zipPayments/html/README.md @@ -0,0 +1,162 @@ +# Zip One-Time Payment Integration + +This example demonstrates how to integrate Zip payments using PayPal's v6 Web SDK. Zip is a buy-now-pay-later payment method that allows customers to split purchases into installments. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Zip integration flow: + +1. Initialize PayPal Web SDK with the Zip component +2. Check eligibility for Zip payment method +3. Create Zip payment session with required payment, billing, and phone fields +4. Validate customer and billing information before initiating payment +5. Process payment through Zip popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Zip one-time payment integration +- Full name, billing address, and phone field validation +- Popup payment flow +- Eligibility checking for Zip +- Error handling and user feedback + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Zip Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Zip** in the payment methods and enable it + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Zip demo directory:** + + ```bash + cd client/components/zipPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Zip components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Zip is eligible for the merchant +3. **Session Creation**: Creates Zip payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name, billing address, and phone fields +5. **Validation**: Validates fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers provide billing details and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Zip not eligible + +- Verify `testBuyerCountry` is set to "US" +- Check that Zip is enabled for the merchant in PayPal account settings + +### Validation fails + +- Ensure full name, billing address, and phone fields are properly mounted +- Check that fields have valid input +- Verify fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/zipPayments/html/src/app.js b/client/components/zipPayments/html/src/app.js new file mode 100644 index 00000000..909582bf --- /dev/null +++ b/client/components/zipPayments/html/src/app.js @@ -0,0 +1,237 @@ +import { + createOrder as createOrderHelper, + getOrder, + initAuth, +} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; +import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; + +function setMessage(message) { + document.querySelector("#message").textContent = message; + console.log(message); +} + +setMessage("Fetching auth..."); +const { auth, clientName } = await initAuth("ZIP"); +setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); + +function createOrderFactory(currencyCode) { + return async function createOrder() { + setMessage("Creating order..."); + const orderPayload = { + intent: "CAPTURE", + processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + purchase_units: [ + { + amount: { + breakdown: { + item_total: { + currency_code: currencyCode, + value: "34.10" + }, + tax_total: { + currency_code: currencyCode, + value: "2.00" + } + }, + currency_code: currencyCode, + value: "36.10" + }, + payee: { + merchant_id: "M683SLY6MTM78" + }, + items: [ + { + name: "Shirt", + quantity: "1", + unit_amount: { + currency_code: currencyCode, + value: "12.05" + }, + tax: { + currency_code: currencyCode, + value: "1.00" + } + }, + { + name: "Trouser", + quantity: "1", + unit_amount: { + currency_code: currencyCode, + value: "22.05" + }, + tax: { + currency_code: currencyCode, + value: "1.00" + } + } + ] + } + ] + }; + const { id: orderId } = await createOrderHelper({ + headers: { "X-CSRF-TOKEN": "" }, + body: orderPayload, + clientName, + }); + setMessage(`Order ${orderId}`); + return { orderId }; + }; +} + +async function onApprove(data) { + console.log("onApprove", data); + setMessage(`Fetching order details for ${data.orderId}...`); + try { + const orderDetails = await getOrder({ + orderId: data.orderId, + headers: { "X-CSRF-TOKEN": "" }, + clientName, + }); + console.log("Order details", orderDetails); + setMessage(JSON.stringify(orderDetails, null, 2)); + } catch (error) { + console.error("Error fetching order details:", error); + setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + } +} + +function onCancel(data) { + console.log("onCancel", data); + let message = "Canceled order"; + if (data) { + message += ` ${data.orderId}`; + } + setMessage(message); +} + +function onError(data) { + console.log("onError", data); + setMessage(data); +} + +function validateBillingAddress() { + const addressLine1 = document.querySelector("#address-line-1").value.trim(); + const adminArea1 = document.querySelector("#admin-area-1").value.trim(); + const adminArea2 = document.querySelector("#admin-area-2").value.trim(); + const postalCode = document.querySelector("#postal-code").value.trim(); + const countryCode = document.querySelector("#country-code").value.trim(); + const phoneCountryCode = document.querySelector("#phone-country-code").value.trim(); + const phoneNationalNumber = document.querySelector("#phone-national-number").value.trim(); + const errors = []; + if (!addressLine1) errors.push('Address Line 1'); + if (!adminArea1) errors.push('Admin Area 1'); + if (!adminArea2) errors.push('Admin Area 2'); + if (!postalCode) errors.push('Postal Code'); + if (!countryCode) errors.push('Country Code'); + if (!phoneCountryCode) errors.push('Phone Country Code'); + if (!phoneNationalNumber) errors.push('Phone National Number'); + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(', ')}`; + setMessage(errorMessage); + throw new Error(errorMessage); + } + return { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode, + phoneCountryCode, + phoneNationalNumber + }; +} + +function zipCheckoutSessionOptionsPromiseFactory(createOrder) { + return async function zipCheckoutSessionOptionsPromise(billingData) { + const orderResult = await createOrder(); + const { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode, + phoneCountryCode, + phoneNationalNumber + } = billingData; + return { + orderId: orderResult.orderId, + billingAddress: { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode + }, + phone: { + countryCode: phoneCountryCode, + nationalNumber: phoneNationalNumber, + }, + }; + }; +} + +(async () => { + let fullnameValue; + const prefillNameCheckbox = document.querySelector("#prefill-full-name"); + prefillNameCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillNameCheckbox.checked) { + searchParams.append("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.href = window.location.pathname + "?" + searchParams.toString(); + }); + const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ + defaultCountryCode: "US", + defaultCurrencyCode: "USD", + }); + const sdkInstance = await window.paypal.createInstance({ + ...auth, + components: ["zip-payments"], + testBuyerCountry: countryCode, + }); + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: currencyCode, + }); + const isZipEligible = paymentMethods.isEligible("zip"); + if (isZipEligible) { + const createOrder = createOrderFactory(currencyCode); + const zipCheckout = sdkInstance.createZipOneTimePaymentSession({ + onApprove, + onCancel, + onError + }); + const fullnameField = zipCheckout.createPaymentFields({ + type: "name", + value: fullnameValue, + }); + document.querySelector("#zip-full-name").appendChild(fullnameField); + const zipButton = document.querySelector("#zip-button"); + zipButton.removeAttribute("hidden"); + document.querySelector("#custom-fields").removeAttribute("hidden"); + async function onClick() { + try { + const billingData = validateBillingAddress(); + const valid = await zipCheckout.validate(); + if(valid) { + await zipCheckout.start( + { presentationMode: "popup" }, + zipCheckoutSessionOptionsPromiseFactory(createOrder)(billingData) + ); + } else { + setMessage("validation failed"); + } + } catch (e) { + console.error(e); + setMessage(e.message || "Validation failed"); + } + } + zipButton.addEventListener("click", onClick); + } + document.querySelector("#update-locale").addEventListener("change", async (event) => { + const newLocale = event.target.value; + sdkInstance.updateLocale(newLocale); + setMessage(`Locale updated to ${newLocale}`); + }); +})(); diff --git a/client/components/zipPayments/html/src/index.html b/client/components/zipPayments/html/src/index.html new file mode 100644 index 00000000..c52f72a4 --- /dev/null +++ b/client/components/zipPayments/html/src/index.html @@ -0,0 +1,97 @@ + + + + + + Zip Button Popup Flow + + + + +
    +
    +
    +

    Zip Button Popup Flow

    +
    +
    + Controls for "Zip" payment flow +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Unbranded Zip Payment Button

    +

    + + This example uses the zip-button web component to launch the popup flow. +

    +
    + +
    + +
    +
    + +
    +
    + + + + From fbdd5f5916322ddc76d97e5a40c77c9f400aff39 Mon Sep 17 00:00:00 2001 From: "Reegan Gunasekaran (CW)" Date: Thu, 19 Feb 2026 20:09:15 +0530 Subject: [PATCH 4/7] adding apms and fixing issues --- .../components/payuPayments/html/src/app.js | 309 ++++++++++++------ .../payuPayments/html/src/index.html | 120 +++---- 2 files changed, 267 insertions(+), 162 deletions(-) diff --git a/client/components/payuPayments/html/src/app.js b/client/components/payuPayments/html/src/app.js index e94a27d4..6823f83d 100644 --- a/client/components/payuPayments/html/src/app.js +++ b/client/components/payuPayments/html/src/app.js @@ -1,144 +1,243 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } } -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); +// Get order details after approval +async function getOrder(orderId) { + try { + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); +// Create PayPal order +async function createOrder() { + try { const orderPayload = { intent: "CAPTURE", processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] + purchase_units: [ + { + amount: { + currency_code: "PLN", + value: "10.00", + }, + }, + ], }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, - }); - - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(orderPayload), + }, + ); + if (!response.ok) { + throw new Error("Failed to create order"); + } + const { id } = await response.json(); + return { orderId: id }; + } catch (error) { + showMessage({ text: "Failed to create order. Please try again.", type: "error" }); + throw error; + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +// Handle successful payment approval +async function handleApprove(data) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + const orderDetails = await getOrder(data.orderId); + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + console.log("Order details:", orderDetails); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + console.error("Failed to fetch order details:", error); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); +// Handle payment cancellation +function handleCancel(data) { + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); + console.log("Payment cancelled:", data); } -function onError(data) { - console.log("onError", data); - setMessage(data); +// Handle payment errors +function handleError(error) { + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); + console.error("Payment error:", error); } -(async () => { - let fullnameValue; - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); - } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); +// Helper to get query param +function getQueryParam(name) { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get(name); +} - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "PL", - defaultCurrencyCode: "PLN", +// Modular payment field setup with prefill +function setupPaymentFields(payuCheckout, prefillValue) { + const fullNameField = payuCheckout.createPaymentFields({ + type: "name", + value: prefillValue || "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, }); + document.querySelector("#payu-full-name").appendChild(fullNameField); +} - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["payu-payments"], - testBuyerCountry: countryCode, - }); +// Main PayU integration logic +window.onPayPalWebSdkLoaded = async function onPayPalWebSdkLoaded() { + try { + // Prefill logic + const prefillChecked = getQueryParam("prefillName") === "true"; + const prefillValue = prefillChecked ? "John Doe" : ""; + + // Set checkbox state and event + const prefillCheckbox = document.getElementById("prefill-full-name"); + if (prefillCheckbox) { + prefillCheckbox.checked = prefillChecked; + prefillCheckbox.addEventListener("change", () => { + const searchParams = new URLSearchParams(window.location.search); + if (prefillCheckbox.checked) { + searchParams.set("prefillName", "true"); + } else { + searchParams.delete("prefillName"); + } + window.location.search = searchParams.toString(); + }); + } - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "PL", + components: ["payu-payments"], + }); - const isPayuEligible = paymentMethods.isEligible("payu"); + // Check if PayU is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "PLN", + }); + const isPayuEligible = paymentMethods.isEligible("payu"); - if (isPayuEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const payuCheckout = sdkInstance.createPayuOneTimePaymentSession({ - onApprove, - onCancel, - onError + if (isPayuEligible) { + setupPayuPayment(sdkInstance, prefillValue); + } else { + showMessage({ + text: "PayU is not eligible. Please ensure your configuration is correct.", + type: "error", + }); + console.error("PayU is not eligible"); + } + } catch (error) { + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", }); + console.error("Error initializing PayPal SDK:", error); + } +} - const fullnameField = payuCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, +function setupPayuPayment(sdkInstance, prefillValue) { + try { + // Create PayU checkout session + const payuCheckout = sdkInstance.createPayuOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - document.querySelector("#payu-full-name").appendChild(fullnameField); - async function onClick() { + // Modular payment field setup + setupPaymentFields(payuCheckout, prefillValue); + + // Setup button click handler + const payuButton = document.querySelector("#payu-button"); + payuButton.removeAttribute("hidden"); + payuButton.addEventListener("click", async () => { try { - const valid = await payuCheckout.validate(); - if(valid) { + // Validate the payment field + const isValid = await payuCheckout.validate(); + if (isValid) { + // Start payment flow with popup mode await payuCheckout.start( { presentationMode: "popup" }, createOrder() ); } else { - setMessage("validation failed"); + showMessage({ + text: "Please fill in the required field correctly.", + type: "error", + }); } - } catch (e) { - console.error(e); + } catch (error) { + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + console.error("Payment error:", error); } - } - - const payuButton = document.querySelector("#payu-button"); - payuButton.removeAttribute("hidden"); - payuButton.addEventListener("click", onClick); + }); + } catch (error) { + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + console.error("Error setting up PayU payment:", error); } - - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +} diff --git a/client/components/payuPayments/html/src/index.html b/client/components/payuPayments/html/src/index.html index 1b9abb2c..7eda51fe 100644 --- a/client/components/payuPayments/html/src/index.html +++ b/client/components/payuPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - PayU Button Popup Flow - - - -
    -
    -
    -

    PayU Button Popup Flow

    -
    -
    - Controls for "PayU" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded PayU Payment Button

    -

    - - This example uses the payu-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + PayU - PayPal Web SDK + + + + + +

    PayU One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + From f00a385171ac14727ce17319c18d1b864f624ebb Mon Sep 17 00:00:00 2001 From: Reegan1819 Date: Thu, 26 Feb 2026 12:20:47 +0530 Subject: [PATCH 5/7] fixing the issues --- .../afterpayPayments/html/src/app.js | 437 +++++++-------- .../afterpayPayments/html/src/index.html | 146 +++-- .../components/bizumPayments/html/src/app.js | 321 +++++++---- .../bizumPayments/html/src/index.html | 120 +++-- .../components/klarnaPayments/html/src/app.js | 497 ++++++++++-------- .../klarnaPayments/html/src/index.html | 274 ++++++---- .../multibancoPayments/html/src/app.js | 332 ++++++++---- .../multibancoPayments/html/src/index.html | 120 +++-- .../components/mybankPayments/html/src/app.js | 333 ++++++++---- .../mybankPayments/html/src/index.html | 120 +++-- .../components/swishPayments/html/src/app.js | 351 ++++++++----- .../swishPayments/html/src/index.html | 122 ++--- .../trustlyPayments/html/src/app.js | 349 +++++++----- .../trustlyPayments/html/src/index.html | 127 ++--- .../components/twintPayments/html/src/app.js | 322 ++++++++---- .../twintPayments/html/src/index.html | 120 +++-- .../verkkopankkiPayments/html/src/app.js | 360 ++++++++----- .../verkkopankkiPayments/html/src/index.html | 127 ++--- .../wechatpayPayments/html/src/app.js | 321 +++++++---- .../wechatpayPayments/html/src/index.html | 120 +++-- client/components/zipPayments/html/src/app.js | 433 +++++++-------- .../zipPayments/html/src/index.html | 157 +++--- 22 files changed, 3263 insertions(+), 2346 deletions(-) diff --git a/client/components/afterpayPayments/html/src/app.js b/client/components/afterpayPayments/html/src/app.js index 4d4e1838..07aafd45 100644 --- a/client/components/afterpayPayments/html/src/app.js +++ b/client/components/afterpayPayments/html/src/app.js @@ -1,241 +1,262 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "US", // United States for Afterpay testing + components: ["afterpay-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("AFTERPAY"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [ - { - reference_id: "default", - amount: { - breakdown: { - item_total: { - currency_code: currencyCode, - value: "24.10" - }, - tax_total: { - currency_code: currencyCode, - value: "2.00" - } - }, - currency_code: currencyCode, - value: "26.10" - }, - invoice_id: "Invoice-12345", - payee: { - merchant_id: "M683SLY6MTM78", - }, - items: [ - { - name: "Shirt", - quantity: "1", - unit_amount: { - currency_code: currencyCode, - value: "12.05" - }, - tax: { - currency_code: currencyCode, - value: "1.00" - } - }, - { - name: "Trouser", - quantity: "1", - unit_amount: { - currency_code: currencyCode, - value: "12.05" - }, - tax: { - currency_code: currencyCode, - value: "1.00" - } - } - ] - } - ] - }; - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Afterpay is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "USD", + }); + + const isAfterpayEligible = paymentMethods.isEligible("afterpay"); + + if (isAfterpayEligible) { + setupAfterpayPayment(sdkInstance); + } else { + showMessage({ + text: "Afterpay is not eligible. Please ensure your buyer country is United States and currency is USD.", + type: "error", + }); + console.error("Afterpay is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupAfterpayPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Afterpay checkout session + const afterpayCheckout = sdkInstance.createAfterpayOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(afterpayCheckout); + + // Setup button click handler + setupButtonHandler(afterpayCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Afterpay payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(afterpayCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = afterpayCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Create payment field for email with optional prefill + const emailField = afterpayCheckout.createPaymentFields({ + type: "email", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#afterpay-full-name").appendChild(fullNameField); + document.querySelector("#afterpay-email").appendChild(emailField); } -function validateBillingAddress() { - const addressLine1 = document.querySelector("#address-line-1").value.trim(); - const adminArea1 = document.querySelector("#admin-area-1").value.trim(); - const postalCode = document.querySelector("#postal-code").value.trim(); - const countryCode = document.querySelector("#country-code").value.trim(); - const errors = []; - if (!addressLine1) errors.push('Address Line 1'); - if (!adminArea1) errors.push('Admin Area 1'); - if (!postalCode) errors.push('Postal Code'); - if (!countryCode) errors.push('Country Code'); - if (errors.length > 0) { - const errorMessage = `The following fields are required: ${errors.join(', ')}`; - setMessage(errorMessage); - throw new Error(errorMessage); - } - return { - addressLine1, - adminArea1, - postalCode, - countryCode - }; +function setupButtonHandler(afterpayCheckout) { + const afterpayButton = document.querySelector("#afterpay-button"); + afterpayButton.removeAttribute("hidden"); + + afterpayButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await afterpayCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrder(); + + // Start payment flow with popup mode + await afterpayCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); } -function afterpayCheckoutSessionOptionsPromiseFactory(createOrder) { - return async function afterpayCheckoutSessionOptionsPromise(billingData) { - const orderResult = await createOrder(); - const { addressLine1, adminArea1, postalCode, countryCode } = billingData; +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "USD" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID for the payment session return { - orderId: orderResult.orderId, - billingAddress: { - addressLine1, - adminArea1, - postalCode, - countryCode - } + orderId: id, }; - }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } } -(async () => { - let fullnameValue; - let emailValue; +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); - } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); - const prefillEmailCheckbox = document.querySelector("#prefill-email"); - prefillEmailCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillEmailCheckbox.checked) { - searchParams.append("prefillEmail", "true"); - } else { - searchParams.delete("prefillEmail"); + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "US", - defaultCurrencyCode: "USD", - }); + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["afterpay-payments"], - testBuyerCountry: countryCode, - }); + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); - const isAfterpayEligible = paymentMethods.isEligible("afterpay"); + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); - if (isAfterpayEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const afterpayCheckout = sdkInstance.createAfterpayOneTimePaymentSession({ - onApprove, - onCancel, - onError + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - const fullnameField = afterpayCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#afterpay-full-name").appendChild(fullnameField); - const emailField = afterpayCheckout.createPaymentFields({ - type: "email", - value: emailValue, + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#afterpay-email").appendChild(emailField); - const afterpayButton = document.querySelector("#afterpay-button"); - afterpayButton.removeAttribute("hidden"); - document.querySelector("#custom-fields").removeAttribute("hidden"); - async function onClick() { - try { - const billingData = validateBillingAddress(); - const valid = await afterpayCheckout.validate(); - if(valid) { - await afterpayCheckout.start( - { presentationMode: "popup" }, - afterpayCheckoutSessionOptionsPromiseFactory(createOrder)(billingData) - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - setMessage(e.message || "Validation failed"); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - afterpayButton.addEventListener("click", onClick); + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/afterpayPayments/html/src/index.html b/client/components/afterpayPayments/html/src/index.html index 4b964e6a..3fd62400 100644 --- a/client/components/afterpayPayments/html/src/index.html +++ b/client/components/afterpayPayments/html/src/index.html @@ -1,84 +1,68 @@ - + - - - - Afterpay Button Popup Flow - - - -
    -
    -
    -

    Afterpay Button Popup Flow

    -
    -
    - Controls for "Afterpay" payment flow -
    - - -
    -
    - - -
    -
    -
    -
    - - -
    -
    - - -
    -
    -

    Unbranded Afterpay Payment Button

    -

    - - This example uses the afterpay-button web component to launch the popup flow. -

    -
    -
    -
    - -
    -
    - + + + Afterpay - PayPal Web SDK + + + + + +

    Afterpay One-Time Payment Integration

    + +
    +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/bizumPayments/html/src/app.js b/client/components/bizumPayments/html/src/app.js index 1482e7a0..ebef54a1 100644 --- a/client/components/bizumPayments/html/src/app.js +++ b/client/components/bizumPayments/html/src/app.js @@ -1,143 +1,236 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ES", // Spain for Bizum testing + components: ["bizum-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Bizum is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "EUR", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isBizumEligible = paymentMethods.isEligible("bizum"); + + if (isBizumEligible) { + setupBizumPayment(sdkInstance); + } else { + showMessage({ + text: "Bizum is not eligible. Please ensure your buyer country is Spain and currency is EUR.", + type: "error", + }); + console.error("Bizum is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage("Transaction Successful"); +function setupBizumPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Bizum checkout session + const bizumCheckout = sdkInstance.createBizumOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(bizumCheckout); + + // Setup button click handler + setupButtonHandler(bizumCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Bizum payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(bizumCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = bizumCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Mount the field to the container + document.querySelector("#bizum-full-name").appendChild(fullNameField); } -(async () => { - let fullnameValue; +function setupButtonHandler(bizumCheckout) { + const bizumButton = document.querySelector("#bizum-button"); + bizumButton.removeAttribute("hidden"); - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + bizumButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await bizumCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await bizumCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); }); +} - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "ES", - defaultCurrencyCode: "EUR", - }); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "EUR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); + } + + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["bizum-payments"], - testBuyerCountry: countryCode, + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", + }); + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", }); +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", }); +} - const isBizumEligible = paymentMethods.isEligible("bizum"); - if (isBizumEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const bizumCheckout = sdkInstance.createBizumOneTimePaymentSession({ - onApprove, - onCancel, - onError +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - const fullnameField = bizumCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, - }); - document.querySelector("#bizum-full-name").appendChild(fullnameField); - - async function onClick() { - try { - const valid = await bizumCheckout.validate(); - if(valid) { - await bizumCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const bizumButton = document.querySelector("#bizum-button"); - bizumButton.removeAttribute("hidden"); - bizumButton.addEventListener("click", onClick); + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/bizumPayments/html/src/index.html b/client/components/bizumPayments/html/src/index.html index 0d432f35..c3dbf924 100644 --- a/client/components/bizumPayments/html/src/index.html +++ b/client/components/bizumPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - Bizum Button Popup Flow - - - -
    -
    -
    -

    Bizum Button Popup Flow

    -
    -
    - Controls for "Bizum" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Bizum Payment Button

    -

    - - This example uses the bizum-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + Bizum - PayPal Web SDK + + + + + +

    Bizum One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/klarnaPayments/html/src/app.js b/client/components/klarnaPayments/html/src/app.js index a459ad8a..e5d5b642 100644 --- a/client/components/klarnaPayments/html/src/app.js +++ b/client/components/klarnaPayments/html/src/app.js @@ -1,257 +1,310 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "GB", // Great Britain for Klarna testing + components: ["klarna-payments"], + }); + + // Check if Klarna is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "GBP", + }); + + const isKlarnaEligible = paymentMethods.isEligible("klarna"); + + if (isKlarnaEligible) { + setupKlarnaPayment(sdkInstance); + } else { + showMessage({ + text: "Klarna is not eligible. Please ensure your buyer country is Great Britain and currency is GBP.", + type: "error", + }); + console.error("Klarna is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "AUTHORIZE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [ - { - payee: { merchant_id: "U83XRVL67BW2J" }, - amount: { - currency_code: currencyCode, - value: 30, - breakdown: { - item_total: { currency_code: currencyCode, value: 30 }, - shipping: { currency_code: currencyCode, value: "0.00" }, - handling: { currency_code: currencyCode, value: "0.00" }, - tax_total: { currency_code: currencyCode, value: "0.00" }, - shipping_discount: { currency_code: currencyCode, value: "0.00" } - } - }, - items: [ - { - name: "jersey", - description: "jersey", - sku: "", - unit_amount: { currency_code: currencyCode, value: "15.00" }, - tax: { currency_code: currencyCode, value: "0.00" }, - category: "PHYSICAL_GOODS", - quantity: "1" - }, - { - name: "jersey", - description: "jersey", - sku: "", - unit_amount: { currency_code: currencyCode, value: "15.00" }, - tax: { currency_code: currencyCode, value: "0.00" }, - category: "PHYSICAL_GOODS", - quantity: "1" - } - ] - } - ] - }; +function setupKlarnaPayment(sdkInstance) { + try { + // Create Klarna checkout session + const klarnaCheckout = sdkInstance.createKlarnaOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Setup payment fields + setupPaymentFields(klarnaCheckout); + + // Setup button click handler + setupButtonHandler(klarnaCheckout); + } catch (error) { + console.error("Error setting up Klarna payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", }); + } +} + +function setupPaymentFields(klarnaCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = klarnaCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); - setMessage(`Order ${orderId}`); - return orderId; + // Create payment field for email with optional prefill + const emailField = klarnaCheckout.createPaymentFields({ + type: "email", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#klarna-full-name").appendChild(fullNameField); + document.querySelector("#klarna-email").appendChild(emailField); +} + +function setupButtonHandler(klarnaCheckout) { + const klarnaButton = document.querySelector("#klarna-button"); + klarnaButton.removeAttribute("hidden"); + + klarnaButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate billing address first + const billingData = validateBillingAddress(); + + // Validate the payment fields + const isValid = await klarnaCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrderWithBillingData() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrderWithBillingData(billingData); + + // Start payment flow with popup mode + await klarnaCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +function validateBillingAddress() { + const addressLine1 = document.querySelector("#address-line-1").value.trim(); + const adminArea1 = document.querySelector("#admin-area-1").value.trim(); + const adminArea2 = document.querySelector("#admin-area-2").value.trim(); + const postalCode = document.querySelector("#postal-code").value.trim(); + const countryCode = document.querySelector("#country-code").value.trim(); + const phoneCountryCode = document.querySelector("#phone-country-code").value.trim(); + const phoneNationalNumber = document.querySelector("#phone-national-number").value.trim(); + + const errors = []; + if (!addressLine1) errors.push("Address Line 1"); + if (!adminArea1) errors.push("Admin Area 1"); + if (!adminArea2) errors.push("Admin Area 2"); + if (!postalCode) errors.push("Postal Code"); + if (!countryCode) errors.push("Country Code"); + if (!phoneCountryCode) errors.push("Phone Country Code"); + if (!phoneNationalNumber) errors.push("Phone National Number"); + + if (errors.length > 0) { + const errorMessage = `The following fields are required: ${errors.join(", ")}`; + throw new Error(errorMessage); + } + + return { + addressLine1, + adminArea1, + adminArea2, + postalCode, + countryCode, + phoneCountryCode, + phoneNationalNumber, }; } -function klarnaCheckoutSessionOptionsPromiseFactory(createOrder) { - return async function klarnaCheckoutSessionOptionsPromise(billingData) { - const orderId = await createOrder(); - const { - addressLine1, - adminArea1, - adminArea2, - postalCode, - countryCode, - phoneCountryCode, - phoneNationalNumber - } = billingData; +// Create PayPal order with billing data +async function createOrderWithBillingData(billingData) { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "GBP" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID with billing data for the payment session return { - orderId, + orderId: id, billingAddress: { - addressLine1, - adminArea2, - adminArea1, - postalCode, - countryCode + addressLine1: billingData.addressLine1, + adminArea2: billingData.adminArea2, + adminArea1: billingData.adminArea1, + postalCode: billingData.postalCode, + countryCode: billingData.countryCode, }, phone: { - nationalNumber: phoneNationalNumber, - countryCode: phoneCountryCode, + nationalNumber: billingData.phoneNationalNumber, + countryCode: billingData.phoneCountryCode, }, }; - }; -} - -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); - try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, - }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); - } -} - -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; } - setMessage(message); -} - -function onError(data) { - console.log("onError", data); - setMessage(data); } -(async () => { - let fullName; - let email; +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); - const prefillCheckbox = document.querySelector("#prefill-full-name"); - prefillCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); - } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); - const prefillEmailCheckbox = document.querySelector("#prefill-email"); - prefillEmailCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillEmailCheckbox.checked) { - searchParams.append("prefillEmail", "true"); - } else { - searchParams.delete("prefillEmail"); + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "GB", - defaultCurrencyCode: "GBP", - }); + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["klarna-payments"], - testBuyerCountry: countryCode, - }); + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); - const isKlarnaEligible = paymentMethods.isEligible("klarna"); + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); - if (isKlarnaEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const klarnaCheckoutSessionOptionsPromise = klarnaCheckoutSessionOptionsPromiseFactory(createOrder); - const klarnaCheckout = sdkInstance.createKlarnaOneTimePaymentSession({ - onApprove, - onCancel, - onError + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - - const fullNameField = klarnaCheckout.createPaymentFields({ - type: "name", - value: fullName, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#klarna-full-name").appendChild(fullNameField); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} - const emailField = klarnaCheckout.createPaymentFields({ - type: "email", - value: email, +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#klarna-email").appendChild(emailField); - - function validateBillingAddress() { - const addressLine1 = document.querySelector("#address-line-1").value.trim(); - const adminArea1 = document.querySelector("#admin-area-1").value.trim(); - const adminArea2 = document.querySelector("#admin-area-2").value.trim(); - const postalCode = document.querySelector("#postal-code").value.trim(); - const countryCode = document.querySelector("#country-code").value.trim(); - const phoneCountryCode = document.querySelector("#phone-country-code").value.trim(); - const phoneNationalNumber = document.querySelector("#phone-national-number").value.trim(); - const errors = []; - if (!addressLine1) errors.push("Address Line 1"); - if (!adminArea1) errors.push("Admin Area 1"); - if (!adminArea2) errors.push("Admin Area 2"); - if (!postalCode) errors.push("Postal Code"); - if (!countryCode) errors.push("Country Code"); - if (!phoneCountryCode) errors.push("Phone Country Code"); - if (!phoneNationalNumber) errors.push("Phone National Number"); - if (errors.length > 0) { - const errorMessage = `The following fields are required: ${errors.join(", ")}`; - setMessage(errorMessage); - throw new Error(errorMessage); - } - return { - addressLine1, - adminArea1, - adminArea2, - postalCode, - countryCode, - phoneCountryCode, - phoneNationalNumber - }; - } - async function onClick() { - try { - const billingData = validateBillingAddress(); - const valid = await klarnaCheckout.validate(); - if(valid) { - await klarnaCheckout.start( - { presentationMode: "popup" }, - klarnaCheckoutSessionOptionsPromise(billingData) - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - setMessage(e.message || "Validation failed"); - } + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const klarnaButton = document.querySelector("#klarna-button"); - klarnaButton.removeAttribute("hidden"); - klarnaButton.addEventListener("click", onClick); - document.querySelector("#custom-fields").removeAttribute("hidden"); + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/klarnaPayments/html/src/index.html b/client/components/klarnaPayments/html/src/index.html index f54b34b3..3be55742 100644 --- a/client/components/klarnaPayments/html/src/index.html +++ b/client/components/klarnaPayments/html/src/index.html @@ -1,97 +1,185 @@ - + - - - - Klarna Button Popup Flow - - - -
    -
    -
    -

    Klarna Button Popup Flow

    -
    -
    - Controls for "Klarna" payment flow -
    - - -
    -
    - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Klarna Payment Button

    -

    - - This example uses the klarna-button web component to launch the popup flow. -

    -
    -
    - -
    - -
    -
    - +
    + +
    +
    +
    + + +
    +
    +
    + + -
    - - - + +
    + + + + diff --git a/client/components/multibancoPayments/html/src/app.js b/client/components/multibancoPayments/html/src/app.js index 2748a84b..84e7de91 100644 --- a/client/components/multibancoPayments/html/src/app.js +++ b/client/components/multibancoPayments/html/src/app.js @@ -1,140 +1,246 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} - -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "PT", // Portugal for Multibanco testing + components: ["multibanco-payments"], + }); - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Multibanco is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "EUR", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isMultibancoEligible = paymentMethods.isEligible("multibanco"); + + if (isMultibancoEligible) { + setupMultibancoPayment(sdkInstance); + } else { + showMessage({ + text: "Multibanco is not eligible. Please ensure your buyer country is Portugal and currency is EUR.", + type: "error", + }); + console.error("Multibanco is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupMultibancoPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Multibanco checkout session + const multibancoCheckout = sdkInstance.createMultibancoOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(multibancoCheckout); + + // Setup button click handler + setupButtonHandler(multibancoCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Multibanco payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); +function setupPaymentFields(multibancoCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = multibancoCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the field to the container + document.querySelector("#multibanco-full-name").appendChild(fullNameField); } -function onError(data) { - console.log("onError", data); - setMessage(data); +function setupButtonHandler(multibancoCheckout) { + const multibancoButton = document.querySelector("#multibanco-button"); + multibancoButton.removeAttribute("hidden"); + + multibancoButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await multibancoCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrder(); + + // Start payment flow with popup mode + await multibancoCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); } -(async () => { - let fullnameValue; +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "EUR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID for the payment session + return { + orderId: id, + }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "PT", - defaultCurrencyCode: "EUR", - }); + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["multibanco-payments"], - testBuyerCountry: countryCode, - }); + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); - const isMultibancoEligible = paymentMethods.isEligible("multibanco"); + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); - if (isMultibancoEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const multibancoCheckout = sdkInstance.createMultibancoOneTimePaymentSession({ - onApprove, - onCancel, - onError + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - const fullnameField = multibancoCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#multibanco-full-name").appendChild(fullnameField); - async function onClick() { - try { - const valid = await multibancoCheckout.validate(); - if(valid) { - await multibancoCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } - } - const multiBancoButton = document.querySelector("#multibanco-button"); - multiBancoButton.removeAttribute("hidden"); - multiBancoButton.addEventListener("click", onClick); } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", }); -})(); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/multibancoPayments/html/src/index.html b/client/components/multibancoPayments/html/src/index.html index 77c6fbcc..1c194203 100644 --- a/client/components/multibancoPayments/html/src/index.html +++ b/client/components/multibancoPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - multibanco Button Popup Flow - - - -
    -
    -
    -

    multibanco Button Popup Flow

    -
    -
    - Controls for "multibanco" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded multibanco Payment Button

    -

    - - This example uses the multibanco-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + Multibanco - PayPal Web SDK + + + + + +

    Multibanco One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/mybankPayments/html/src/app.js b/client/components/mybankPayments/html/src/app.js index 7eba1df1..1e7aa7ae 100644 --- a/client/components/mybankPayments/html/src/app.js +++ b/client/components/mybankPayments/html/src/app.js @@ -1,141 +1,246 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} - -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "IT", // Italy for MyBank testing + components: ["mybank-payments"], + }); - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if MyBank is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "EUR", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isMyBankEligible = paymentMethods.isEligible("mybank"); + + if (isMyBankEligible) { + setupMyBankPayment(sdkInstance); + } else { + showMessage({ + text: "MyBank is not eligible. Please ensure your buyer country is Italy and currency is EUR.", + type: "error", + }); + console.error("MyBank is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupMyBankPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create MyBank checkout session + const mybankCheckout = sdkInstance.createMyBankOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(mybankCheckout); + + // Setup button click handler + setupButtonHandler(mybankCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up MyBank payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); +function setupPaymentFields(mybankCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = mybankCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the field to the container + document.querySelector("#mybank-full-name").appendChild(fullNameField); } -function onError(data) { - console.log("onError", data); - setMessage(data); +function setupButtonHandler(mybankCheckout) { + const mybankButton = document.querySelector("#mybank-button"); + mybankButton.removeAttribute("hidden"); + + mybankButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await mybankCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrder(); + + // Start payment flow with popup mode + await mybankCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); } -(async () => { - let fullnameValue; +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "EUR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID for the payment session + return { + orderId: id, + }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "IT", - defaultCurrencyCode: "EUR", - }); + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["mybank-payments"], - testBuyerCountry: countryCode, - }); + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); - const isMyBankEligible = paymentMethods.isEligible("mybank"); + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); - if (isMyBankEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const mybankCheckout = sdkInstance.createMyBankOneTimePaymentSession({ - onApprove, - onCancel, - onError + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - const fullnameField = mybankCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#mybank-full-name").appendChild(fullnameField); - async function onClick() { - try { - const valid = await mybankCheckout.validate(); - if(valid) { - await mybankCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } - } - const myBankButton = document.querySelector("#mybank-button"); - myBankButton.removeAttribute("hidden"); - myBankButton.addEventListener("click", onClick); } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", }); -})(); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/mybankPayments/html/src/index.html b/client/components/mybankPayments/html/src/index.html index 978efddd..4688f85c 100644 --- a/client/components/mybankPayments/html/src/index.html +++ b/client/components/mybankPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - myBank Button Popup Flow - - - -
    -
    -
    -

    myBank Button Popup Flow

    -
    -
    - Controls for "myBank" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded myBank Payment Button

    -

    - - This example uses the mybank-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + MyBank - PayPal Web SDK + + + + + +

    MyBank One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/swishPayments/html/src/app.js b/client/components/swishPayments/html/src/app.js index 247de5d9..ad0bad34 100644 --- a/client/components/swishPayments/html/src/app.js +++ b/client/components/swishPayments/html/src/app.js @@ -1,169 +1,236 @@ -import { - createOrder as createOrderHelper, - captureOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "SE", // Sweden for Swish testing + components: ["swish-payments"], + }); + + // Check if Swish is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "SEK", + }); + + const isSwishEligible = paymentMethods.isEligible("swish"); + + if (isSwishEligible) { + setupSwishPayment(sdkInstance); + } else { + showMessage({ + text: "Swish is not eligible. Please ensure your buyer country is Sweden and currency is SEK.", + type: "error", + }); + console.error("Swish is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode, countryCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - payer: { - name: { - given_name: "Carlos", - surname: "Abejundio" - } - }, - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [ - { - reference_id: "#0000", - description: "Items bought at Hemm Store", - custom_id: "#1111", - soft_descriptor: "ABC", - amount: { - currency_code: currencyCode, - value: "1000", - breakdown: { - item_total: { - currency_code: currencyCode, - value: "980" - }, - tax_total: { - currency_code: currencyCode, - value: "20" - } - } - }, - shipping: { - address: { - address_line_1: "595343 / 86693 West Elm", - admin_area_2: "Stockholm", - postal_code: "SE-113 49", - country_code: countryCode - } - } - } - ] - }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, +function setupSwishPayment(sdkInstance) { + try { + // Create Swish checkout session + const swishCheckout = sdkInstance.createSwishOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + // Setup payment fields + setupPaymentFields(swishCheckout); + + // Setup button click handler + setupButtonHandler(swishCheckout); + } catch (error) { + console.error("Error setting up Swish payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Capturing order ${data.orderId}...`); - const orderData = await captureOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, +function setupPaymentFields(swishCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = swishCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, }); - console.log("Capture result", orderData); - setMessage(JSON.stringify(orderData, null, 2)); + + // Mount the field to the container + document.querySelector("#swish-full-name").appendChild(fullNameField); } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); +function setupButtonHandler(swishCheckout) { + const swishButton = document.querySelector("#swish-button"); + swishButton.removeAttribute("hidden"); + + swishButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await swishCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await swishCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); } -function onError(data) { - console.log("onError", data); - setMessage(data); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "SEK" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } } -(async () => { - let fullName; +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); - const prefillCheckbox = document.querySelector("#prefill-full-name"); - prefillCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "SE", - defaultCurrencyCode: "SEK", - }); + const data = await response.json(); + console.log("Order captured successfully:", data); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["swish-payments"], - testBuyerCountry: countryCode, - }); + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); - const isSwishEligible = paymentMethods.isEligible("swish"); + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); - if (isSwishEligible) { - const createOrder = buildCreateOrderFunction(currencyCode, countryCode); - const swishCheckout = sdkInstance.createSwishOneTimePaymentSession({ - onApprove, - onCancel, - onError + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", + }); + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} - const fullNameField = swishCheckout.createPaymentFields({ - type: "name", - value: fullName, +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#swish-full-name").appendChild(fullNameField); - - async function onClick() { - try { - const valid = await swishCheckout.validate(); - if (valid) { - await swishCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const swishButton = document.querySelector("#swish-button"); - swishButton.removeAttribute("hidden"); - swishButton.addEventListener("click", onClick); + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/swishPayments/html/src/index.html b/client/components/swishPayments/html/src/index.html index ee5067f7..4bfe3f40 100644 --- a/client/components/swishPayments/html/src/index.html +++ b/client/components/swishPayments/html/src/index.html @@ -1,62 +1,66 @@ - + - - - - Swish Button Popup Flow - - - -
    -
    -
    -

    Swish Button Popup Flow

    -
    -
    - Controls for "Swish" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Swish Payment Button

    -

    - - This example uses the swish-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + Swish - PayPal Web SDK + + + + + +

    Swish One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/trustlyPayments/html/src/app.js b/client/components/trustlyPayments/html/src/app.js index a4f978ac..a972a26b 100644 --- a/client/components/trustlyPayments/html/src/app.js +++ b/client/components/trustlyPayments/html/src/app.js @@ -1,158 +1,251 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "SE", // Sweden for Trustly testing + components: ["trustly-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Trustly is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "SEK", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isTrustlyEligible = paymentMethods.isEligible("trustly"); + + if (isTrustlyEligible) { + setupTrustlyPayment(sdkInstance); + } else { + showMessage({ + text: "Trustly is not eligible. Please ensure your buyer country is Sweden and currency is SEK.", + type: "error", + }); + console.error("Trustly is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupTrustlyPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Trustly checkout session + const trustlyCheckout = sdkInstance.createTrustlyOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(trustlyCheckout); + + // Setup button click handler + setupButtonHandler(trustlyCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Trustly payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(trustlyCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = trustlyCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Create email field + const emailField = trustlyCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#trustly-full-name").appendChild(fullNameField); + document.querySelector("#trustly-email").appendChild(emailField); } -(async () => { - let fullnameValue; - let emailValue; +function setupButtonHandler(trustlyCheckout) { + const trustlyButton = document.querySelector("#trustly-button"); + trustlyButton.removeAttribute("hidden"); - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + trustlyButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await trustlyCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await trustlyCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); }); +} - const prefillEmailCheckbox = document.querySelector("#prefill-email"); - prefillEmailCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillEmailCheckbox.checked) { - searchParams.append("prefillEmail", "true"); - } else { - searchParams.delete("prefillEmail"); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "SEK" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "SE", - defaultCurrencyCode: "SEK", - }); + const { id } = await response.json(); + console.log("Order created with ID:", id); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["trustly-payments"], - testBuyerCountry: countryCode, - }); + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); + } - const isTrustlyEligible = paymentMethods.isEligible("trustly"); + const data = await response.json(); + console.log("Order captured successfully:", data); - if (isTrustlyEligible) { - const trustlyCheckout = sdkInstance.createTrustlyOneTimePaymentSession({ - onApprove, - onCancel, - onError + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - const createOrder = buildCreateOrderFunction(currencyCode); - const fullnameField = trustlyCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#trustly-full-name").appendChild(fullnameField); - const emailField = trustlyCheckout.createPaymentFields({ - type: "email", - value: emailValue, + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#trustly-email").appendChild(emailField); - async function onClick() { - try { - const valid = await trustlyCheckout.validate(); - if(valid) { - await trustlyCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const trustlyButton = document.querySelector("#trustly-button"); - trustlyButton.removeAttribute("hidden"); - trustlyButton.addEventListener("click", onClick); + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/trustlyPayments/html/src/index.html b/client/components/trustlyPayments/html/src/index.html index edb6e1b1..bb249caf 100644 --- a/client/components/trustlyPayments/html/src/index.html +++ b/client/components/trustlyPayments/html/src/index.html @@ -1,65 +1,68 @@ - + - - - - Trustly Button Popup Flow - - - -
    -
    -
    -

    Trustly Button Popup Flow

    -
    -
    - Controls for "Trustly" payment flow -
    - - -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Trustly Payment Button

    -

    - - This example uses the trustly-button web component to launch the popup flow. -

    -
    -
    - -
    -
    - + + + Trustly - PayPal Web SDK + + + + + +

    Trustly One-Time Payment Integration

    + +
    +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/twintPayments/html/src/app.js b/client/components/twintPayments/html/src/app.js index 9f36f0f1..c2a91f6b 100644 --- a/client/components/twintPayments/html/src/app.js +++ b/client/components/twintPayments/html/src/app.js @@ -1,144 +1,236 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "CH", // Switzerland for TWINT testing + components: ["twint-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if TWINT is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "CHF", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isTwintEligible = paymentMethods.isEligible("twint"); + + if (isTwintEligible) { + setupTwintPayment(sdkInstance); + } else { + showMessage({ + text: "TWINT is not eligible. Please ensure your buyer country is Switzerland and currency is CHF.", + type: "error", + }); + console.error("TWINT is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupTwintPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create TWINT checkout session + const twintCheckout = sdkInstance.createTwintOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(twintCheckout); + + // Setup button click handler + setupButtonHandler(twintCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up TWINT payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(twintCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = twintCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Mount the field to the container + document.querySelector("#twint-full-name").appendChild(fullNameField); } -(async () => { - let fullName; +function setupButtonHandler(twintCheckout) { + const twintButton = document.querySelector("#twint-button"); + twintButton.removeAttribute("hidden"); - const prefillCheckbox = document.querySelector("#prefill-full-name"); - prefillCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + twintButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await twintCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await twintCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); }); +} - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "CH", - defaultCurrencyCode: "CHF", - }); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "CHF" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["twint-payments"], - testBuyerCountry: countryCode, - }); + const { id } = await response.json(); + console.log("Order created with ID:", id); - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} - const isTwintEligible = paymentMethods.isEligible("twint"); +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); + } - if (isTwintEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const twintCheckout = sdkInstance.createTwintOneTimePaymentSession({ - onApprove, - onCancel, - onError + const data = await response.json(); + console.log("Order captured successfully:", data); + + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} - const fullNameField = twintCheckout.createPaymentFields({ - type: "name", - value: fullName, +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#twint-full-name").appendChild(fullNameField); - - async function onClick() { - try { - const valid = await twintCheckout.validate(); - if(valid) { - await twintCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const twintButton = document.querySelector("#twint-button"); - twintButton.removeAttribute("hidden"); - twintButton.addEventListener("click", onClick); + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/twintPayments/html/src/index.html b/client/components/twintPayments/html/src/index.html index 553e743c..2b6a2196 100644 --- a/client/components/twintPayments/html/src/index.html +++ b/client/components/twintPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - Twint Button Popup Flow - - - -
    -
    -
    -

    Twint Button Popup Flow

    -
    -
    - Controls for "Twint" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Twint Payment Button

    -

    - - This example uses the twint-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + TWINT - PayPal Web SDK + + + + + +

    TWINT One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/verkkopankkiPayments/html/src/app.js b/client/components/verkkopankkiPayments/html/src/app.js index 230869ae..e25f83db 100644 --- a/client/components/verkkopankkiPayments/html/src/app.js +++ b/client/components/verkkopankkiPayments/html/src/app.js @@ -1,158 +1,262 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} - -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "FI", // Finland for Verkkopankki testing + components: ["verkkopankki-payments"], + }); - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Verkkopankki is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "EUR", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isVerkkopankkiEligible = paymentMethods.isEligible("verkkopankki"); + + if (isVerkkopankkiEligible) { + setupVerkkopankkiPayment(sdkInstance); + } else { + showMessage({ + text: "Verkkopankki is not eligible. Please ensure your buyer country is Finland and currency is EUR.", + type: "error", + }); + console.error("Verkkopankki is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupVerkkopankkiPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Verkkopankki checkout session + const verkkopankkiCheckout = sdkInstance.createVerkkopankkiOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(verkkopankkiCheckout); + + // Setup button click handler + setupButtonHandler(verkkopankkiCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Verkkopankki payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(verkkopankkiCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = verkkopankkiCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Create payment field for email with optional prefill + const emailField = verkkopankkiCheckout.createPaymentFields({ + type: "email", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#verkkopankki-full-name").appendChild(fullNameField); + document.querySelector("#verkkopankki-email").appendChild(emailField); } -(async () => { - let fullnameValue; - let emailValue; +function setupButtonHandler(verkkopankkiCheckout) { + const verkkopankkiButton = document.querySelector("#verkkopankki-button"); + verkkopankkiButton.removeAttribute("hidden"); - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + verkkopankkiButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await verkkopankkiCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrder(); + + // Start payment flow with popup mode + await verkkopankkiCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); }); +} - const prefillEmailCheckbox = document.querySelector("#prefill-email"); - prefillEmailCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillEmailCheckbox.checked) { - searchParams.append("prefillEmail", "true"); - } else { - searchParams.delete("prefillEmail"); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "EUR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "FI", - defaultCurrencyCode: "EUR", - }); + const { id } = await response.json(); + console.log("Order created with ID:", id); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["verkkopankki-payments"], - testBuyerCountry: countryCode, - }); + // Return order ID for the payment session + return { + orderId: id, + }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); - const isVerkkopankkiEligible = paymentMethods.isEligible("verkkopankki"); + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); - if (isVerkkopankkiEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const verkkopankkiCheckout = sdkInstance.createVerkkopankkiOneTimePaymentSession({ - onApprove, - onCancel, - onError + if (!response.ok) { + throw new Error("Failed to capture order"); + } + + const data = await response.json(); + console.log("Order captured successfully:", data); + + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); - const fullnameField = verkkopankkiCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", }); - document.querySelector("#verkkopankki-full-name").appendChild(fullnameField); - const emailField = verkkopankkiCheckout.createPaymentFields({ - type: "email", - value: emailValue, + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#verkkopankki-email").appendChild(emailField); - async function onClick() { - try { - const valid = await verkkopankkiCheckout.validate(); - if(valid) { - await verkkopankkiCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const verkkopankkiButton = document.querySelector("#verkkopankki-button"); - verkkopankkiButton.removeAttribute("hidden"); - verkkopankkiButton.addEventListener("click", onClick); + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/verkkopankkiPayments/html/src/index.html b/client/components/verkkopankkiPayments/html/src/index.html index 544d805e..e35a20e5 100644 --- a/client/components/verkkopankkiPayments/html/src/index.html +++ b/client/components/verkkopankkiPayments/html/src/index.html @@ -1,65 +1,68 @@ - + - - - - Verkkopankki Button Popup Flow - - - -
    -
    -
    -

    Verkkopankki Button Popup Flow

    -
    -
    - Controls for "Verkkopankki" payment flow -
    - - -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Verkkopankki Payment Button

    -

    - - This example uses the verkkopankki-button web component to launch the popup flow. -

    -
    -
    - -
    -
    - + + + Verkkopankki - PayPal Web SDK + + + + + +

    Verkkopankki One-Time Payment Integration

    + +
    +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/wechatpayPayments/html/src/app.js b/client/components/wechatpayPayments/html/src/app.js index 223085e4..43317a92 100644 --- a/client/components/wechatpayPayments/html/src/app.js +++ b/client/components/wechatpayPayments/html/src/app.js @@ -1,143 +1,236 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "CN", // China for WeChat Pay testing + components: ["wechatpay-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("LPM"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function buildCreateOrderFunction(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - purchase_units: [{ - amount: { - currency_code: currencyCode, - value: "10.00" - } - }] - }; - - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if WeChat Pay is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "CNY", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + const isWechatpayEligible = paymentMethods.isEligible("wechatpay"); + + if (isWechatpayEligible) { + setupWechatpayPayment(sdkInstance); + } else { + showMessage({ + text: "WeChat Pay is not eligible. Please ensure your buyer country is China and currency is CNY.", + type: "error", + }); + console.error("WeChat Pay is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupWechatpayPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create WeChat Pay checkout session + const wechatpayCheckout = sdkInstance.createWechatpayOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(wechatpayCheckout); + + // Setup button click handler + setupButtonHandler(wechatpayCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up WeChat Pay payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(wechatpayCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = wechatpayCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Mount the field to the container + document.querySelector("#wechatpay-full-name").appendChild(fullNameField); } -(async () => { - let fullnameValue; +function setupButtonHandler(wechatpayCheckout) { + const wechatpayButton = document.querySelector("#wechatpay-button"); + wechatpayButton.removeAttribute("hidden"); - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); + wechatpayButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await wechatpayCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await wechatpayCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); }); +} - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "CN", - defaultCurrencyCode: "CNY", - }); +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "CNY" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["wechatpay-payments"], - testBuyerCountry: countryCode, - }); + const { id } = await response.json(); + console.log("Order created with ID:", id); - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, - }); + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} - const isWechatpayEligible = paymentMethods.isEligible("wechatpay"); +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); + } - if (isWechatpayEligible) { - const createOrder = buildCreateOrderFunction(currencyCode); - const wechatpayCheckout = sdkInstance.createWechatpayOneTimePaymentSession({ - onApprove, - onCancel, - onError + const data = await response.json(); + console.log("Order captured successfully:", data); + + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", }); + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} - const fullnameField = wechatpayCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#wechatpay-full-name").appendChild(fullnameField); - - async function onClick() { - try { - const valid = await wechatpayCheckout.validate(); - if(valid) { - await wechatpayCheckout.start( - { presentationMode: "popup" }, - createOrder() - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - const wechatpayButton = document.querySelector("#wechatpay-button"); - wechatpayButton.removeAttribute("hidden"); - wechatpayButton.addEventListener("click", onClick); + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } +} - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/wechatpayPayments/html/src/index.html b/client/components/wechatpayPayments/html/src/index.html index c173357e..1e3c3650 100644 --- a/client/components/wechatpayPayments/html/src/index.html +++ b/client/components/wechatpayPayments/html/src/index.html @@ -1,60 +1,66 @@ - + - - - - WeChat Pay Button Popup Flow - - - -
    -
    -
    -

    WeChat Pay Button Popup Flow

    -
    -
    - Controls for "WeChat Pay" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded WeChat Pay Payment Button

    -

    - - This example uses the wechatpay-button web component to launch the popup flow. -

    -
    - -
    -
    - + + + WeChat Pay - PayPal Web SDK + + + + + +

    WeChat Pay One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + diff --git a/client/components/zipPayments/html/src/app.js b/client/components/zipPayments/html/src/app.js index 909582bf..ce102423 100644 --- a/client/components/zipPayments/html/src/app.js +++ b/client/components/zipPayments/html/src/app.js @@ -1,237 +1,246 @@ -import { - createOrder as createOrderHelper, - getOrder, - initAuth, -} from "/web-sdk/demo/assets/js/ordersApiFetchHelpers.mjs"; -import { initializeCountryAndCurrencyCodes } from "/web-sdk/demo/assets/js/lpmHelpers.mjs"; - -function setMessage(message) { - document.querySelector("#message").textContent = message; - console.log(message); -} +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "US", // United States for Zip testing + components: ["zip-payments"], + }); -setMessage("Fetching auth..."); -const { auth, clientName } = await initAuth("ZIP"); -setMessage("Auth ready: " + (auth.clientId ? "clientId" : "clientToken")); - -function createOrderFactory(currencyCode) { - return async function createOrder() { - setMessage("Creating order..."); - const orderPayload = { - intent: "CAPTURE", - processing_instruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", - purchase_units: [ - { - amount: { - breakdown: { - item_total: { - currency_code: currencyCode, - value: "34.10" - }, - tax_total: { - currency_code: currencyCode, - value: "2.00" - } - }, - currency_code: currencyCode, - value: "36.10" - }, - payee: { - merchant_id: "M683SLY6MTM78" - }, - items: [ - { - name: "Shirt", - quantity: "1", - unit_amount: { - currency_code: currencyCode, - value: "12.05" - }, - tax: { - currency_code: currencyCode, - value: "1.00" - } - }, - { - name: "Trouser", - quantity: "1", - unit_amount: { - currency_code: currencyCode, - value: "22.05" - }, - tax: { - currency_code: currencyCode, - value: "1.00" - } - } - ] - } - ] - }; - const { id: orderId } = await createOrderHelper({ - headers: { "X-CSRF-TOKEN": "" }, - body: orderPayload, - clientName, + // Check if Zip is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "USD", + }); + + const isZipEligible = paymentMethods.isEligible("zip"); + + if (isZipEligible) { + setupZipPayment(sdkInstance); + } else { + showMessage({ + text: "Zip is not eligible. Please ensure your buyer country is United States and currency is USD.", + type: "error", + }); + console.error("Zip is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", }); - setMessage(`Order ${orderId}`); - return { orderId }; - }; + } } -async function onApprove(data) { - console.log("onApprove", data); - setMessage(`Fetching order details for ${data.orderId}...`); +function setupZipPayment(sdkInstance) { try { - const orderDetails = await getOrder({ - orderId: data.orderId, - headers: { "X-CSRF-TOKEN": "" }, - clientName, + // Create Zip checkout session + const zipCheckout = sdkInstance.createZipOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, }); - console.log("Order details", orderDetails); - setMessage(JSON.stringify(orderDetails, null, 2)); + + // Setup payment fields + setupPaymentFields(zipCheckout); + + // Setup button click handler + setupButtonHandler(zipCheckout); } catch (error) { - console.error("Error fetching order details:", error); - setMessage(`Transaction Successful but failed to fetch order details: ${error.message}`); + console.error("Error setting up Zip payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); } } -function onCancel(data) { - console.log("onCancel", data); - let message = "Canceled order"; - if (data) { - message += ` ${data.orderId}`; - } - setMessage(message); -} +function setupPaymentFields(zipCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = zipCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); -function onError(data) { - console.log("onError", data); - setMessage(data); + // Mount the field to the container + document.querySelector("#zip-full-name").appendChild(fullNameField); } -function validateBillingAddress() { - const addressLine1 = document.querySelector("#address-line-1").value.trim(); - const adminArea1 = document.querySelector("#admin-area-1").value.trim(); - const adminArea2 = document.querySelector("#admin-area-2").value.trim(); - const postalCode = document.querySelector("#postal-code").value.trim(); - const countryCode = document.querySelector("#country-code").value.trim(); - const phoneCountryCode = document.querySelector("#phone-country-code").value.trim(); - const phoneNationalNumber = document.querySelector("#phone-national-number").value.trim(); - const errors = []; - if (!addressLine1) errors.push('Address Line 1'); - if (!adminArea1) errors.push('Admin Area 1'); - if (!adminArea2) errors.push('Admin Area 2'); - if (!postalCode) errors.push('Postal Code'); - if (!countryCode) errors.push('Country Code'); - if (!phoneCountryCode) errors.push('Phone Country Code'); - if (!phoneNationalNumber) errors.push('Phone National Number'); - if (errors.length > 0) { - const errorMessage = `The following fields are required: ${errors.join(', ')}`; - setMessage(errorMessage); - throw new Error(errorMessage); - } - return { - addressLine1, - adminArea1, - adminArea2, - postalCode, - countryCode, - phoneCountryCode, - phoneNationalNumber - }; +function setupButtonHandler(zipCheckout) { + const zipButton = document.querySelector("#zip-button"); + zipButton.removeAttribute("hidden"); + + zipButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await zipCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // get the promise reference by invoking createOrder() + // do not await this async function since it can cause transient activation issues + const createOrderPromise = createOrder(); + + // Start payment flow with popup mode + await zipCheckout.start( + { presentationMode: "popup" }, + createOrderPromise, + ); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: error.message || "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); } -function zipCheckoutSessionOptionsPromiseFactory(createOrder) { - return async function zipCheckoutSessionOptionsPromise(billingData) { - const orderResult = await createOrder(); - const { - addressLine1, - adminArea1, - adminArea2, - postalCode, - countryCode, - phoneCountryCode, - phoneNationalNumber - } = billingData; - return { - orderId: orderResult.orderId, - billingAddress: { - addressLine1, - adminArea1, - adminArea2, - postalCode, - countryCode - }, - phone: { - countryCode: phoneCountryCode, - nationalNumber: phoneNationalNumber, +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "USD" }), }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + // Return order ID for the payment session + return { + orderId: id, }; - }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } } -(async () => { - let fullnameValue; - const prefillNameCheckbox = document.querySelector("#prefill-full-name"); - prefillNameCheckbox.addEventListener("change", () => { - const searchParams = new URLSearchParams(window.location.search); - if (prefillNameCheckbox.checked) { - searchParams.append("prefillName", "true"); - } else { - searchParams.delete("prefillName"); +// Capture order after approval +async function captureOrder(orderId) { + try { + console.log("Capturing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/capture`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to capture order"); } - window.location.href = window.location.pathname + "?" + searchParams.toString(); - }); - const { countryCode, currencyCode } = initializeCountryAndCurrencyCodes({ - defaultCountryCode: "US", - defaultCurrencyCode: "USD", - }); - const sdkInstance = await window.paypal.createInstance({ - ...auth, - components: ["zip-payments"], - testBuyerCountry: countryCode, + + const data = await response.json(); + console.log("Order captured successfully:", data); + + return data; + } catch (error) { + console.error("Error capturing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await captureOrder(data.orderId); + console.log("Capture successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + type: "success", + }); + } catch (error) { + console.error("Capture failed:", error); + showMessage({ + text: "Payment approved but capture failed.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", }); - const paymentMethods = await sdkInstance.findEligibleMethods({ - currencyCode: currencyCode, +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", }); - const isZipEligible = paymentMethods.isEligible("zip"); - if (isZipEligible) { - const createOrder = createOrderFactory(currencyCode); - const zipCheckout = sdkInstance.createZipOneTimePaymentSession({ - onApprove, - onCancel, - onError - }); - const fullnameField = zipCheckout.createPaymentFields({ - type: "name", - value: fullnameValue, +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, }); - document.querySelector("#zip-full-name").appendChild(fullnameField); - const zipButton = document.querySelector("#zip-button"); - zipButton.removeAttribute("hidden"); - document.querySelector("#custom-fields").removeAttribute("hidden"); - async function onClick() { - try { - const billingData = validateBillingAddress(); - const valid = await zipCheckout.validate(); - if(valid) { - await zipCheckout.start( - { presentationMode: "popup" }, - zipCheckoutSessionOptionsPromiseFactory(createOrder)(billingData) - ); - } else { - setMessage("validation failed"); - } - } catch (e) { - console.error(e); - setMessage(e.message || "Validation failed"); - } + + if (!response.ok) { + throw new Error("Failed to fetch client id"); } - zipButton.addEventListener("click", onClick); + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; } - document.querySelector("#update-locale").addEventListener("change", async (event) => { - const newLocale = event.target.value; - sdkInstance.updateLocale(newLocale); - setMessage(`Locale updated to ${newLocale}`); - }); -})(); +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/zipPayments/html/src/index.html b/client/components/zipPayments/html/src/index.html index c52f72a4..aa32f628 100644 --- a/client/components/zipPayments/html/src/index.html +++ b/client/components/zipPayments/html/src/index.html @@ -1,97 +1,66 @@ - + - - - - Zip Button Popup Flow - - - - -
    -
    -
    -

    Zip Button Popup Flow

    -
    -
    - Controls for "Zip" payment flow -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -

    Unbranded Zip Payment Button

    -

    - - This example uses the zip-button web component to launch the popup flow. -

    -
    - -
    - -
    -
    - + + + Zip - PayPal Web SDK + + + + + +

    Zip One-Time Payment Integration

    + +
    +
    +
    -
    - - - + +
    + + + + From 0c4f88dbbf88ff933a95dbd7bf68902042966cbe Mon Sep 17 00:00:00 2001 From: Reegan1819 Date: Fri, 27 Feb 2026 00:15:30 +0530 Subject: [PATCH 6/7] [DTLAMBO-496] adding apms and fixing issues --- .../afterpayPayments/html/src/app.js | 26 +- .../alfamartPayments/html/README.md | 173 ++++++++++++ .../alfamartPayments/html/src/app.js | 251 ++++++++++++++++++ .../alfamartPayments/html/src/index.html | 68 +++++ .../components/bizumPayments/html/src/app.js | 26 +- client/components/dokuPayments/html/README.md | 173 ++++++++++++ .../components/dokuPayments/html/src/app.js | 251 ++++++++++++++++++ .../dokuPayments/html/src/index.html | 68 +++++ .../components/gopayPayments/html/README.md | 173 ++++++++++++ .../components/gopayPayments/html/src/app.js | 251 ++++++++++++++++++ .../gopayPayments/html/src/index.html | 68 +++++ .../indomaretPayments/html/README.md | 173 ++++++++++++ .../indomaretPayments/html/src/app.js | 251 ++++++++++++++++++ .../indomaretPayments/html/src/index.html | 68 +++++ .../indonesiaBanksPayments/html/README.md | 173 ++++++++++++ .../indonesiaBanksPayments/html/src/app.js | 251 ++++++++++++++++++ .../html/src/index.html | 68 +++++ .../jeniuspayPayments/html/README.md | 173 ++++++++++++ .../jeniuspayPayments/html/src/app.js | 251 ++++++++++++++++++ .../jeniuspayPayments/html/src/index.html | 68 +++++ .../components/klarnaPayments/html/src/app.js | 26 +- .../components/kredivoPayments/html/README.md | 173 ++++++++++++ .../kredivoPayments/html/src/app.js | 251 ++++++++++++++++++ .../kredivoPayments/html/src/index.html | 68 +++++ .../components/linkajaPayments/html/README.md | 173 ++++++++++++ .../linkajaPayments/html/src/app.js | 251 ++++++++++++++++++ .../linkajaPayments/html/src/index.html | 68 +++++ .../multibancoPayments/html/src/app.js | 26 +- .../components/mybankPayments/html/src/app.js | 26 +- client/components/ovoPayments/html/README.md | 173 ++++++++++++ client/components/ovoPayments/html/src/app.js | 251 ++++++++++++++++++ .../ovoPayments/html/src/index.html | 68 +++++ .../components/swishPayments/html/src/app.js | 26 +- .../trustlyPayments/html/src/app.js | 26 +- .../components/twintPayments/html/src/app.js | 26 +- .../verkkopankkiPayments/html/src/app.js | 26 +- .../wechatpayPayments/html/src/app.js | 26 +- client/components/zipPayments/html/src/app.js | 26 +- 38 files changed, 4571 insertions(+), 143 deletions(-) create mode 100644 client/components/alfamartPayments/html/README.md create mode 100644 client/components/alfamartPayments/html/src/app.js create mode 100644 client/components/alfamartPayments/html/src/index.html create mode 100644 client/components/dokuPayments/html/README.md create mode 100644 client/components/dokuPayments/html/src/app.js create mode 100644 client/components/dokuPayments/html/src/index.html create mode 100644 client/components/gopayPayments/html/README.md create mode 100644 client/components/gopayPayments/html/src/app.js create mode 100644 client/components/gopayPayments/html/src/index.html create mode 100644 client/components/indomaretPayments/html/README.md create mode 100644 client/components/indomaretPayments/html/src/app.js create mode 100644 client/components/indomaretPayments/html/src/index.html create mode 100644 client/components/indonesiaBanksPayments/html/README.md create mode 100644 client/components/indonesiaBanksPayments/html/src/app.js create mode 100644 client/components/indonesiaBanksPayments/html/src/index.html create mode 100644 client/components/jeniuspayPayments/html/README.md create mode 100644 client/components/jeniuspayPayments/html/src/app.js create mode 100644 client/components/jeniuspayPayments/html/src/index.html create mode 100644 client/components/kredivoPayments/html/README.md create mode 100644 client/components/kredivoPayments/html/src/app.js create mode 100644 client/components/kredivoPayments/html/src/index.html create mode 100644 client/components/linkajaPayments/html/README.md create mode 100644 client/components/linkajaPayments/html/src/app.js create mode 100644 client/components/linkajaPayments/html/src/index.html create mode 100644 client/components/ovoPayments/html/README.md create mode 100644 client/components/ovoPayments/html/src/app.js create mode 100644 client/components/ovoPayments/html/src/index.html diff --git a/client/components/afterpayPayments/html/src/app.js b/client/components/afterpayPayments/html/src/app.js index 07aafd45..2313714d 100644 --- a/client/components/afterpayPayments/html/src/app.js +++ b/client/components/afterpayPayments/html/src/app.js @@ -166,29 +166,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -198,17 +198,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/alfamartPayments/html/README.md b/client/components/alfamartPayments/html/README.md new file mode 100644 index 00000000..f43f10fe --- /dev/null +++ b/client/components/alfamartPayments/html/README.md @@ -0,0 +1,173 @@ +# Alfamart One-Time Payment Integration + +This example demonstrates how to integrate Alfamart payments using PayPal's v6 Web SDK. Alfamart is one of Indonesia's largest convenience store chains, offering cash payment services that allow customers to complete online purchases by paying at any Alfamart store location. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Alfamart integration flow: + +1. Initialize PayPal Web SDK with the Alfamart component +2. Check eligibility for Alfamart payment method +3. Create Alfamart payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Alfamart popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Alfamart one-time payment integration +- Full name and email field validation +- Popup payment flow with payment code generation +- Eligibility checking for Alfamart +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Alfamart Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Alfamart** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Alfamart demo directory:** + + ```bash + cd client/components/alfamartPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Alfamart is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Alfamart components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Alfamart is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Alfamart payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers receive a payment code to complete their purchase at any Alfamart store. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Alfamart not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Alfamart is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Alfamart + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/alfamartPayments/html/src/app.js b/client/components/alfamartPayments/html/src/app.js new file mode 100644 index 00000000..7925b5cf --- /dev/null +++ b/client/components/alfamartPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Alfamart testing + components: ["alfamart-payments"], + }); + + // Check if Alfamart is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isAlfamartEligible = paymentMethods.isEligible("alfamart"); + + if (isAlfamartEligible) { + setupAlfamartPayment(sdkInstance); + } else { + showMessage({ + text: "Alfamart is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Alfamart is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupAlfamartPayment(sdkInstance) { + try { + // Create Alfamart checkout session + const alfamartCheckout = sdkInstance.createAlfamartOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(alfamartCheckout); + + // Setup button click handler + setupButtonHandler(alfamartCheckout); + } catch (error) { + console.error("Error setting up Alfamart payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(alfamartCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = alfamartCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = alfamartCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#alfamart-full-name").appendChild(fullNameField); + document.querySelector("#alfamart-email").appendChild(emailField); +} + +function setupButtonHandler(alfamartCheckout) { + const alfamartButton = document.querySelector("#alfamart-button"); + alfamartButton.removeAttribute("hidden"); + + alfamartButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await alfamartCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await alfamartCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/alfamartPayments/html/src/index.html b/client/components/alfamartPayments/html/src/index.html new file mode 100644 index 00000000..d27e7e83 --- /dev/null +++ b/client/components/alfamartPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Alfamart - PayPal Web SDK + + + + + +

    Alfamart One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/bizumPayments/html/src/app.js b/client/components/bizumPayments/html/src/app.js index ebef54a1..627d4963 100644 --- a/client/components/bizumPayments/html/src/app.js +++ b/client/components/bizumPayments/html/src/app.js @@ -140,29 +140,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -172,17 +172,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/dokuPayments/html/README.md b/client/components/dokuPayments/html/README.md new file mode 100644 index 00000000..977f6314 --- /dev/null +++ b/client/components/dokuPayments/html/README.md @@ -0,0 +1,173 @@ +# Doku One-Time Payment Integration + +This example demonstrates how to integrate Doku payments using PayPal's v6 Web SDK. Doku is a digital wallet payment method in Indonesia that provides secure online payment solutions for customers and merchants. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Doku integration flow: + +1. Initialize PayPal Web SDK with the Doku component +2. Check eligibility for Doku payment method +3. Create Doku payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Doku popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Doku one-time payment integration +- Full name and email field validation +- Popup payment flow with Doku authentication +- Eligibility checking for Doku +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Doku Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Doku** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Doku demo directory:** + + ```bash + cd client/components/dokuPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Doku is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Doku components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Doku is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Doku payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with Doku and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Doku not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Doku is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Doku + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/dokuPayments/html/src/app.js b/client/components/dokuPayments/html/src/app.js new file mode 100644 index 00000000..5fcf7aa4 --- /dev/null +++ b/client/components/dokuPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Doku testing + components: ["doku-payments"], + }); + + // Check if Doku is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isDokuEligible = paymentMethods.isEligible("doku"); + + if (isDokuEligible) { + setupDokuPayment(sdkInstance); + } else { + showMessage({ + text: "Doku is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Doku is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupDokuPayment(sdkInstance) { + try { + // Create Doku checkout session + const dokuCheckout = sdkInstance.createDokuOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(dokuCheckout); + + // Setup button click handler + setupButtonHandler(dokuCheckout); + } catch (error) { + console.error("Error setting up Doku payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(dokuCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = dokuCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = dokuCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#doku-full-name").appendChild(fullNameField); + document.querySelector("#doku-email").appendChild(emailField); +} + +function setupButtonHandler(dokuCheckout) { + const dokuButton = document.querySelector("#doku-button"); + dokuButton.removeAttribute("hidden"); + + dokuButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await dokuCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await dokuCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/dokuPayments/html/src/index.html b/client/components/dokuPayments/html/src/index.html new file mode 100644 index 00000000..f80019b7 --- /dev/null +++ b/client/components/dokuPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Doku - PayPal Web SDK + + + + + +

    Doku One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/gopayPayments/html/README.md b/client/components/gopayPayments/html/README.md new file mode 100644 index 00000000..b06a9461 --- /dev/null +++ b/client/components/gopayPayments/html/README.md @@ -0,0 +1,173 @@ +# Gopay One-Time Payment Integration + +This example demonstrates how to integrate Gopay payments using PayPal's v6 Web SDK. Gopay is Go-Jek's mobile wallet service and one of Indonesia's most widely used digital payment platforms, allowing customers to make secure payments using their Gopay account balance. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Gopay integration flow: + +1. Initialize PayPal Web SDK with the Gopay component +2. Check eligibility for Gopay payment method +3. Create Gopay payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Gopay popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Gopay one-time payment integration +- Full name and email field validation +- Popup payment flow with Gopay authentication +- Eligibility checking for Gopay +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Gopay Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Gopay** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Gopay demo directory:** + + ```bash + cd client/components/gopayPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Gopay is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Gopay components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Gopay is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Gopay payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with their Gopay account and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Gopay not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Gopay is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Gopay + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/gopayPayments/html/src/app.js b/client/components/gopayPayments/html/src/app.js new file mode 100644 index 00000000..cfba00eb --- /dev/null +++ b/client/components/gopayPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Gopay testing + components: ["gopay-payments"], + }); + + // Check if Gopay is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isGopayEligible = paymentMethods.isEligible("gopay"); + + if (isGopayEligible) { + setupGopayPayment(sdkInstance); + } else { + showMessage({ + text: "Gopay is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Gopay is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupGopayPayment(sdkInstance) { + try { + // Create Gopay checkout session + const gopayCheckout = sdkInstance.createGopayOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(gopayCheckout); + + // Setup button click handler + setupButtonHandler(gopayCheckout); + } catch (error) { + console.error("Error setting up Gopay payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(gopayCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = gopayCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = gopayCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#gopay-full-name").appendChild(fullNameField); + document.querySelector("#gopay-email").appendChild(emailField); +} + +function setupButtonHandler(gopayCheckout) { + const gopayButton = document.querySelector("#gopay-button"); + gopayButton.removeAttribute("hidden"); + + gopayButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await gopayCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await gopayCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/gopayPayments/html/src/index.html b/client/components/gopayPayments/html/src/index.html new file mode 100644 index 00000000..37ed2005 --- /dev/null +++ b/client/components/gopayPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Gopay - PayPal Web SDK + + + + + +

    Gopay One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/indomaretPayments/html/README.md b/client/components/indomaretPayments/html/README.md new file mode 100644 index 00000000..c6f17017 --- /dev/null +++ b/client/components/indomaretPayments/html/README.md @@ -0,0 +1,173 @@ +# Indomaret One-Time Payment Integration + +This example demonstrates how to integrate Indomaret payments using PayPal's v6 Web SDK. Indomaret is Indonesia's largest convenience store chain, offering cash payment services that enable customers to complete online purchases by paying at any Indomaret store location nationwide. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Indomaret integration flow: + +1. Initialize PayPal Web SDK with the Indomaret component +2. Check eligibility for Indomaret payment method +3. Create Indomaret payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Indomaret popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Indomaret one-time payment integration +- Full name and email field validation +- Popup payment flow with payment code generation +- Eligibility checking for Indomaret +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Indomaret Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Indomaret** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Indomaret demo directory:** + + ```bash + cd client/components/indomaretPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Indomaret is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Indomaret components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Indomaret is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Indomaret payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers receive a payment code to complete their purchase at any Indomaret store. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Indomaret not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Indomaret is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Indomaret + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/indomaretPayments/html/src/app.js b/client/components/indomaretPayments/html/src/app.js new file mode 100644 index 00000000..56b12950 --- /dev/null +++ b/client/components/indomaretPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Indomaret testing + components: ["indomaret-payments"], + }); + + // Check if Indomaret is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isIndomaretEligible = paymentMethods.isEligible("indomaret"); + + if (isIndomaretEligible) { + setupIndomaretPayment(sdkInstance); + } else { + showMessage({ + text: "Indomaret is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Indomaret is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupIndomaretPayment(sdkInstance) { + try { + // Create Indomaret checkout session + const indomaretCheckout = sdkInstance.createIndomaretOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(indomaretCheckout); + + // Setup button click handler + setupButtonHandler(indomaretCheckout); + } catch (error) { + console.error("Error setting up Indomaret payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(indomaretCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = indomaretCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = indomaretCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#indomaret-full-name").appendChild(fullNameField); + document.querySelector("#indomaret-email").appendChild(emailField); +} + +function setupButtonHandler(indomaretCheckout) { + const indomaretButton = document.querySelector("#indomaret-button"); + indomaretButton.removeAttribute("hidden"); + + indomaretButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await indomaretCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await indomaretCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/indomaretPayments/html/src/index.html b/client/components/indomaretPayments/html/src/index.html new file mode 100644 index 00000000..8f1bf7a2 --- /dev/null +++ b/client/components/indomaretPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Indomaret - PayPal Web SDK + + + + + +

    Indomaret One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/indonesiaBanksPayments/html/README.md b/client/components/indonesiaBanksPayments/html/README.md new file mode 100644 index 00000000..616afdf3 --- /dev/null +++ b/client/components/indonesiaBanksPayments/html/README.md @@ -0,0 +1,173 @@ +# Indonesia Banks One-Time Payment Integration + +This example demonstrates how to integrate Indonesia Banks payments using PayPal's v6 Web SDK. Indonesia Banks is a bank transfer payment method that allows customers to make secure payments directly from their Indonesian bank accounts to complete online purchases. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Indonesia Banks integration flow: + +1. Initialize PayPal Web SDK with the Indonesia Banks component +2. Check eligibility for Indonesia Banks payment method +3. Create Indonesia Banks payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Indonesia Banks popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Indonesia Banks one-time payment integration +- Full name and email field validation +- Popup payment flow with bank selection and authentication +- Eligibility checking for Indonesia Banks +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Indonesia Banks Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Indonesia Banks** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Indonesia Banks demo directory:** + + ```bash + cd client/components/indonesiaBanksPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Indonesia Banks is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Indonesia Banks components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Indonesia Banks is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Indonesia Banks payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers select their bank and authenticate the transfer. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Indonesia Banks not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Indonesia Banks is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Indonesia Banks + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/indonesiaBanksPayments/html/src/app.js b/client/components/indonesiaBanksPayments/html/src/app.js new file mode 100644 index 00000000..6f30e05d --- /dev/null +++ b/client/components/indonesiaBanksPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Indonesia Banks testing + components: ["indonesia-banks-payments"], + }); + + // Check if Indonesia Banks is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isIndonesiaBanksEligible = paymentMethods.isEligible("indonesia_banks"); + + if (isIndonesiaBanksEligible) { + setupIndonesiaBanksPayment(sdkInstance); + } else { + showMessage({ + text: "Indonesia Banks is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Indonesia Banks is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupIndonesiaBanksPayment(sdkInstance) { + try { + // Create Indonesia Banks checkout session + const indonesiaBanksCheckout = sdkInstance.createIndonesiaBanksOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(indonesiaBanksCheckout); + + // Setup button click handler + setupButtonHandler(indonesiaBanksCheckout); + } catch (error) { + console.error("Error setting up Indonesia Banks payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(indonesiaBanksCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = indonesiaBanksCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = indonesiaBanksCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#indonesia-banks-full-name").appendChild(fullNameField); + document.querySelector("#indonesia-banks-email").appendChild(emailField); +} + +function setupButtonHandler(indonesiaBanksCheckout) { + const indonesiaBanksButton = document.querySelector("#indonesia-banks-button"); + indonesiaBanksButton.removeAttribute("hidden"); + + indonesiaBanksButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await indonesiaBanksCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await indonesiaBanksCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/indonesiaBanksPayments/html/src/index.html b/client/components/indonesiaBanksPayments/html/src/index.html new file mode 100644 index 00000000..b9b08aec --- /dev/null +++ b/client/components/indonesiaBanksPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Indonesia Banks - PayPal Web SDK + + + + + +

    Indonesia Banks One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/jeniuspayPayments/html/README.md b/client/components/jeniuspayPayments/html/README.md new file mode 100644 index 00000000..0e6c6d63 --- /dev/null +++ b/client/components/jeniuspayPayments/html/README.md @@ -0,0 +1,173 @@ +# Jeniuspay One-Time Payment Integration + +This example demonstrates how to integrate Jeniuspay payments using PayPal's v6 Web SDK. Jeniuspay is a digital banking and payment solution in Indonesia that offers customers a seamless payment experience through mobile banking integration. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Jeniuspay integration flow: + +1. Initialize PayPal Web SDK with the Jeniuspay component +2. Check eligibility for Jeniuspay payment method +3. Create Jeniuspay payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Jeniuspay popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Jeniuspay one-time payment integration +- Full name and email field validation +- Popup payment flow with Jeniuspay authentication +- Eligibility checking for Jeniuspay +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Jeniuspay Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Jeniuspay** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Jeniuspay demo directory:** + + ```bash + cd client/components/jeniuspayPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Jeniuspay is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Jeniuspay components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Jeniuspay is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Jeniuspay payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with Jeniuspay and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Jeniuspay not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Jeniuspay is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Jeniuspay + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/jeniuspayPayments/html/src/app.js b/client/components/jeniuspayPayments/html/src/app.js new file mode 100644 index 00000000..a2a66fcb --- /dev/null +++ b/client/components/jeniuspayPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Jeniuspay testing + components: ["jeniuspay-payments"], + }); + + // Check if Jeniuspay is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isJeniuspayEligible = paymentMethods.isEligible("jeniuspay"); + + if (isJeniuspayEligible) { + setupJeniuspayPayment(sdkInstance); + } else { + showMessage({ + text: "Jeniuspay is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Jeniuspay is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupJeniuspayPayment(sdkInstance) { + try { + // Create Jeniuspay checkout session + const jeniuspayCheckout = sdkInstance.createJeniuspayOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(jeniuspayCheckout); + + // Setup button click handler + setupButtonHandler(jeniuspayCheckout); + } catch (error) { + console.error("Error setting up Jeniuspay payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(jeniuspayCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = jeniuspayCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = jeniuspayCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#jeniuspay-full-name").appendChild(fullNameField); + document.querySelector("#jeniuspay-email").appendChild(emailField); +} + +function setupButtonHandler(jeniuspayCheckout) { + const jeniuspayButton = document.querySelector("#jeniuspay-button"); + jeniuspayButton.removeAttribute("hidden"); + + jeniuspayButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await jeniuspayCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await jeniuspayCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/jeniuspayPayments/html/src/index.html b/client/components/jeniuspayPayments/html/src/index.html new file mode 100644 index 00000000..f09a393e --- /dev/null +++ b/client/components/jeniuspayPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Jeniuspay - PayPal Web SDK + + + + + +

    Jeniuspay One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/klarnaPayments/html/src/app.js b/client/components/klarnaPayments/html/src/app.js index e5d5b642..fb7e2a04 100644 --- a/client/components/klarnaPayments/html/src/app.js +++ b/client/components/klarnaPayments/html/src/app.js @@ -214,29 +214,29 @@ async function createOrderWithBillingData(billingData) { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -246,17 +246,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/kredivoPayments/html/README.md b/client/components/kredivoPayments/html/README.md new file mode 100644 index 00000000..4e11dc8c --- /dev/null +++ b/client/components/kredivoPayments/html/README.md @@ -0,0 +1,173 @@ +# Kredivo One-Time Payment Integration + +This example demonstrates how to integrate Kredivo payments using PayPal's v6 Web SDK. Kredivo is a leading buy now pay later (BNPL) installment service in Indonesia that allows customers to make purchases and pay in flexible installments through their mobile app or web platform. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete Kredivo integration flow: + +1. Initialize PayPal Web SDK with the Kredivo component +2. Check eligibility for Kredivo payment method +3. Create Kredivo payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through Kredivo popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- Kredivo one-time payment integration +- Full name and email field validation +- Popup payment flow with Kredivo authentication +- Eligibility checking for Kredivo +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable Kredivo Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **Kredivo** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the Kredivo demo directory:** + + ```bash + cd client/components/kredivoPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +Kredivo is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with Kredivo components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies Kredivo is eligible for the merchant with IDR currency +3. **Session Creation**: Creates Kredivo payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with Kredivo and select their preferred installment plan. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### Kredivo not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure Kredivo is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for Kredivo + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/kredivoPayments/html/src/app.js b/client/components/kredivoPayments/html/src/app.js new file mode 100644 index 00000000..160d0223 --- /dev/null +++ b/client/components/kredivoPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Kredivo testing + components: ["kredivo-payments"], + }); + + // Check if Kredivo is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isKredivoEligible = paymentMethods.isEligible("kredivo"); + + if (isKredivoEligible) { + setupKredivoPayment(sdkInstance); + } else { + showMessage({ + text: "Kredivo is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Kredivo is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupKredivoPayment(sdkInstance) { + try { + // Create Kredivo checkout session + const kredivoCheckout = sdkInstance.createKredivoOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(kredivoCheckout); + + // Setup button click handler + setupButtonHandler(kredivoCheckout); + } catch (error) { + console.error("Error setting up Kredivo payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(kredivoCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = kredivoCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = kredivoCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#kredivo-full-name").appendChild(fullNameField); + document.querySelector("#kredivo-email").appendChild(emailField); +} + +function setupButtonHandler(kredivoCheckout) { + const kredivoButton = document.querySelector("#kredivo-button"); + kredivoButton.removeAttribute("hidden"); + + kredivoButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await kredivoCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await kredivoCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/kredivoPayments/html/src/index.html b/client/components/kredivoPayments/html/src/index.html new file mode 100644 index 00000000..f1eaedd5 --- /dev/null +++ b/client/components/kredivoPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Kredivo - PayPal Web SDK + + + + + +

    Kredivo One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/linkajaPayments/html/README.md b/client/components/linkajaPayments/html/README.md new file mode 100644 index 00000000..3a5298f6 --- /dev/null +++ b/client/components/linkajaPayments/html/README.md @@ -0,0 +1,173 @@ +# LinkAja One-Time Payment Integration + +This example demonstrates how to integrate LinkAja payments using PayPal's v6 Web SDK. LinkAja is a popular e-wallet and mobile payment service in Indonesia that allows customers to make secure digital payments using their LinkAja account balance. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete LinkAja integration flow: + +1. Initialize PayPal Web SDK with the LinkAja component +2. Check eligibility for LinkAja payment method +3. Create LinkAja payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through LinkAja popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- LinkAja one-time payment integration +- Full name and email field validation +- Popup payment flow with LinkAja authentication +- Eligibility checking for LinkAja +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable LinkAja Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **LinkAja** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the LinkAja demo directory:** + + ```bash + cd client/components/linkajaPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +LinkAja is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with LinkAja components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies LinkAja is eligible for the merchant with IDR currency +3. **Session Creation**: Creates LinkAja payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with their LinkAja account and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### LinkAja not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure LinkAja is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for LinkAja + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/linkajaPayments/html/src/app.js b/client/components/linkajaPayments/html/src/app.js new file mode 100644 index 00000000..8740dc8d --- /dev/null +++ b/client/components/linkajaPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Linkaja testing + components: ["linkaja-payments"], + }); + + // Check if Linkaja is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isLinkajaEligible = paymentMethods.isEligible("linkaja"); + + if (isLinkajaEligible) { + setupLinkajaPayment(sdkInstance); + } else { + showMessage({ + text: "Linkaja is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Linkaja is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupLinkajaPayment(sdkInstance) { + try { + // Create Linkaja checkout session + const linkajaCheckout = sdkInstance.createLinkajaOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(linkajaCheckout); + + // Setup button click handler + setupButtonHandler(linkajaCheckout); + } catch (error) { + console.error("Error setting up Linkaja payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(linkajaCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = linkajaCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = linkajaCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#linkaja-full-name").appendChild(fullNameField); + document.querySelector("#linkaja-email").appendChild(emailField); +} + +function setupButtonHandler(linkajaCheckout) { + const linkajaButton = document.querySelector("#linkaja-button"); + linkajaButton.removeAttribute("hidden"); + + linkajaButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await linkajaCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await linkajaCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/linkajaPayments/html/src/index.html b/client/components/linkajaPayments/html/src/index.html new file mode 100644 index 00000000..806cf1f7 --- /dev/null +++ b/client/components/linkajaPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Linkaja - PayPal Web SDK + + + + + +

    Linkaja One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/multibancoPayments/html/src/app.js b/client/components/multibancoPayments/html/src/app.js index 84e7de91..d76656f5 100644 --- a/client/components/multibancoPayments/html/src/app.js +++ b/client/components/multibancoPayments/html/src/app.js @@ -150,29 +150,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -182,17 +182,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/mybankPayments/html/src/app.js b/client/components/mybankPayments/html/src/app.js index 1e7aa7ae..56f0763e 100644 --- a/client/components/mybankPayments/html/src/app.js +++ b/client/components/mybankPayments/html/src/app.js @@ -150,29 +150,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -182,17 +182,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/ovoPayments/html/README.md b/client/components/ovoPayments/html/README.md new file mode 100644 index 00000000..ddd15d36 --- /dev/null +++ b/client/components/ovoPayments/html/README.md @@ -0,0 +1,173 @@ +# OVO One-Time Payment Integration + +This example demonstrates how to integrate OVO payments using PayPal's v6 Web SDK. OVO is one of Indonesia's most popular e-wallet services, enabling customers to make secure payments using their OVO account balance or linked payment methods. + +## πŸ—οΈ Architecture Overview + +This sample demonstrates a complete OVO integration flow: + +1. Initialize PayPal Web SDK with the OVO component +2. Check eligibility for OVO payment method +3. Create OVO payment session with required payment fields +4. Validate customer information before initiating payment +5. Authorize the payment through OVO popup flow +6. Handle payment approval, cancellation, and errors + +## Features + +- OVO one-time payment integration +- Full name and email field validation +- Popup payment flow with OVO authentication +- Eligibility checking for OVO +- Error handling and user feedback +- IDR (Indonesian Rupiah) currency support + +## πŸ“‹ Prerequisites + +Before running this demo, you'll need to set up accounts and configure your development environment. + +### 1. PayPal Developer Account Setup + +1. **PayPal Developer Account** + - Visit [developer.paypal.com](https://developer.paypal.com) + - Sign up for a developer account or log in with existing credentials + - Navigate to the **Apps & Credentials** section in your dashboard + +2. **Create a PayPal Application** (or configure the default application) + - Click **Create App** + - Name your app + - Select **Merchant** under **Type** + - Choose the **Sandbox** account for testing + - Click **Create App** at the bottom of the modal + - Note your **Client ID** and **Secret key** under **API credentials** for later configuration of the `.env` file + +3. **Enable OVO Payment** + - Visit [sandbox.paypal.com](https://www.sandbox.paypal.com) + - Log in with your **Sandbox** merchant account credentials + - Navigate to **Account Settings** by clicking the profile icon in the top right corner + - Select **Payment methods** from the left sidebar + - Find **OVO** in the payment methods and enable it + - Ensure your account is configured to accept **IDR (Indonesian Rupiah)** currency + +### 2. System Requirements + +- Node.js version 20 or higher +- Server running on port 8080 (see `server/node/` directory) + +## πŸš€ Running the Demo + +### 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 using your client credentials from the previous Create Application step: + + ```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 `http://localhost:8080` + +### Client Setup + +1. **Navigate to the OVO demo directory:** + + ```bash + cd client/components/ovoPayments/html + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + ```bash + npm start + ``` + The demo will be available at `http://localhost:3000` + +## How It Works + +### Geographic Availability + +OVO is available in Indonesia + +### Client-Side Flow + +1. **SDK Initialization**: Loads PayPal Web SDK with OVO components using a client token fetched from the server's `/paypal-api/auth/browser-safe-client-token` endpoint +2. **Eligibility Check**: Verifies OVO is eligible for the merchant with IDR currency +3. **Session Creation**: Creates OVO payment session with event callbacks for handling payment lifecycle events +4. **Field Setup**: Mounts the required full name and email fields +5. **Validation**: Validates both fields before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers authenticate with their OVO account and authorize the payment. The order is created server-side via `/paypal-api/checkout/orders/create` before displaying the popup +7. **Completion**: Processes the payment result by capturing the approved order via `/paypal-api/checkout/orders/:orderId/capture`, or handles cancellation and error scenarios appropriately + +### Server-Side Requirements + +The integration requires these endpoints (provided by the API server): + +- `GET /paypal-api/auth/browser-safe-client-token` - Generate client token +- `POST /paypal-api/checkout/orders/create` - Create order (must use IDR currency) +- `POST /paypal-api/checkout/orders/:orderId/capture` - Capture order + +## Available Scripts + +- `npm start` - Start Vite development server (port 3000) +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting + +## Troubleshooting + +### OVO not eligible + +- Verify `testBuyerCountry` is set to "ID" +- Check that `currencyCode` is set to "IDR" (Indonesian Rupiah) +- Ensure OVO is enabled for the merchant in PayPal account settings +- Verify your PayPal account is configured to accept IDR currency + +### Validation fails + +- Ensure both full name and email fields are properly mounted with javascript +- Check that both fields have valid input +- Verify email format is correct +- Ensure fields are visible in the DOM + +### Payment popup doesn't open + +- Check for popup blockers +- Verify order creation returns valid orderId +- Ensure proper event handler setup +- Check browser console for errors +- Verify order is created with IDR currency + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- **Ensure currency_code is set to "IDR"** - this is critical for OVO + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/ovoPayments/html/src/app.js b/client/components/ovoPayments/html/src/app.js new file mode 100644 index 00000000..82acaa29 --- /dev/null +++ b/client/components/ovoPayments/html/src/app.js @@ -0,0 +1,251 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientId = await getBrowserSafeClientId(); + const sdkInstance = await window.paypal.createInstance({ + clientId, + testBuyerCountry: "ID", // Indonesia for Ovo testing + components: ["ovo-payments"], + }); + + // Check if Ovo is eligible + const paymentMethods = await sdkInstance.findEligibleMethods({ + currencyCode: "IDR", + }); + + const isOvoEligible = paymentMethods.isEligible("ovo"); + + if (isOvoEligible) { + setupOvoPayment(sdkInstance); + } else { + showMessage({ + text: "Ovo is not eligible. Please ensure your buyer country is Indonesia and currency is IDR.", + type: "error", + }); + console.error("Ovo is not eligible"); + } + } catch (error) { + console.error("Error initializing PayPal SDK:", error); + showMessage({ + text: "Failed to initialize payment system. Please try again.", + type: "error", + }); + } +} + +function setupOvoPayment(sdkInstance) { + try { + // Create Ovo checkout session + const ovoCheckout = sdkInstance.createOvoOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // Setup payment fields + setupPaymentFields(ovoCheckout); + + // Setup button click handler + setupButtonHandler(ovoCheckout); + } catch (error) { + console.error("Error setting up Ovo payment:", error); + showMessage({ + text: "Failed to setup payment. Please refresh the page.", + type: "error", + }); + } +} + +function setupPaymentFields(ovoCheckout) { + // Create payment field for full name with optional prefill + const fullNameField = ovoCheckout.createPaymentFields({ + type: "name", + value: "", // Optional prefill value + style: { + // Optional styling to match your website + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Create email field + const emailField = ovoCheckout.createPaymentFields({ + type: "email", + value: "", + style: { + variables: { + textColor: "#333333", + colorTextPlaceholder: "#999999", + fontFamily: "Verdana, sans-serif", + fontSizeBase: "14px", + }, + }, + }); + + // Mount the fields to the containers + document.querySelector("#ovo-full-name").appendChild(fullNameField); + document.querySelector("#ovo-email").appendChild(emailField); +} + +function setupButtonHandler(ovoCheckout) { + const ovoButton = document.querySelector("#ovo-button"); + ovoButton.removeAttribute("hidden"); + + ovoButton.addEventListener("click", async () => { + try { + console.log("Validating payment fields..."); + + // Validate the payment fields + const isValid = await ovoCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Start payment flow with popup mode + await ovoCheckout.start({ presentationMode: "popup" }, createOrder()); + } else { + console.error("Validation failed"); + showMessage({ + text: "Please fill in all required fields correctly.", + type: "error", + }); + } + } catch (error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again.", + type: "error", + }); + } + }); +} + +// Create PayPal order +async function createOrder() { + try { + console.log("Creating PayPal order..."); + + const response = await fetch( + "/paypal-api/checkout/orders/create-order-for-one-time-payment", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currencyCode: "IDR" }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to create order"); + } + + const { id } = await response.json(); + console.log("Order created with ID:", id); + + return { orderId: id }; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Get order details after approval +async function getOrder(orderId) { + try { + console.log("Fetching order details:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch order details"); + } + + const data = await response.json(); + console.log("Order details fetched successfully:", data); + + return data; + } catch (error) { + console.error("Error fetching order details:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, + type: "success", + }); + } catch (error) { + console.error("Failed to fetch order details:", error); + showMessage({ + text: "Transaction successful but failed to fetch order details.", + type: "error", + }); + } +} + +// Handle payment cancellation +function handleCancel(data) { + console.log("Payment cancelled:", data); + showMessage({ + text: "Payment was cancelled. You can try again.", + type: "error", + }); +} + +// Handle payment errors +function handleError(error) { + console.error("Payment error:", error); + showMessage({ + text: "An error occurred during payment. Please try again or contact support.", + type: "error", + }); +} + +// Get client id from server +async function getBrowserSafeClientId() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-id", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client id"); + } + + const { clientId } = await response.json(); + return clientId; + } catch (error) { + console.error("Error fetching client id:", error); + throw error; + } +} + +// Utility function to show messages to user +function showMessage({ text, type }) { + const messageEl = document.getElementById("message"); + messageEl.textContent = text; + messageEl.className = `message ${type} show`; +} diff --git a/client/components/ovoPayments/html/src/index.html b/client/components/ovoPayments/html/src/index.html new file mode 100644 index 00000000..c8cf44ed --- /dev/null +++ b/client/components/ovoPayments/html/src/index.html @@ -0,0 +1,68 @@ + + + + + Ovo - PayPal Web SDK + + + + + +

    Ovo One-Time Payment Integration

    + +
    +
    +
    + +
    + +
    + + + + + diff --git a/client/components/swishPayments/html/src/app.js b/client/components/swishPayments/html/src/app.js index ad0bad34..9394627f 100644 --- a/client/components/swishPayments/html/src/app.js +++ b/client/components/swishPayments/html/src/app.js @@ -140,29 +140,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -172,17 +172,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/trustlyPayments/html/src/app.js b/client/components/trustlyPayments/html/src/app.js index a972a26b..a567ef47 100644 --- a/client/components/trustlyPayments/html/src/app.js +++ b/client/components/trustlyPayments/html/src/app.js @@ -155,29 +155,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -187,17 +187,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/twintPayments/html/src/app.js b/client/components/twintPayments/html/src/app.js index c2a91f6b..acb0ab3b 100644 --- a/client/components/twintPayments/html/src/app.js +++ b/client/components/twintPayments/html/src/app.js @@ -140,29 +140,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -172,17 +172,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/verkkopankkiPayments/html/src/app.js b/client/components/verkkopankkiPayments/html/src/app.js index e25f83db..443831f5 100644 --- a/client/components/verkkopankkiPayments/html/src/app.js +++ b/client/components/verkkopankkiPayments/html/src/app.js @@ -166,29 +166,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -198,17 +198,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/wechatpayPayments/html/src/app.js b/client/components/wechatpayPayments/html/src/app.js index 43317a92..e676200d 100644 --- a/client/components/wechatpayPayments/html/src/app.js +++ b/client/components/wechatpayPayments/html/src/app.js @@ -140,29 +140,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -172,17 +172,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } diff --git a/client/components/zipPayments/html/src/app.js b/client/components/zipPayments/html/src/app.js index ce102423..21bdf553 100644 --- a/client/components/zipPayments/html/src/app.js +++ b/client/components/zipPayments/html/src/app.js @@ -150,29 +150,29 @@ async function createOrder() { } } -// Capture order after approval -async function captureOrder(orderId) { +// Get order details after approval +async function getOrder(orderId) { try { - console.log("Capturing order:", orderId); + console.log("Fetching order details:", orderId); const response = await fetch( - `/paypal-api/checkout/orders/${orderId}/capture`, + `/paypal-api/checkout/orders/${orderId}`, { - method: "POST", + method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { - throw new Error("Failed to capture order"); + throw new Error("Failed to fetch order details"); } const data = await response.json(); - console.log("Order captured successfully:", data); + console.log("Order details fetched successfully:", data); return data; } catch (error) { - console.error("Error capturing order:", error); + console.error("Error fetching order details:", error); throw error; } } @@ -182,17 +182,17 @@ async function handleApprove(data) { console.log("Payment approved:", data); try { - const result = await captureOrder(data.orderId); - console.log("Capture successful:", result); + const orderDetails = await getOrder(data.orderId); + console.log("Order details:", orderDetails); showMessage({ - text: `Payment successful! Order ID: ${data.orderId}. Check console for details.`, + text: `Payment successful! Order ID: ${data.orderId}. Check console for order details.`, type: "success", }); } catch (error) { - console.error("Capture failed:", error); + console.error("Failed to fetch order details:", error); showMessage({ - text: "Payment approved but capture failed.", + text: "Transaction successful but failed to fetch order details.", type: "error", }); } From 64f6ff91f33040af2f4ed3483a3df01c9e07f9ca Mon Sep 17 00:00:00 2001 From: Reegan1819 Date: Mon, 2 Mar 2026 15:56:10 +0530 Subject: [PATCH 7/7] Adding apm html --- client/index.html | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/client/index.html b/client/index.html index 5490ad74..58b5ccdf 100644 --- a/client/index.html +++ b/client/index.html @@ -262,6 +262,216 @@

    Static Examples

    +
  • + PayU Payments + +
  • +
  • + Klarna Payments + +
  • +
  • + Swish Payments + +
  • +
  • + TWINT Payments + +
  • +
  • + Bizum Payments + +
  • +
  • + Trustly Payments + +
  • +
  • + MyBank Payments + +
  • +
  • + Multibanco Payments + +
  • +
  • + Verkkopankki Payments + +
  • +
  • + Afterpay Payments + +
  • +
  • + Zip Payments + +
  • +
  • + WeChat Pay Payments + +
  • +
  • + Kredivo Payments + +
  • +
  • + Alfamart Payments + +
  • +
  • + OVO Payments + +
  • +
  • + Doku Payments + +
  • +
  • + LinkAja Payments + +
  • +
  • + JeniusPay Payments + +
  • +
  • + Indomaret Payments + +
  • +
  • + Indonesia Banks Payments + +
  • +
  • + Gopay Payments + +
  • Dynamic Examples