From 44cee3003369375a3f4fc909cf6057e01599db47 Mon Sep 17 00:00:00 2001 From: rnedium Date: Thu, 30 Oct 2025 11:43:21 +0530 Subject: [PATCH] Add Klarna Examples --- .../components/klarnaPayments/html/README.md | 280 ++++++++++++++++ .../klarnaPayments/html/package.json | 19 ++ .../components/klarnaPayments/html/src/app.js | 310 ++++++++++++++++++ .../klarnaPayments/html/src/index.html | 146 +++++++++ .../klarnaPayments/html/vite.config.js | 18 + 5 files changed, 773 insertions(+) create mode 100644 client/components/klarnaPayments/html/README.md create mode 100644 client/components/klarnaPayments/html/package.json 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/klarnaPayments/html/vite.config.js diff --git a/client/components/klarnaPayments/html/README.md b/client/components/klarnaPayments/html/README.md new file mode 100644 index 0000000..60ce5ea --- /dev/null +++ b/client/components/klarnaPayments/html/README.md @@ -0,0 +1,280 @@ +# Klarna One-Time Payment Integration + +This example demonstrates how to integrate Klarna payments using PayPal's v6 Web SDK. Klarna is a popular "buy now, pay later" payment method that allows customers to pay over time with 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. Collect billing address and phone information +6. Process payment through Klarna popup flow +7. Handle payment approval, cancellation, and errors +8. Authorize the order after approval + +## Features + +- Klarna one-time payment integration +- Full name and email field validation +- Billing address collection +- Phone number collection +- Popup payment flow +- Eligibility checking for Klarna +- Order authorization (not capture) +- 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. **Session Creation**: Creates Klarna payment session with event callbacks for handling payment lifecycle events +3. **Field Setup**: Mounts the required full name and email fields +4. **Billing Data Collection**: Collects billing address and phone information from HTML form +5. **Validation**: Validates fields and billing data before initiating the payment flow +6. **Payment Flow**: Opens a popup window where customers 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 authorizing the approved order via `/paypal-api/checkout/orders/:orderId/authorize`, 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 with AUTHORIZE intent +- `POST /paypal-api/checkout/orders/:orderId/authorize` - Authorize order + +## Key Integration Points + +### Payment Intent + +Unlike most other payment methods, Klarna uses **AUTHORIZE** intent instead of CAPTURE: + +- Orders are created with `intent: "AUTHORIZE"` +- After approval, orders are authorized (not captured) +- Authorizations must be captured separately within 29 days +- Include `processingInstruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL"` in order payload + +### Required Fields + +Klarna requires two payment fields: + +1. **Full Name** - Customer's full name +2. **Email** - Customer's email address + +### Billing Address Requirements + +Klarna requires complete billing address information: + +```javascript +{ + addressLine1: "123 Main St", // Required + adminArea1: "London", // State/Province - Required + adminArea2: "Greater London", // City/Region - Required + postalCode: "SW1A 1AA", // Postal/ZIP code - Required + countryCode: "GB" // ISO country code - Required +} +``` + +### Phone Requirements + +Klarna requires phone information: + +```javascript +{ + countryCode: "44", // Country code - Required + nationalNumber: "7123456789" // Phone number - Required +} +``` + +### Order Payload + +The order payload must include: + +- `intent`: Set to `"AUTHORIZE"` (not CAPTURE) +- `processingInstruction`: Set to `"ORDER_COMPLETE_ON_PAYMENT_APPROVAL"` +- `currency_code`: Must match the buyer country (e.g., "GBP" for "GB") + +Example: + +```javascript +const orderPayload = { + intent: "AUTHORIZE", + purchaseUnits: [ + { + amount: { + currencyCode: "GBP", + value: "100.00", + }, + }, + ], + processingInstruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", +}; +``` + +## 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 (e.g., "GB", "NL") +- Check that Klarna is enabled for the merchant in PayPal account settings +- Ensure currency code matches the country (e.g., "GBP" for "GB") + +### Validation fails + +- Ensure full name and email fields are properly mounted with javascript +- Check that fields have valid input +- Verify field is visible in the DOM +- Verify billing address form is complete + +### 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 +- Confirm billing data is correctly formatted + +### Order creation fails + +- Verify API server is running on port 8080 +- Check server logs for errors +- Validate order payload format +- Ensure intent is set to "AUTHORIZE" (not "CAPTURE") +- Verify processingInstruction is included + +### Authorization fails + +- Check that order was created successfully +- Verify billing address and phone data are complete +- Ensure customer completed the Klarna flow +- Check that all required billing fields are provided + +## Supported Countries and Currencies + +Klarna is available in the following countries: + +- **AT (Austria)**: EUR +- **AU (Australia)**: AUD +- **BE (Belgium)**: EUR +- **CA (Canada)**: CAD +- **CH (Switzerland)**: CHF +- **CZ (Czech Republic)**: CZK +- **DE (Germany)**: EUR +- **DK (Denmark)**: DKK +- **ES (Spain)**: EUR +- **FI (Finland)**: EUR +- **FR (France)**: EUR +- **GB (United Kingdom)**: GBP +- **GR (Greece)**: EUR +- **HU (Hungary)**: HUF +- **IE (Ireland)**: EUR +- **IT (Italy)**: EUR +- **MX (Mexico)**: MXN +- **NL (Netherlands)**: EUR +- **NO (Norway)**: NOK +- **NZ (New Zealand)**: NZD +- **PL (Poland)**: PLN +- **PT (Portugal)**: EUR +- **RO (Romania)**: RON +- **SE (Sweden)**: SEK +- **SK (Slovakia)**: EUR +- **US (United States)**: USD + +Ensure your `testBuyerCountry` and `currencyCode` match appropriately. + +## Documentation + +- [PayPal Developer Documentation](https://developer.paypal.com/docs/) +- [PayPal Developer Community](https://developer.paypal.com/community/) diff --git a/client/components/klarnaPayments/html/package.json b/client/components/klarnaPayments/html/package.json new file mode 100644 index 0000000..6bf1fb1 --- /dev/null +++ b/client/components/klarnaPayments/html/package.json @@ -0,0 +1,19 @@ +{ + "name": "v6-web-sdk-sample-integration-client-klarna", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview", + "start": "vite", + "format": "prettier . --write", + "format:check": "prettier . --check", + "lint": "echo \"eslint is not set up\"" + }, + "license": "Apache-2.0", + "devDependencies": { + "prettier": "^3.6.2", + "vite": "^7.0.4" + } +} diff --git a/client/components/klarnaPayments/html/src/app.js b/client/components/klarnaPayments/html/src/app.js new file mode 100644 index 0000000..73dd90e --- /dev/null +++ b/client/components/klarnaPayments/html/src/app.js @@ -0,0 +1,310 @@ +async function onPayPalWebSdkLoaded() { + try { + const clientToken = await getBrowserSafeClientToken(); + const sdkInstance = await window.paypal.createInstance({ + clientToken, + testBuyerCountry: "GB", // United Kingdom 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 and currency are supported.", + 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", + }); + } +} + +function setupKlarnaPayment(sdkInstance) { + try { + // Create Klarna checkout session + const klarnaCheckout = sdkInstance.createKlarnaOneTimePaymentSession({ + onApprove: handleApprove, + onCancel: handleCancel, + onError: handleError, + }); + + // 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", + }, + }, + }); + + // 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 the payment fields + const isValid = await klarnaCheckout.validate(); + + if (isValid) { + console.log("Validation successful, starting payment flow..."); + + // Collect billing address data from form + const billingData = { + orderId: await createOrderId(), + billingAddress: { + addressLine1: document.getElementById("address-line1").value, + adminArea1: document.getElementById("admin-area1").value, + adminArea2: document.getElementById("admin-area2").value, + postalCode: document.getElementById("postal-code").value, + countryCode: document.getElementById("country-code").value, + }, + phone: { + countryCode: document.getElementById("phone-country-code").value, + nationalNumber: document.getElementById("phone-national-number") + .value, + }, + }; + + // Validate billing address + if (!validateBillingAddress(billingData)) { + showMessage({ + text: "Please fill in all billing address fields correctly.", + type: "error", + }); + return; + } + + // Start payment flow with popup mode + await klarnaCheckout.start( + { presentationMode: "popup" }, + Promise.resolve(billingData), + ); + } 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", + }); + } + }); +} + +// Validate billing address data +function validateBillingAddress(billingData) { + const { billingAddress, phone } = billingData; + + if ( + !billingAddress.addressLine1 || + !billingAddress.adminArea1 || + !billingAddress.adminArea2 || + !billingAddress.postalCode || + !billingAddress.countryCode || + !phone.countryCode || + !phone.nationalNumber + ) { + return false; + } + + return true; +} + +// Create PayPal order and return order ID +async function createOrderId() { + try { + console.log("Creating PayPal order..."); + + const orderPayload = { + intent: "AUTHORIZE", + purchaseUnits: [ + { + amount: { + currencyCode: "GBP", + value: "100.00", + }, + }, + ], + processingInstruction: "ORDER_COMPLETE_ON_PAYMENT_APPROVAL", + }; + + const response = await fetch("/paypal-api/checkout/orders/create", { + 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 id; + } catch (error) { + console.error("Error creating order:", error); + showMessage({ + text: "Failed to create order. Please try again.", + type: "error", + }); + throw error; + } +} + +// Authorize order after approval +async function authorizeOrder(orderId) { + try { + console.log("Authorizing order:", orderId); + + const response = await fetch( + `/paypal-api/checkout/orders/${orderId}/authorize`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to authorize order"); + } + + const data = await response.json(); + console.log("Order authorized successfully:", data); + + return data; + } catch (error) { + console.error("Error authorizing order:", error); + throw error; + } +} + +// Handle successful payment approval +async function handleApprove(data) { + console.log("Payment approved:", data); + + try { + const result = await authorizeOrder(data.orderId); + console.log("Authorization successful:", result); + + showMessage({ + text: `Payment successful! Order ID: ${data.orderId}. Authorization ID: ${result.purchase_units?.[0]?.payments?.authorizations?.[0]?.id || "N/A"}. Check console for details.`, + type: "success", + }); + } catch (error) { + console.error("Authorization failed:", error); + showMessage({ + text: "Payment approved but authorization 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", + }); +} + +// Get client token from server +async function getBrowserSafeClientToken() { + try { + const response = await fetch("/paypal-api/auth/browser-safe-client-token", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch client token"); + } + + const { accessToken } = await response.json(); + return accessToken; + } catch (error) { + console.error("Error fetching client token:", 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/klarnaPayments/html/src/index.html b/client/components/klarnaPayments/html/src/index.html new file mode 100644 index 0000000..a77c604 --- /dev/null +++ b/client/components/klarnaPayments/html/src/index.html @@ -0,0 +1,146 @@ + + + + + Klarna - PayPal Web SDK + + + + +

Klarna One-Time Payment Integration

+ +
+ +
+ + +
+ + +
+

Billing Address

+ + + + + + + +
+ + +
+ +
+ + + + + diff --git a/client/components/klarnaPayments/html/vite.config.js b/client/components/klarnaPayments/html/vite.config.js new file mode 100644 index 0000000..651550b --- /dev/null +++ b/client/components/klarnaPayments/html/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + root: "src", + server: { + port: 3000, + host: true, + proxy: { + "/paypal-api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + }, + }, +});