diff --git a/README.md b/README.md index 0c7daf49..ba473443 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,318 @@ -[![N|Solid](https://cdn.checkout.com/img/checkout-logo-online-payments.jpg)](https://checkout.com/) +# Checkout.com WooCommerce Plugin - Flow Integration -# Woocommerce Extension -[Checkout.com](https://www.checkout.com "Checkout.com") is a software platform that has integrated 100% of the value chain to create payment infrastructures that truly make a difference. +Checkout.com Payment Gateway plugin for WooCommerce with Flow integration support. -This extension allows shop owners to process online payments (card / alternative payments) using: - - **Frames.js** - Customisable payment form, embedded within your website - - **Apple Pay & Google Pay** - Shoppers can pay using mobile wallets - - **Alternative payments** - Shoppers can pay using local payment options (Sofort, iDEAL, Boleto ... etc.) +## Version -# Installation -You can find a full installation guide [here](https://github.com/checkout/checkout-woocommerce-plugin/wiki/Installation) +**Current Version:** 5.0.0 -# Initial Setup -If you do not have an account yet, simply go to [checkout.com](https://checkout.com/) and hit the "Get Test Account" button. +## Features -# Keys -There are 3 keys to configure in the module: -- **Secret Key** -- **Public Key** -- **Private Shared Key** (not required if using v4.2.0+ of our WooCommerce plugin) +### Flow Integration +- **Checkout.com Flow** - Modern, secure payment processing using Checkout.com's Flow Web Components +- **Saved Cards** - Customers can save payment methods for future use +- **3D Secure (3DS)** - Full support for 3D Secure authentication +- **Card Validation** - Real-time card validation before order creation +- **Webhook Processing** - Reliable webhook handling with queue system +- **Order Management** - Automatic order status updates based on payment status -> The Private Shared Key is generated when you [configure the Webhook URL](https://docs.checkout.com/the-hub/manage-webhooks) in the Checkout HUB. +### Payment Methods Supported +- Credit/Debit Cards (via Flow) +- Saved Payment Methods +- Apple Pay +- Google Pay +- PayPal +- Alternative Payment Methods (APMs) -# Webhook -In order to keep WooCommerce order statuses in sync you need to configure the following webhook URL in your Checkout HUB (where _example.com_ is your store URL): +## Installation -> The following URL format is for plugins versions 4.X or newer; click [here](https://github.com/checkout/checkout-woocommerce-plugin/wiki/URLs--2.x) to get the URLs for older plugin versions +1. Download the plugin zip file: `checkout-com-unified-payments-api.zip` +2. Go to WordPress Admin → Plugins → Add New → Upload Plugin +3. Upload the zip file +4. Activate the plugin +5. Configure your Checkout.com credentials in WooCommerce → Settings → Payments → Checkout.com Payment +## Configuration -| URL Example | API Version | Events | -| ------ | ------ | ------ | -| _example.com_**/?wc-api=wc_checkoutcom_webhook**           | 2.0 | All | +### Required Settings -> You can see a guide on how to manage webhooks in the HUB [here](https://docs.checkout.com/the-hub/manage-webhooks) ; You can find test card details [here](https://docs.checkout.com/testing) +1. **Secret Key** - Your Checkout.com secret key +2. **Public Key** - Your Checkout.com public key +3. **Webhook URL** - Configure in Checkout.com Hub: + ``` + https://your-site.com/?wc-api=wc_checkoutcom_webhook + ``` -# Going LIVE +### Flow Integration Settings -Upon receiving your live credentials from your account manager, here are the required steps to start processing live transactions: +- Enable Flow payment method +- Configure Flow appearance and behavior +- Set up saved cards functionality +- Configure 3DS settings -- In the plugin settings, input your **Live** keys -- Switch the _Endpoint URL mode_ to **Live**. +## Flow Integration Details -# Development environment Setup +### Overview -- Clone the repository to in `wp-content/plugins` folder with the name `woocommerce-gateway-checkout-com` - `git clone git@github.com:checkout/checkout-woocommerce-plugin.git woocommerce-gateway-checkout-com` - or - `git clone https://github.com/checkout/checkout-woocommerce-plugin.git woocommerce-gateway-checkout-com` -- Use required `npm` version by executing `nvm use`. -- Install the `npm` packages using `npm install`. -- Install the `composer` packages using `composer install`. -- During pushing the committing the code the PHPCS check will run at pre-commit hook. **So, till the PHPCS errors are not fixed for during commit we have to use `--no-verify` option in the git commit command.** +The Flow integration provides a modern, secure payment experience using Checkout.com's Flow Web Components. This integration ensures reliable payment processing with comprehensive validation, webhook handling, and order management. +### How Flow Integration Works -# Reference +The payment flow follows these steps: -You can find our complete Documentation [here](http://docs.checkout.com/). -If you would like to be assigned an account manager, please contact us at sales@checkout.com -For help during the integration process you can contact us at integration@checkout.com -For support, you can contact us at support@checkout.com +#### Step 1: Checkout Page Load +- Customer fills out billing and shipping information +- Flow payment method is selected +- Flow Web Component is initialized and mounted -_Checkout.com is authorised and regulated as a Payment institution by the UK Financial Conduct Authority._ +#### Step 2: Order Creation (Before Payment) +- **Why Early?** Orders are created via AJAX before payment processing begins +- This ensures the order exists in the database for webhook matching +- Order status: `Pending payment` +- Payment session ID is stored with the order + +#### Step 3: Payment Session Creation +- Payment session is created with Checkout.com API +- Session includes order details, customer information, and amount +- Payment session ID is returned and stored + +#### Step 4: Flow Component Validation +- **Client-Side Validation**: Flow component validates card details in real-time +- Card number, expiry, CVV are validated before submission +- Invalid cards are rejected before payment attempt + +#### Step 5: Payment Processing +- Customer submits payment through Flow component +- Payment is processed securely through Checkout.com +- For 3D Secure: Customer is redirected for authentication +- Payment result is returned + +#### Step 6: Webhook Processing +- Checkout.com sends webhook with payment status +- Webhook is matched to order using: + 1. Order ID from metadata (primary) + 2. Payment Session ID + Payment ID (secondary) + 3. Payment ID alone (fallback) +- If order not found immediately, webhook is queued for later processing + +#### Step 7: Order Status Update +- Order status is automatically updated based on payment result: + - ✅ **Payment Approved** → Order status: `Processing` + - ✅ **Payment Captured** → Order status: `Processing` (if not already) + - ❌ **Payment Declined** → Order status: `Failed` + - ⏸️ **Payment Cancelled** → Order status: `Cancelled` + +### Key Features Explained + +#### 🔒 Early Order Creation +**What it does:** Creates the WooCommerce order before payment processing begins. + +**Why it's important:** +- Ensures webhooks can always find the order +- Prevents webhook matching failures +- Allows order tracking throughout the payment process + +**How it works:** +- Order is created via AJAX when customer clicks "Place Order" +- Order is saved with `Pending payment` status +- Payment session ID is stored for webhook matching + +#### ✅ Dual Validation System +**Client-Side Validation:** +- Flow component validates card details in real-time +- Prevents invalid cards from being submitted +- Provides instant feedback to customers + +**Server-Side Validation:** +- Comprehensive validation of all checkout fields +- Validates billing/shipping addresses +- Ensures data integrity before order creation +- Blocks order creation if validation fails + +#### 🚫 Duplicate Prevention +**Problem:** Multiple clicks or slow networks can cause duplicate orders. + +**Solution:** +- Client-side lock prevents multiple simultaneous requests +- Server-side check prevents duplicate orders with same payment session ID +- If duplicate detected, existing order is returned instead of creating new one + +#### 📬 Webhook Queue System +**Problem:** Webhooks might arrive before order is fully saved to database. + +**Solution:** +- Webhook queue temporarily stores webhooks if order not found +- Queue is processed when order becomes available +- Ensures no webhooks are lost +- Automatic retry mechanism + +#### 🔐 3D Secure (3DS) Support +**How it works:** +1. Payment requires 3DS authentication +2. Customer is redirected to bank's 3DS page +3. Customer completes authentication +4. Customer is redirected back to store +5. Payment status is confirmed via webhook +6. Order status is updated accordingly + +**Features:** +- Automatic 3DS detection +- Seamless redirect flow +- Webhook handling after 3DS return +- Prevents duplicate status updates + +#### 💳 Saved Cards +**How it works:** +1. Customer opts to save card during checkout +2. Card is tokenized securely by Checkout.com +3. Token is stored in customer's account +4. Saved cards appear on future checkouts +5. Customer can select saved card for quick checkout + +**Security:** +- Cards are never stored on your server +- Only secure tokens are stored +- PCI compliance handled by Checkout.com +- Cards can be deleted by customer + +### Payment Flow Diagram + +``` +Customer Checkout + ↓ +Fill Billing/Shipping Info + ↓ +Select Flow Payment Method + ↓ +Click "Place Order" + ↓ +[VALIDATION] Client-Side + Server-Side + ↓ +[ORDER CREATED] Status: Pending payment + ↓ +Create Payment Session with Checkout.com + ↓ +[FLOW COMPONENT] Card Details Entered + ↓ +[VALIDATION] Flow Component Validates Card + ↓ +Submit Payment + ↓ +[3DS?] If Required → Redirect → Authenticate → Return + ↓ +Payment Processed + ↓ +[WEBHOOK] Payment Status Received + ↓ +Match Webhook to Order + ↓ +Update Order Status + ↓ +[COMPLETE] Order Status: Processing/Failed +``` + +### Technical Architecture + +#### Frontend (JavaScript) +- **payment-session.js**: Handles order creation, payment session, Flow component integration +- **flow-container.js**: Manages Flow component container and initialization +- **flow-customization.js**: Customizes Flow component appearance and behavior + +#### Backend (PHP) +- **class-wc-gateway-checkout-com-flow.php**: Main gateway class, handles payment processing +- **Webhook Handler**: Processes incoming webhooks and updates orders +- **Webhook Queue**: Manages webhook queuing system + +#### Database +- Order meta stores: Payment Session ID, Payment ID, Webhook IDs +- Webhook queue table: Temporary storage for unmatched webhooks + +## Webhook Configuration + +Configure the following webhook URL in your Checkout.com Hub: + +``` +https://your-site.com/?wc-api=wc_checkoutcom_webhook +``` + +### Webhook Events Supported + +- `payment_approved` +- `payment_captured` +- `payment_declined` +- `payment_cancelled` +- `payment_voided` +- `payment_refunded` + +## Development + +### Building the Plugin + +Use the build script to create the plugin zip: + +```bash +./bin/build.sh +``` + +This will create `checkout-com-unified-payments-api.zip` with the correct WordPress plugin structure. + +### File Structure + +``` +checkout-com-unified-payments-api/ +├── woocommerce-gateway-checkout-com.php # Main plugin file +├── flow-integration/ # Flow integration +│ ├── class-wc-gateway-checkout-com-flow.php +│ └── assets/ +│ ├── js/ +│ │ ├── payment-session.js +│ │ ├── flow-container.js +│ │ └── flow-customization.js +│ └── css/ +│ └── flow.css +├── includes/ # Core functionality +│ ├── api/ +│ ├── settings/ +│ ├── admin/ +│ └── ... +├── assets/ # Frontend assets +├── lib/ # Libraries +└── vendor/ # Composer dependencies +``` + +## Requirements + +- WordPress 5.0+ +- WooCommerce 3.0+ +- PHP 7.3+ +- SSL Certificate (required for production) + +## Support + +For support and integration help: +- **Integration Support**: integration@checkout.com +- **General Support**: support@checkout.com +- **Sales**: sales@checkout.com + +## License + +MIT License + +## Changelog + +### Version 5.0.0 +- Initial Flow integration release +- Complete Flow Web Components integration +- Saved cards functionality +- 3D Secure support +- Webhook queue system +- Enhanced order management +- Comprehensive validation and error handling + +## Documentation + +For detailed documentation, visit: [Checkout.com Documentation](https://docs.checkout.com) + +--- + +**Checkout.com** is authorised and regulated as a Payment institution by the UK Financial Conduct Authority. diff --git a/WEBHOOK_MATCHING_LOG_GUIDE.md b/WEBHOOK_MATCHING_LOG_GUIDE.md new file mode 100644 index 00000000..bbf55df0 --- /dev/null +++ b/WEBHOOK_MATCHING_LOG_GUIDE.md @@ -0,0 +1,372 @@ +# Webhook Matching - Log Analysis Guide + +This guide shows you exactly what to search for in logs to trace how a payment webhook was matched to an order. + +--- + +## 🔍 **Quick Search Guide** + +### **Step 1: Find the Webhook Entry** + +Search for the **Payment ID** from the order notes: +``` +pay_u43vgdybaz5ybexffmkkytorxu +``` + +Or search for: +``` +WEBHOOK MATCHING: ========== STARTING ORDER LOOKUP ========== +``` + +--- + +## 📋 **Complete Log Sequence** + +### **1. Webhook Received (Start)** + +**Search:** `WEBHOOK MATCHING: ========== STARTING ORDER LOOKUP ==========` + +**What you'll see:** +``` +WEBHOOK MATCHING: ========== STARTING ORDER LOOKUP ========== +WEBHOOK MATCHING: Event Type: payment_captured +WEBHOOK MATCHING: Payment ID: pay_u43vgdybaz5ybexffmkkytorxu +WEBHOOK MATCHING: Session ID in metadata: ps_xxxxx (or NOT SET) +WEBHOOK MATCHING: Order ID in metadata: 12345 (or NOT SET) +``` + +**What this tells you:** +- ✅ Webhook event type +- ✅ Payment ID from webhook +- ✅ Session ID (if present) +- ✅ Order ID (if present in metadata) + +--- + +### **2. Method 1: Order ID from Metadata** + +**Search:** `WEBHOOK MATCHING: Trying METHOD 1` + +**What you'll see if Order ID exists:** +``` +WEBHOOK MATCHING: Trying METHOD 1 (Order ID from metadata): 12345 +WEBHOOK MATCHING: ✅ MATCHED BY METHOD 1 (Order ID from metadata) - Order ID: 12345 +``` + +**OR if Order ID not found:** +``` +WEBHOOK MATCHING: Trying METHOD 1 (Order ID from metadata): 12345 +WEBHOOK MATCHING: ❌ METHOD 1 FAILED - Order ID 12345 not found +``` + +**What this tells you:** +- ✅ If order_id was in webhook metadata +- ✅ If order was found by order_id +- ❌ If order_id was missing or order not found + +--- + +### **3. Method 2: Combined (Session ID + Payment ID)** + +**Search:** `WEBHOOK MATCHING: METHOD 2` or `COMBINED` + +**What you'll see if matched:** +``` +WEBHOOK MATCHING: ✅ MATCHED BY METHOD 2 (COMBINED: Session ID + Payment ID) - Order ID: 12345 +``` + +**OR if failed:** +``` +WEBHOOK MATCHING: ❌ METHOD 2 FAILED - No order found by COMBINED match (Session ID: ps_xxxxx, Payment ID: pay_xxxxx) +``` + +**What this tells you:** +- ✅ Most reliable matching method (requires BOTH session ID and payment ID) +- ✅ Order found by combined identifiers +- ❌ If either session ID or payment ID missing/doesn't match + +--- + +### **4. Method 3: Payment ID Alone** + +**Search:** `WEBHOOK MATCHING: METHOD 3` or `PAYMENT ID ALONE` + +**What you'll see if matched:** +``` +WEBHOOK MATCHING: ✅ MATCHED BY METHOD 3 (PAYMENT ID ALONE) - Order ID: 12345 +WEBHOOK MATCHING: ⚠️ WARNING: Matched by payment ID alone (less reliable than combined match) +``` + +**OR if failed:** +``` +WEBHOOK MATCHING: ❌ METHOD 3 FAILED - No order found by payment ID: pay_xxxxx +``` + +**What this tells you:** +- ✅ Fallback method (less reliable) +- ✅ Order found by payment ID only +- ⚠️ Warning that this is less reliable +- ❌ If payment ID doesn't match any order + +--- + +### **5. Order Details (If Found)** + +**Search:** `WEBHOOK MATCHING: ✅ ORDER FOUND` + +**What you'll see:** +``` +WEBHOOK MATCHING: ✅ ORDER FOUND - Order ID: 12345 +WEBHOOK MATCHING: Order Status: pending +WEBHOOK MATCHING: Order Payment Session ID: ps_xxxxx +WEBHOOK MATCHING: Order Payment ID (_cko_flow_payment_id): pay_xxxxx +WEBHOOK MATCHING: Order Payment ID (_cko_payment_id): pay_xxxxx +``` + +**What this tells you:** +- ✅ Order was successfully matched +- ✅ Current order status +- ✅ Payment session ID stored on order +- ✅ Payment IDs stored on order (both fields) + +--- + +### **6. Order Not Found** + +**Search:** `WEBHOOK MATCHING: ❌ ORDER NOT FOUND` + +**What you'll see:** +``` +WEBHOOK MATCHING: ❌ ORDER NOT FOUND - No matching order found +Flow webhook: No order found for webhook processing. Payment ID: pay_xxxxx - Will attempt to queue or process via webhook handlers +``` + +**What this tells you:** +- ❌ All 3 methods failed +- ✅ Webhook will be queued (if payment_approved or payment_captured) +- ✅ Checkout.com will retry (for other webhook types) + +--- + +### **7. Payment ID Validation** + +**Search:** `WEBHOOK MATCHING: Payment ID mismatch` or `Payment ID validation` + +**What you'll see if mismatch:** +``` +WEBHOOK MATCHING: ❌ CRITICAL ERROR - Payment ID mismatch in Flow webhook handler! +WEBHOOK MATCHING: Order ID: 12345 +WEBHOOK MATCHING: Order _cko_flow_payment_id: pay_xxxxx +WEBHOOK MATCHING: Order _cko_payment_id: pay_xxxxx +WEBHOOK MATCHING: Expected payment ID: pay_xxxxx +WEBHOOK MATCHING: Webhook payment ID: pay_different +WEBHOOK MATCHING: ❌ REJECTING WEBHOOK - Payment ID does not match order! +``` + +**OR if match:** +``` +Flow webhook: ✅ Payment ID validation passed - Order payment ID: pay_xxxxx, Webhook payment ID: pay_xxxxx +``` + +**What this tells you:** +- ✅ Payment IDs match (webhook is for correct order) +- ❌ Payment IDs don't match (webhook rejected - wrong payment) + +--- + +### **8. Duplicate Prevention** + +**Search:** `WEBHOOK: Already processed` or `WEBHOOK: ✅ Marked as processed` + +**What you'll see:** +``` +WEBHOOK: ✅ Already processed - Payment ID: pay_xxxxx, Type: payment_captured, Order: 12345 +WEBHOOK: ✅ Skipping duplicate webhook processing to prevent multiple order updates +``` + +**OR:** +``` +WEBHOOK: ✅ Marked as processed - Payment ID: pay_xxxxx, Type: payment_captured, Order: 12345 +``` + +**What this tells you:** +- ✅ Webhook already processed (duplicate prevention working) +- ✅ Webhook marked as processed (will skip duplicates) + +--- + +### **9. Webhook Processing** + +**Search:** `WEBHOOK PROCESS:` + event type (e.g., `capture_payment`, `authorize_payment`) + +**What you'll see:** +``` +=== WEBHOOK PROCESS: capture_payment START === +WEBHOOK PROCESS: Event type: payment_captured +WEBHOOK PROCESS: Order ID from metadata: 12345 +WEBHOOK PROCESS: Payment ID: pay_xxxxx +WEBHOOK PROCESS: Order loaded successfully - Order ID: 12345, Status: pending +WEBHOOK PROCESS: Order status updated to: processing +=== WEBHOOK PROCESS: capture_payment END (SUCCESS) === +``` + +**What this tells you:** +- ✅ Webhook handler executed +- ✅ Order was found and loaded +- ✅ Order status was updated +- ✅ Processing completed successfully + +--- + +## 🔎 **Search Patterns for Specific Scenarios** + +### **Scenario 1: Find How Order Was Matched** + +**Search:** Payment ID + `WEBHOOK MATCHING: ✅ MATCHED BY METHOD` + +**Example:** +``` +pay_u43vgdybaz5ybexffmkkytorxu WEBHOOK MATCHING: ✅ MATCHED BY METHOD +``` + +**Result:** Shows which method (1, 2, or 3) matched the order + +--- + +### **Scenario 2: Find Why Matching Failed** + +**Search:** Payment ID + `WEBHOOK MATCHING: ❌` + +**Example:** +``` +pay_u43vgdybaz5ybexffmkkytorxu WEBHOOK MATCHING: ❌ +``` + +**Result:** Shows which methods failed and why + +--- + +### **Scenario 3: Find Payment ID Mismatch** + +**Search:** Payment ID + `Payment ID mismatch` or `REJECTING WEBHOOK` + +**Example:** +``` +pay_u43vgdybaz5ybexffmkkytorxu Payment ID mismatch +``` + +**Result:** Shows if webhook was rejected due to payment ID mismatch + +--- + +### **Scenario 4: Find Duplicate Webhook** + +**Search:** Payment ID + `Already processed` + +**Example:** +``` +pay_u43vgdybaz5ybexffmkkytorxu Already processed +``` + +**Result:** Shows if webhook was skipped as duplicate + +--- + +### **Scenario 5: Find Queued Webhook** + +**Search:** Payment ID + `WEBHOOK QUEUE` + +**Example:** +``` +pay_u43vgdybaz5ybexffmkkytorxu WEBHOOK QUEUE +``` + +**Result:** Shows if webhook was queued (order not found yet) + +--- + +## 📊 **Log Analysis Checklist** + +For a specific payment, check these in order: + +1. ✅ **Webhook Received** + - Search: `WEBHOOK MATCHING: ========== STARTING ORDER LOOKUP ==========` + - Check: Payment ID, Session ID, Order ID in metadata + +2. ✅ **Matching Method Used** + - Search: `WEBHOOK MATCHING: ✅ MATCHED BY METHOD` + - Check: Which method (1, 2, or 3) matched + +3. ✅ **Order Details** + - Search: `WEBHOOK MATCHING: ✅ ORDER FOUND` + - Check: Order ID, Status, Payment IDs + +4. ✅ **Payment ID Validation** + - Search: `Payment ID validation` or `Payment ID mismatch` + - Check: If payment IDs match + +5. ✅ **Duplicate Check** + - Search: `Already processed` or `Marked as processed` + - Check: If webhook was already processed + +6. ✅ **Processing Result** + - Search: `WEBHOOK PROCESS:` + event type + - Check: If webhook was processed successfully + +--- + +## 🎯 **Example: Complete Log Trace** + +For payment `pay_u43vgdybaz5ybexffmkkytorxu`: + +``` +1. WEBHOOK MATCHING: ========== STARTING ORDER LOOKUP ========== +2. WEBHOOK MATCHING: Event Type: payment_captured +3. WEBHOOK MATCHING: Payment ID: pay_u43vgdybaz5ybexffmkkytorxu +4. WEBHOOK MATCHING: Session ID in metadata: ps_xxxxx +5. WEBHOOK MATCHING: Order ID in metadata: NOT SET +6. WEBHOOK MATCHING: ❌ METHOD 1 FAILED - Order ID not found +7. WEBHOOK MATCHING: ✅ MATCHED BY METHOD 2 (COMBINED: Session ID + Payment ID) - Order ID: 12345 +8. WEBHOOK MATCHING: ✅ ORDER FOUND - Order ID: 12345 +9. WEBHOOK MATCHING: Order Status: pending +10. Flow webhook: ✅ Payment ID validation passed +11. WEBHOOK PROCESS: capture_payment START +12. WEBHOOK PROCESS: Order status updated to: processing +13. WEBHOOK PROCESS: capture_payment END (SUCCESS) +14. WEBHOOK: ✅ Marked as processed - Payment ID: pay_u43vgdybaz5ybexffmkkytorxu +``` + +**Analysis:** +- ✅ Webhook received for `payment_captured` event +- ✅ Order ID NOT in metadata (normal for Flow checkout) +- ✅ Method 1 failed (no order_id in metadata) +- ✅ Method 2 succeeded (matched by Session ID + Payment ID) +- ✅ Payment ID validation passed +- ✅ Webhook processed successfully +- ✅ Order status updated to `processing` + +--- + +## 🔧 **Log File Location** + +Logs are written to: +- **WordPress Debug Log:** `wp-content/debug.log` (if `WP_DEBUG_LOG` enabled) +- **Checkout.com Plugin Logs:** Check plugin settings for log location +- **Server Error Logs:** Check your hosting provider's error logs + +--- + +## 💡 **Tips** + +1. **Use Payment ID as primary search term** - It's unique and appears in all relevant logs +2. **Search for "WEBHOOK MATCHING:"** - Shows the matching process +3. **Search for "WEBHOOK PROCESS:"** - Shows the processing result +4. **Check timestamps** - Match logs with order note timestamps +5. **Look for ❌ and ✅** - Quick visual indicators of success/failure + +--- + +**Last Updated:** 2025-01-17 +**Version:** 5.0.0 + + diff --git a/WEBHOOK_PROCESSING_STEPS.md b/WEBHOOK_PROCESSING_STEPS.md new file mode 100644 index 00000000..99da62a8 --- /dev/null +++ b/WEBHOOK_PROCESSING_STEPS.md @@ -0,0 +1,507 @@ +# Webhook Processing Steps - Complete Guide + +This document details the exact steps and checks performed for each webhook type in the Checkout.com WooCommerce Flow integration. + +--- + +## 🔄 **FLOW WEBHOOK HANDLER (Entry Point)** + +**Location:** `class-wc-gateway-checkout-com-flow.php` → `webhook_handler()` + +### **Step 1: Order Matching (BEFORE Event Processing)** + +The Flow webhook handler matches orders using 3 methods (in order): + +1. **Method 1: Order ID from Metadata** + - Extract `order_id` from `$data->data->metadata->order_id` + - Load order using `wc_get_order($order_id)` + - ✅ If found: Use this order + - ❌ If not found: Try Method 2 + +2. **Method 2: Combined (Payment Session ID + Payment ID)** + - Extract `cko_payment_session_id` from `$data->data->metadata->cko_payment_session_id` + - Extract `payment_id` from `$data->data->id` + - Query orders with BOTH: + - `_cko_payment_session_id` = session ID + - `_cko_flow_payment_id` = payment ID + - First try orders with status: `pending`, `failed`, `on-hold`, `processing` + - If not found, try all orders (fallback) + - ✅ If found: Use this order + - ❌ If not found: Try Method 3 + +3. **Method 3: Payment ID Alone** + - Extract `payment_id` from `$data->data->id` + - Query orders with `_cko_flow_payment_id` = payment ID + - First try orders with status: `pending`, `failed`, `on-hold`, `processing` + - If not found, try all orders (fallback) + - ✅ If found: Use this order + - ❌ If not found: Order not found + +### **Step 2: Payment ID Validation (BEFORE Event Processing)** + +**CRITICAL CHECK:** This happens BEFORE any webhook event is processed. + +- Get order's payment IDs: + - `_cko_flow_payment_id` (preferred) + - `_cko_payment_id` (fallback) +- Get webhook payment ID: `$data->data->id` +- **If order has payment ID:** + - ✅ **Match:** Continue processing + - ❌ **Mismatch:** Reject webhook (HTTP 200, but don't process) +- **If order has NO payment ID:** + - Set payment ID from webhook (first payment attempt) + - Continue processing + +### **Step 3: Event Type Routing** + +Based on `$data->type`, route to appropriate handler: + +- `payment_approved` → `authorize_payment()` +- `payment_captured` → `capture_payment()` +- `payment_declined` / `payment_authentication_failed` → `decline_payment()` +- `payment_capture_declined` → `capture_declined()` +- `payment_refunded` → `refund_payment()` +- `payment_voided` → `void_payment()` +- `payment_canceled` → `cancel_payment()` + +### **Step 4: Mark as Processed** + +After successful processing: +- Create unique webhook ID: `{payment_id}_{event_type}` +- Add to order meta: `_cko_processed_webhook_ids` (array) +- Prevents duplicate processing + +--- + +## 1️⃣ **PAYMENT APPROVED (Authorization)** + +**Event Type:** `payment_approved` +**Handler:** `WC_Checkout_Com_Webhook::authorize_payment()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` (webhook queued if possible) + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` (webhook queued if possible) + +4. **Check Current State** + - Get `cko_payment_captured` meta + - Get current order status + - Get `cko_payment_authorized` meta + +5. **Status Update Logic** + - ✅ **If already captured:** + - Add note only + - Update meta: `cko_payment_authorized = true` + - Set transaction ID + - **DO NOT change status** (prevent downgrade) + - Return `true` + + - ✅ **If already authorized AND status matches configured status:** + - Add note only + - Return `true` + + - ✅ **If order status is `processing` or `completed`:** + - Add note + - Update meta: `cko_payment_authorized = true` + - Set transaction ID + - **DO NOT change status** (prevent downgrade) + - Return `true` + + - ✅ **Otherwise:** + - Set transaction ID: `action_id` + - Update meta: `_cko_payment_id = payment_id` + - Update meta: `cko_payment_authorized = true` + - Add order note + - Update status to configured status (default: `on-hold`) + - Return `true` + +6. **If Processing Failed** + - Return `false` + - Flow handler queues webhook for later processing + +--- + +## 2️⃣ **PAYMENT CAPTURED** + +**Event Type:** `payment_captured` +**Handler:** `WC_Checkout_Com_Webhook::capture_payment()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + - `amount` from `$data->data->amount` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` (webhook queued if possible) + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` (webhook queued if possible) + +4. **Check Authorization Status** + - Get `cko_payment_authorized` meta + - ✅ **If not authorized:** Set `cko_payment_authorized = true` (capture implies authorization) + +5. **Check Capture Status** + - Get `cko_payment_captured` meta + - ✅ **If already captured:** + - Add note only + - Return `true` + +6. **Process Capture** + - Add generic capture note + - Set transaction ID: `action_id` + - Update meta: `cko_payment_captured = true` + - Compare amounts: + - If `amount < order_amount`: Partial capture note + - If `amount == order_amount`: Full capture note + - Add specific capture note + - Update status to configured status (default: `processing`) + - Return `true` + +7. **If Processing Failed** + - Return `false` + - Flow handler queues webhook for later processing + +--- + +## 3️⃣ **PAYMENT DECLINED (Failed)** + +**Event Type:** `payment_declined` or `payment_authentication_failed` +**Handler:** `WC_Checkout_Com_Webhook::decline_payment()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + - `response_summary` from `$data->data->response_summary` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` + +4. **CRITICAL: Payment ID Validation** + - Get order's `_cko_flow_payment_id` or `_cko_payment_id` + - Compare with webhook `payment_id` + - ✅ **If order has payment ID AND matches:** Continue + - ✅ **If order has NO payment ID:** Continue (first attempt) + - ❌ **If order has payment ID AND doesn't match:** + - Log error + - Return `false` (reject webhook) + +5. **Check Order Status** + - Get current order status + - ✅ **If status is `failed`:** + - Add note only + - Return `true` (prevent duplicate status change) + +6. **Process Decline** + - Create decline message with payment ID, action ID, and reason + - Add order note + - Update status to `failed` + - Return `true` + +--- + +## 4️⃣ **PAYMENT CAPTURE DECLINED (Partial Capture Failed)** + +**Event Type:** `payment_capture_declined` +**Handler:** `WC_Checkout_Com_Webhook::capture_declined()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + - `response_summary` from `$data->data->response_summary` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` + +4. **Process Capture Decline** + - Create message with payment ID, action ID, and reason + - Add order note + - **Note:** Status is NOT changed (order remains in current state) + - Return `true` + +**Note:** This webhook does NOT validate payment ID (may need to be added for consistency). + +--- + +## 5️⃣ **PAYMENT REFUNDED (Returned)** + +**Event Type:** `payment_refunded` +**Handler:** `WC_Checkout_Com_Webhook::refund_payment()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + - `amount` from `$data->data->amount` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` + +4. **CRITICAL: Payment ID Validation** + - Get order's `_cko_flow_payment_id` or `_cko_payment_id` + - Compare with webhook `payment_id` + - ✅ **If order has payment ID AND matches:** Continue + - ✅ **If order has NO payment ID:** Continue (first attempt) + - ❌ **If order has payment ID AND doesn't match:** + - Log error + - Return `false` (reject webhook) + +5. **Check Transaction ID** + - Get order's transaction ID + - ✅ **If transaction ID matches `action_id`:** + - Return `true` (already processed) + +6. **Check Refund Status** + - Get `order->get_total_refunded()` + - ✅ **If fully refunded:** + - Add note only + - Return `true` + +7. **Process Refund** + - Set transaction ID: `action_id` + - Update meta: `cko_payment_refunded = true` + - Convert amount to order currency + - Compare amounts: + - If `amount < order_amount`: **Partial refund** + - Create partial refund note + - Create WooCommerce refund record + - If `amount == order_amount`: **Full refund** + - Create full refund note + - Create WooCommerce refund record + - Add order note + - Return `true` + +--- + +## 6️⃣ **PAYMENT VOIDED** + +**Event Type:** `payment_voided` +**Handler:** `WC_Checkout_Com_Webhook::void_payment()` + +### **Steps:** + +1. **Extract Data** + - `order_id` from `$data->data->metadata->order_id` + - `payment_id` from `$data->data->id` + - `action_id` from `$data->data->action_id` + +2. **Validate Order ID** + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` + +3. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` + +4. **CRITICAL: Payment ID Validation** + - Get order's `_cko_flow_payment_id` or `_cko_payment_id` + - Compare with webhook `payment_id` + - ✅ **If order has payment ID AND matches:** Continue + - ✅ **If order has NO payment ID:** Continue (first attempt) + - ❌ **If order has payment ID AND doesn't match:** + - Log error + - Return `false` (reject webhook) + +5. **Check Void Status** + - Get `cko_payment_voided` meta + - ✅ **If already voided:** + - Add note only + - Return `true` + +6. **Process Void** + - Create void message with payment ID and action ID + - Update meta: `cko_payment_voided = true` + - Add order note + - Update status to `cancelled` + - Return `true` + +--- + +## 7️⃣ **PAYMENT CANCELLED** + +**Event Type:** `payment_canceled` +**Handler:** `WC_Checkout_Com_Webhook::cancel_payment()` + +### **Steps:** + +1. **Extract Payment ID** + - `payment_id` from `$data->data->id` + +2. **Fetch Payment Details from Checkout.com API** + - Initialize Checkout SDK + - Call `getPaymentDetails($payment_id)` + - ❌ If API call fails: Return `false` + +3. **Extract Order ID** + - `order_id` from `$details['metadata']['order_id']` + - ✅ Must be numeric and not empty + - ❌ If invalid: Return `false` + +4. **Load Order** + - Use `self::get_wc_order($order_id)` + - ❌ If not found: Return `false` + +5. **CRITICAL: Payment ID Validation** + - Get order's `_cko_flow_payment_id` or `_cko_payment_id` + - Compare with webhook `payment_id` + - ✅ **If order has payment ID AND matches:** Continue + - ✅ **If order has NO payment ID:** Continue (first attempt) + - ❌ **If order has payment ID AND doesn't match:** + - Log error + - Return `false` (reject webhook) + +6. **Process Cancellation** + - Create cancellation message with payment ID + - Add order note + - Update status to `cancelled` + - Return `true` + +--- + +## 🔒 **SECURITY & VALIDATION SUMMARY** + +### **Payment ID Validation (Critical)** + +**Applied to:** +- ✅ `payment_declined` / `payment_authentication_failed` +- ✅ `payment_refunded` +- ✅ `payment_voided` +- ✅ `payment_canceled` +- ✅ Flow webhook handler (before event routing) + +**NOT Applied to:** +- ⚠️ `payment_approved` (relies on order_id matching) +- ⚠️ `payment_captured` (relies on order_id matching) +- ⚠️ `payment_capture_declined` (should be added) + +### **Duplicate Prevention** + +1. **Webhook Queue:** + - Checks for duplicate webhooks before queuing + - Prevents same `payment_id + webhook_type` from being queued twice + +2. **Processed Webhooks Tracking:** + - Stores `_cko_processed_webhook_ids` array on order + - Format: `{payment_id}_{event_type}` + - Prevents same webhook from being processed twice + +3. **Status Checks:** + - `authorize_payment()`: Checks if already captured/authorized + - `capture_payment()`: Checks if already captured + - `decline_payment()`: Checks if already failed + - `void_payment()`: Checks if already voided + - `refund_payment()`: Checks if transaction ID matches or fully refunded + +### **Status Protection** + +- **Prevents Downgrades:** + - `authorize_payment()` won't downgrade from `processing`/`completed` to `on-hold` + - `decline_payment()` won't change status if already `failed` + +- **Status Update Rules:** + - `authorize_payment()`: Updates to configured status (default: `on-hold`) + - `capture_payment()`: Updates to configured status (default: `processing`) + - `decline_payment()`: Updates to `failed` + - `void_payment()`: Updates to `cancelled` + - `cancel_payment()`: Updates to `cancelled` + - `capture_declined()`: **NO status change** (note only) + - `refund_payment()`: **NO status change** (WooCommerce handles refund status) + +--- + +## 📝 **LOGGING** + +All webhook processing includes extensive logging: + +- **Always Logged (Critical):** + - Order matching results + - Payment ID mismatches + - Order not found errors + - Invalid order IDs + +- **Debug Mode Only:** + - Full webhook data structure + - Step-by-step processing details + - Status change decisions + - Meta updates + +**Log Location:** WordPress debug log (if enabled) or Checkout.com plugin logs + +--- + +## 🔄 **WEBHOOK QUEUE SYSTEM** + +**When Used:** +- `payment_approved` webhook fails to process +- `payment_captured` webhook fails to process + +**How It Works:** +1. Webhook processing fails (returns `false`) +2. Flow handler checks if webhook can be queued +3. If `payment_id` exists, saves to `wp_cko_webhook_queue` table +4. Returns `true` to Checkout.com (prevents retries) +5. Queue processor runs when order is found/created +6. Processes queued webhooks in order + +**Duplicate Prevention:** +- Checks for existing unprocessed webhook with same `payment_id + webhook_type` +- Checks `_cko_processed_webhook_ids` before processing +- Marks as processed after successful processing + +--- + +## ✅ **CHECKLIST FOR EACH WEBHOOK** + +Before processing any webhook, verify: + +- [ ] Order ID is valid and numeric +- [ ] Order exists in WooCommerce +- [ ] Payment ID matches order (if order has payment ID) +- [ ] Webhook hasn't been processed before (`_cko_processed_webhook_ids`) +- [ ] Order status allows this update (no downgrades) +- [ ] All required data is present (payment_id, action_id, etc.) + +--- + +**Last Updated:** 2025-01-17 +**Version:** 5.0.0 + + diff --git a/WOOCOMMERCE_FLOW_INTEGRATION_DOCS.md b/WOOCOMMERCE_FLOW_INTEGRATION_DOCS.md new file mode 100644 index 00000000..814a0993 --- /dev/null +++ b/WOOCOMMERCE_FLOW_INTEGRATION_DOCS.md @@ -0,0 +1,526 @@ +# WooCommerce Flow Integration Documentation + +Last updated: January 2025 + +From downloading the plugin to requesting your first test payment, learn how to get started with the Checkout.com Flow integration for WooCommerce. + +## Information + +This guide assumes you have already set up WooCommerce on your WordPress instance. + +--- + +## Before you start + +You must create a public and private key to configure the integration. + +Additionally, you need a signature key to configure webhooks. + +### Create a public API key + +1. Sign in to the sandbox environment in the Dashboard. +2. Select the _Developers_ icon in the top navigation bar, and then select the _Keys_ tab. +3. Select _Create a new key_. +4. When you're prompted for which type of key to create, select _Public API key_. +5. Give the API key a description to make it easier to identify in the future. +6. Disable the _Allow any processing channel_ setting. +7. Select the processing channel you want to use for WooCommerce from the list. +8. Select _Submit_ to create the key. + +Make a note of your public API key as you'll need it for a later step. You can view your public API key at any time after creation. + +### Create a private API key + +1. Sign in to the sandbox environment in the Dashboard. +2. Select the _Developers_ icon in the top navigation bar, and then select the _Keys_ tab. +3. Select _Create a new key_. +4. When you're prompted for which type of key to create, select _Secret API key_. +5. Give the API key a description to make it easier to identify in the future. +6. Under _Scopes_, select _Default_. +7. Disable the _Allow any processing channel_ setting. +8. Select the processing channel you want to use for WooCommerce from the list. +9. Select _Create key_. +10. Copy your private API key securely. You'll need it to configure the plugin. + +### Note + +For security, you cannot view the secret API key again after you've left the _Create a new key_ page. Ensure you copy its value securely before you exit or close the window. + +### Create a webhook + +Webhooks are notifications that we send when an event occurs on your account. For example, when a payment is captured. The WooCommerce plugin uses them to update order statuses automatically. + +You can configure a webhook in your WooCommerce settings. + +**Webhook URL Format:** +``` +https://your-site.com/?wc-api=wc_checkoutcom_webhook +``` + +### Check you have no previous version of the plugin + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _Plugins_. +3. Look for Checkout.com plugins. If you find one, select _Delete_, or select _Deactivate_ and then _Delete_. + +--- + +## Install the plugin + +You can install the plugin in the following ways: + +### Use the WordPress plugin directory + +1. Sign in to WordPress as an administrator. +2. In your WordPress dashboard, go to _Plugins_ > _Add New_. +3. Search for _Checkout.com Payment Gateway_. +4. Select _Install Now_. +5. After the installation completes, select _Activate Plugin_. + +### Download the plugin and install it manually + +1. Go to the WooCommerce plugin repository or download from GitHub releases. +2. Download the latest release of the plugin (`checkout-com-unified-payments-api.zip`). +3. Sign in to WordPress as an administrator. +4. In your WordPress dashboard, go to _Plugins_ > _Add New_. +5. Select _Upload Plugin_ > _Choose file_. +6. Upload your downloaded ZIP file. +7. Select _Install Now_. +8. After the installation completes, select _Activate Plugin_. + +--- + +## Configure the plugin + +1. In your WordPress dashboard, go to _WooCommerce_ > _Settings_ > _Payments_. +2. Find _Checkout.com Payment_ and select _Manage_. +3. Select _Enable Checkout.com card payments_. +4. Set the environment to _Sandbox_ (for testing) or _Live_ (for production). +5. Enter a payment option title. This is displayed to customers on your checkout page (e.g., "Credit/Debit Card"). +6. Under _Checkout mode_, select _Flow_. +7. Set _Account type_ to _NAS_ (or your account type). +8. Enter your **Secret key** and **Public key**. +9. Select _Save changes_. +10. Select _Card Settings_, configure your preferences, then select _Save changes_. +11. Select _Order Settings_, review the order status mappings, then select _Save changes_. + +### Register a webhook with the default configuration + +To check the current webhook status and register a webhook with the default configuration: + +1. In your WordPress dashboard, go to _WooCommerce_ > _Settings_ > _Payments_. +2. Find _Checkout.com Payment_ and select _Manage_. +3. Select the _Webhooks_ tab. +4. Select _Run Webhook check_ to check if a webhook is configured for the current site. + +If no webhook is configured, select _Register Webhook_. This creates a new webhook for all events listed in your Dashboard account. + +**Webhook Events Supported:** +- `payment_approved` - Payment authorized successfully +- `payment_captured` - Payment captured successfully +- `payment_declined` - Payment declined +- `payment_cancelled` - Payment cancelled +- `payment_voided` - Payment voided +- `payment_refunded` - Payment refunded + +--- + +## Test your integration + +1. Go to your shop's public URL and add a product to your cart. +2. Go to your cart then proceed to checkout. +3. Enter the required billing details. We recommend using a real email address so that you can receive the order confirmation. +4. Select the _Checkout.com Payment_ method. +5. The Flow payment form will appear. Enter the following card details: + * Number – `4242 4242 4242 4242` + * Expiry date – Any future date (e.g., `12/25`) + * CVV – `100` + * Cardholder name – Any name +6. Select the terms and conditions box. +7. Select _Place order_. + - The order will be created first (status: `Pending payment`) + - Payment will be processed through Flow + - If 3D Secure is required, you'll be redirected for authentication + - After successful payment, you'll be redirected to the order confirmation page +8. If you entered a real email address in the billing details, you'll receive an order confirmation email. +9. Sign in to your WordPress account as an administrator. +10. Select _WooCommerce_ > _Orders_ in the left menu. Your test order is displayed and has a status of `Processing`. This indicates that the payment has been successfully captured and that your webhooks are set up correctly. + +### Test Cards + +For test cards and a range of possible scenarios, see [Checkout.com Testing Documentation](https://www.checkout.com/docs/testing). + +**Common Test Cards:** +- **Success:** `4242 4242 4242 4242` +- **3D Secure Required:** `4000 0025 0000 3155` +- **Declined:** `4000 0000 0000 0002` +- **Insufficient Funds:** `4000 0000 0000 9995` + +You can now either go live as is or extend your configuration. + +--- + +## Go live + +When your testing is complete and you're ready to start accepting payments: + +1. Contact our Sales team to move to a live account. +2. Update your plugin settings: + - Change _Environment_ from _Sandbox_ to _Live_ + - Update your _Secret key_ and _Public key_ with live credentials + - Re-register your webhook URL in the live Dashboard +3. Test a small transaction to verify everything works correctly. + +--- + +## How Flow Integration Works + +The Flow integration provides a modern, secure payment experience using Checkout.com's Flow Web Components. This integration ensures reliable payment processing with comprehensive validation, webhook handling, and order management. + +### Payment Flow Overview + +The payment flow follows these steps: + +#### Step 1: Checkout Page Load +- Customer fills out billing and shipping information +- Flow payment method is selected +- Flow Web Component is initialized and mounted + +#### Step 2: Order Creation (Before Payment) +- **Why Early?** Orders are created via AJAX before payment processing begins +- This ensures the order exists in the database for webhook matching +- Order status: `Pending payment` +- Payment session ID is stored with the order + +#### Step 3: Payment Session Creation +- Payment session is created with Checkout.com API +- Session includes order details, customer information, and amount +- Payment session ID is returned and stored + +#### Step 4: Flow Component Validation +- **Client-Side Validation:** Flow component validates card details in real-time +- Card number, expiry, CVV are validated before submission +- Invalid cards are rejected before payment attempt + +#### Step 5: Payment Processing +- Customer submits payment through Flow component +- Payment is processed securely through Checkout.com +- For 3D Secure: Customer is redirected for authentication +- Payment result is returned + +#### Step 6: Webhook Processing +- Checkout.com sends webhook with payment status +- Webhook is matched to order using: + 1. **Order ID from metadata** (primary method) + 2. **Payment Session ID + Payment ID** (secondary method) + 3. **Payment ID alone** (fallback method) +- If order not found immediately, webhook is queued for later processing + +#### Step 7: Order Status Update +- Order status is automatically updated based on payment result: + * ✅ **Payment Approved** → Order status: `Processing` or `On hold` (if manual capture) + * ✅ **Payment Captured** → Order status: `Processing` + * ❌ **Payment Declined** → Order status: `Failed` + * ⏸️ **Payment Cancelled** → Order status: `Cancelled` + +### Key Features + +#### 🔒 Early Order Creation +Orders are created before payment processing to ensure webhooks can always find the order. This prevents webhook matching failures and allows order tracking throughout the payment process. + +#### ✅ Dual Validation System +- **Client-Side:** Flow component validates card details in real-time +- **Server-Side:** Comprehensive validation of all checkout fields before order creation + +#### 🚫 Duplicate Prevention +- Client-side lock prevents multiple simultaneous requests +- Server-side check prevents duplicate orders with same payment session ID +- Webhook queue prevents duplicate webhook processing + +#### 📬 Webhook Queue System +- Temporarily stores webhooks if order not found immediately +- Queue is processed when order becomes available +- Ensures no webhooks are lost +- Automatic retry mechanism + +#### 🔐 3D Secure (3DS) Support +- Automatic 3DS detection +- Seamless redirect flow +- Webhook handling after 3DS return +- Prevents duplicate status updates + +#### 💳 Saved Cards +- Customer can opt to save card during checkout +- Cards are tokenized securely by Checkout.com +- Saved cards appear on future checkouts +- Cards can be deleted by customer + +--- + +## Extend your configuration + +There are a number of ways you can extend your WooCommerce integration so that it suits all your business needs. + +### Add more payment methods + +#### Note +To start accepting an alternative payment method, we first need to enable it on your account. Contact your account manager or our Sales team to get started. + +We currently support the following payment methods on WooCommerce: + +* Apple Pay +* Google Pay +* PayPal +* Bancontact +* Cartes Bancaires +* EPS +* iDEAL +* Klarna Payments +* Klarna Debit Risk +* KNET +* Multibanco +* Poli +* Sofort + +#### Enable alternative payments + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _WooCommerce_ > _Settings_ > _Payments_. +3. Find the payment method you want to enable (e.g., _Checkout.com - PayPal_). +4. Select _Manage_. +5. Tick _Enable Checkout.com_. +6. Enter a _Title_. This is what the customer sees on the checkout page. +7. Enter your API credentials if required. +8. Select _Save changes_. + +That's it! Your checkout page now includes the option to pay using your additional payment method(s). + +#### Apple Pay + +##### Information +Apple Pay is only supported on self-hosted instances of WordPress. + +##### Before you start +If you're located in the UAE or Saudi Arabia, contact your account manager or our Sales team to activate Apple Pay on your account. + +To get started with Apple Pay payments, you must first generate your certificate signing request and upload it to the Apple Development Center. + +Once this is done, you'll need to complete the certification process. Read our [Apple Pay guide](https://www.checkout.com/docs/payments/add-payment-methods/apple-pay) to configure your environment. + +##### Enable Apple Pay + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _WooCommerce_ > _Settings_ > _Payments_. +3. Find _Checkout.com - Apple Pay_ and select _Manage_. +4. Select _Enable Checkout.com_. +5. Enter a title and description. These are displayed to customers on your checkout page. +6. Enter your merchant identifier. You can find it in the Apple Development Center. +7. Enter the absolute path to your merchant certificate and merchant certificate key. +8. Select a button type and button theme. +9. Set the button language using a two-digit ISO 639-1 code (for example, use `en` for English). +10. Select _Save changes_. + +To test Apple Pay, use the Apple Pay test cards. + +#### Google Pay + +##### Before you start +If you're located in the UAE or Saudi Arabia, contact your account manager or our Sales team to activate Google Pay on your account. + +To get started with Google Pay payments, you must register with Google Pay and choose Checkout.com as your payment processor. + +##### Enable Google Pay + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _WooCommerce_ > _Settings_ > _Payments_. +3. Find _Checkout.com - Google Pay_ and select _Manage_. +4. Select _Enable Checkout.com_. +5. Enter a title and description. These are displayed to customers on your checkout page. +6. Leave the merchant identifier set to `01234567890123456789` for testing purposes. +7. To enable 3DS for Google Pay, set _Use 3D Secure_ to _Yes_. +8. Select a button style. +9. Select _Save changes_. + +### Enable 3D Secure payments + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _WooCommerce_ > _Settings_ > _Payments_. +3. Find _Checkout.com Payment_ and select _Manage_. +4. Select _Card Settings_. +5. Set _Use 3D Secure_ to _Yes_. +6. Select _Save changes_. + +3D Secure payments are now enabled on your account. When a payment requires 3DS authentication, customers will be automatically redirected to their bank's authentication page. + +### Capture payments manually + +#### Enable manual captures + +1. Sign in to WordPress as an administrator. +2. In the left menu, select _WooCommerce_ > _Settings_ > _Payments_. +3. Find _Checkout.com Payment_ and select _Manage_. +4. Select _Card Settings_. +5. Set _Payment Action_ to _Authorize Only_. +6. Select _Save changes_. + +Any payments received are authorized only. You must manually capture them within seven days, or they are automatically voided. + +#### Capture a payment + +1. In the Dashboard sandbox, select _Payments_ > _Processing_ > _All Payments_. +2. Select the test payment. The _Payment details_ page opens. +3. Select _Capture payment_ in the top right. +4. Select _Capture payment_. The _Status_ column on the _Payments_ page is updated to say `CAPTURED`. +5. Sign in to WordPress as an administrator. +6. Select _WooCommerce_ > _Orders_ in the left menu. +7. Select your test order to display the order details. + +The order note confirms that your payment has been successfully captured. + +### Accept recurring payments via the WooCommerce Subscriptions extension + +With recurring payments, you can process shopper interactions for scheduled payments, such as subscription payments. + +#### Note +To use this feature, you must be using WooCommerce Subscriptions to manage subscriptions within WooCommerce. See [WooCommerce Subscriptions Store Manager Guide](https://woocommerce.com/document/subscriptions/store-manager-guide/). + +The Checkout.com WooCommerce plugin registers with payment events triggered by WooCommerce Subscriptions to support the following actions: + +* Cancellation of a subscription +* Suspension of a subscription +* Re-activation of a subscription +* Change of amount for a subscription +* Change of date for a subscription +* Management of multiple subscriptions + +### Configure order statuses + +These settings allow you to edit the order statuses in line with the status of the payment. They are automatically set to WooCommerce's default values, so be aware that editing them may cause problems with the order flow. + +To find these settings: + +1. Sign in to WordPress as an administrator. +2. Go to _WooCommerce_ > _Settings_ > _Payments_. +3. Find the Checkout.com plugin. +4. Select _Manage_ and then _Order Settings_. + +**Default Order Status Mappings:** +- **Payment Approved:** `Processing` (or `On hold` if manual capture enabled) +- **Payment Captured:** `Processing` +- **Payment Declined:** `Failed` +- **Payment Cancelled:** `Cancelled` +- **Payment Refunded:** `Refunded` + +### Saved Cards Configuration + +The Flow integration supports saving customer payment methods for future use. + +#### Enable Saved Cards + +1. Sign in to WordPress as an administrator. +2. Go to _WooCommerce_ > _Settings_ > _Payments_. +3. Find _Checkout.com Payment_ and select _Manage_. +4. Select _Card Settings_. +5. Enable _Save card for future use_ or _Enable tokenization_. +6. Select _Save changes_. + +#### Saved Cards Display Options + +You can configure how saved cards are displayed: + +- **Saved Cards First:** Saved cards appear before the new card form +- **New Payment First:** New card form appears first, saved cards below + +This can be configured in the Flow integration settings. + +--- + +## Troubleshooting + +### Flow Component Not Loading + +If the Flow payment form is not appearing: + +1. Check that Flow mode is enabled in settings +2. Verify your Public API key is correct +3. Check browser console for JavaScript errors +4. Ensure all required checkout fields are filled +5. Verify SSL certificate is valid (required for Flow) + +### Webhooks Not Processing + +If order statuses are not updating: + +1. Verify webhook URL is registered in Dashboard +2. Check webhook signature key matches +3. Review webhook logs in WordPress (if logging enabled) +4. Check that webhook events are enabled in Dashboard +5. Verify order exists before webhook arrives (early order creation should handle this) + +### Payment Session Errors + +If you see payment session errors: + +1. Verify all required billing fields are filled +2. Check email format is valid +3. Ensure amount and currency are set correctly +4. Verify API keys are correct for your environment +5. Check network connectivity to Checkout.com API + +### 3D Secure Issues + +If 3DS authentication is not working: + +1. Verify 3DS is enabled in Card Settings +2. Check that test card requires 3DS (use `4000 0025 0000 3155`) +3. Ensure redirect URLs are configured correctly +4. Check that webhook is processing after 3DS return + +--- + +## Support + +For support and integration help: + +* **Integration Support:** integration@checkout.com +* **General Support:** support@checkout.com +* **Sales:** sales@checkout.com +* **Documentation:** [Checkout.com Documentation](https://www.checkout.com/docs) + +--- + +## Requirements + +* WordPress 5.0+ +* WooCommerce 3.0+ +* PHP 7.3+ +* SSL Certificate (required for production) +* Modern browser with JavaScript enabled + +--- + +## License + +MIT License + +--- + +## Changelog + +### Version 5.0.0 + +* Initial Flow integration release +* Complete Flow Web Components integration +* Saved cards functionality +* 3D Secure support +* Webhook queue system +* Enhanced order management +* Comprehensive validation and error handling +* Duplicate prevention (orders and webhooks) +* Early order creation for reliable webhook matching + +--- + +**Checkout.com** is authorised and regulated as a Payment institution by the UK Financial Conduct Authority. + + diff --git a/assets/js/cko-paypal-express-integration.js b/assets/js/cko-paypal-express-integration.js deleted file mode 100644 index b4d2fdc4..00000000 --- a/assets/js/cko-paypal-express-integration.js +++ /dev/null @@ -1,176 +0,0 @@ -/* global cko_paypal_vars */ - -jQuery( function ( $ ) { - - const formSelector = 'form.cart'; - - const onFormChange = function ( e ) { - const form = document.querySelector( formSelector ); - - const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null; - - const isEnabled = ( null === addToCartButton ) || ! addToCartButton.classList.contains( 'disabled' ); - - const element = jQuery( cko_paypal_vars.paypal_button_selector ); - - if ( isEnabled ) { - jQuery(element) - .removeClass('cko-disabled') - .off('mouseup') - .find('> *') - .css('pointer-events', ''); - } else { - jQuery(element) - .addClass('cko-disabled') - .on('mouseup', function(event) { - event.stopImmediatePropagation(); - }) - .find('> *') - .css('pointer-events', 'none'); - } - - }; - - const getAttributes = function() { - var select = $( '.variations_form' ).find( '.variations select' ), - data = {}, - count = 0, - chosen = 0; - - select.each( function() { - var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' ); - var value = $( this ).val() || ''; - - if ( value.length > 0 ) { - chosen ++; - } - - count ++; - data[ attribute_name ] = value; - }); - - return { - 'count' : count, - 'chosenCount': chosen, - 'data' : data - }; - }; - - const cko_express_add_to_cart = async function () { - var product_id = $( '.single_add_to_cart_button' ).val(); - - // Check if product is a variable product. - if ( $( '.single_variation_wrap' ).length ) { - product_id = $( '.single_variation_wrap' ).find( 'input[name="product_id"]' ).val(); - } - - var data = { - product_id: product_id, - qty: $( '.quantity .qty' ).val(), - attributes: $( '.variations_form' ).length ? getAttributes().data : [], - nonce: cko_paypal_vars.paypal_express_add_to_cart_nonce - }; - - return await $.ajax( { - url: cko_paypal_vars.add_to_cart_url, - type: 'POST', - async: false, - data: data - } ).done( function ( response ) { - cko_paypal_vars.debug && console.log( response ); - } ) - } - - const cko_express_create_order_id = async function () { - let addToCartSuccess = await cko_express_add_to_cart() - - // Prepare add-to-cart for express checkout. - let data = { - express_checkout: true, - add_to_cart: addToCartSuccess.result - } - - cko_paypal_vars.debug && console.log( data ); - - // Get Order ID from below endpoint. - return fetch( cko_paypal_vars.create_order_url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: jQuery.param( data ) - }).then(function (res) { - return res.json(); - }).then(function (data) { - if (typeof data.success !== 'undefined') { - let messages = data.data.messages ? data.data.messages : data.data; - - if ( 'string' === typeof messages || Array.isArray( messages ) ) { - showError( messages ); - } - return null; - } else { - return data.order_id; - } - }); - }; - - const paypalButton = { - init: function () { - // Initialize PayPal express button. - paypal.Buttons({ ...this.paypalButtonProps() }).render( cko_paypal_vars.paypal_button_selector ); - - this.updateButtonVisibility(); - - jQuery(document).on('change', formSelector, onFormChange ); - }, - - paypalButtonProps: function () { - let paypalButtonProps = { - onApprove: async function (data) { - cko_paypal_vars.debug && console.log(data); - - jQuery.post(cko_paypal_vars.paypal_order_session_url + "&paypal_order_id=" + data.orderID + "&woocommerce-process-checkout-nonce=" + cko_paypal_vars.woocommerce_process_checkout, function (data) { - if (typeof data.success !== 'undefined' && data.success !== true ) { - var messages = data.data.messages ? data.data.messages : data.data; - - if ( 'string' === typeof messages || Array.isArray( messages ) ) { - showError( messages ); - } - } else { - window.location.href = cko_paypal_vars.redirect; - } - }); - }, - onCancel: function (data, actions) { - cko_paypal_vars.debug && console.log(data); - jQuery('.woocommerce').unblock(); - }, - onError: function (err) { - cko_paypal_vars.debug && console.log(err); - jQuery('.woocommerce').unblock(); - }, - }; - - if ( cko_paypal_vars.is_cart_contains_subscription ) { - paypalButtonProps.createBillingAgreement = function( data, actions ) { - return cko_express_create_order_id(); - }; - } else { - paypalButtonProps.createOrder = function( data, actions ) { - return cko_express_create_order_id(); - }; - } - - return paypalButtonProps; - }, - - updateButtonVisibility: function () { - if ( jQuery( cko_paypal_vars.paypal_button_selector ) ) { - jQuery( cko_paypal_vars.paypal_button_selector ).show(); - } - } - } - - paypalButton.init(); -}); \ No newline at end of file diff --git a/backups/selective-sync-backup-20251005_235329 b/backups/selective-sync-backup-20251005_235329 new file mode 160000 index 00000000..1391aaea --- /dev/null +++ b/backups/selective-sync-backup-20251005_235329 @@ -0,0 +1 @@ +Subproject commit 1391aaea4a7a9ee9bba28ff361fb3acef38a6377 diff --git a/bin/build.sh b/bin/build.sh index 204ed638..9f7264f5 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -1,28 +1,138 @@ #!/bin/sh +# Build WordPress plugin zip with CORRECT structure for updates +# This ensures merchants won't see duplicate plugins -PLUGIN_SLUG="checkout-com-unified-payments-api" +set -e -echo "Installing PHP and JS dependencies..." -composer install --no-dev || exit "$?" +# Get script directory (works with both sh and bash) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/.." # Go to repo root -echo "Create build directory..." -mkdir "$PLUGIN_SLUG" +# Plugin identifiers (MUST match existing installation) +PLUGIN_FOLDER="checkout-com-unified-payments-api" +MAIN_FILE="woocommerce-gateway-checkout-com.php" +PLUGIN_NAME="Checkout.com Payment Gateway" +PLUGIN_SOURCE_DIR="${PLUGIN_FOLDER}" -echo "Copy plugin data to build folder..." -cp -R "assets" "$PLUGIN_SLUG/assets/" -cp -R "includes" "$PLUGIN_SLUG/includes/" -cp -R "languages" "$PLUGIN_SLUG/languages/" -cp -R "lib" "$PLUGIN_SLUG/lib/" -cp -R "vendor" "$PLUGIN_SLUG/vendor/" -cp -R "templates" "$PLUGIN_SLUG/templates/" -cp "readme.txt" "$PLUGIN_SLUG/readme.txt" -cp "woocommerce-gateway-checkout-com.php" "$PLUGIN_SLUG/woocommerce-gateway-checkout-com.php" +echo "🔍 Verifying plugin identifiers..." +echo " Folder: $PLUGIN_FOLDER" +echo " Main file: $MAIN_FILE" +echo " Plugin Name: $PLUGIN_NAME" +echo " Source directory: $PLUGIN_SOURCE_DIR" +echo "" -echo "Generating zip file..." -zip -q -r "${PLUGIN_SLUG}.zip" "$PLUGIN_SLUG/" +# Verify source directory exists +if [ ! -d "$PLUGIN_SOURCE_DIR" ]; then + echo "❌ ERROR: Plugin source directory not found: $PLUGIN_SOURCE_DIR" + exit 1 +fi -echo "Removing folder $PLUGIN_SLUG" -rm -r "$PLUGIN_SLUG" +# Verify main file exists in source directory +if [ ! -f "${PLUGIN_SOURCE_DIR}/${MAIN_FILE}" ]; then + echo "❌ ERROR: Main plugin file not found: ${PLUGIN_SOURCE_DIR}/${MAIN_FILE}" + exit 1 +fi -echo "${PLUGIN_SLUG}.zip file generated!" -echo "Build done!" +# Verify Plugin Name in header +if ! grep -q "Plugin Name: $PLUGIN_NAME" "${PLUGIN_SOURCE_DIR}/${MAIN_FILE}"; then + echo "⚠️ WARNING: Plugin Name header might not match!" +else + echo "✅ Plugin Name header verified" +fi + +# Create generic zip name for client distribution +ZIP_NAME="${PLUGIN_FOLDER}.zip" + +echo "" +echo "📦 Building zip file: $ZIP_NAME" +echo "" + +# Create temp directory with plugin folder structure +TEMP_DIR=$(mktemp -d) +PLUGIN_DIR="${TEMP_DIR}/${PLUGIN_FOLDER}" +mkdir -p "${PLUGIN_DIR}" + +echo "📁 Creating plugin folder structure..." + +# Copy files from plugin source directory (excluding unwanted ones) +rsync -av \ + --exclude='.git' \ + --exclude='.gitignore' \ + --exclude='*.zip' \ + --exclude='*.md' \ + --exclude='tests' \ + --exclude='*.log' \ + --exclude='node_modules' \ + --exclude='.DS_Store' \ + --exclude='__MACOSX' \ + --exclude='backups' \ + --exclude='*-backup-*' \ + --exclude='check-domain-association-file.php' \ + --exclude='diagnose-*.php' \ + --exclude='generate-*.php' \ + --exclude='test-*.php' \ + --exclude='terms-and-conditions-checkbox.php' \ + --exclude='create-zip.py' \ + --exclude='build-zip.sh' \ + --exclude='build-webhook-queue-zip.sh' \ + --exclude='build-plugin-zip.py' \ + --exclude='build-correct-zip.sh' \ + --exclude='check-zip-structure.py' \ + --exclude='verify-and-fix-zip.py' \ + --exclude='diagnose-header-error.py' \ + --exclude='php-uploads.ini' \ + "${PLUGIN_SOURCE_DIR}/" "${PLUGIN_DIR}/" > /dev/null 2>&1 + +# Verify structure +if [ ! -f "${PLUGIN_DIR}/${MAIN_FILE}" ]; then + echo "❌ ERROR: Main plugin file not found in plugin directory!" + rm -rf "${TEMP_DIR}" + exit 1 +fi + +echo "✅ Files copied to plugin folder" + +# Create zip from temp directory (so folder structure is preserved) +cd "${TEMP_DIR}" +echo "📦 Creating zip archive..." +zip -r "${ZIP_NAME}" "${PLUGIN_FOLDER}" > /dev/null + +# Move to original directory +mv "${ZIP_NAME}" "${SCRIPT_DIR}/../" + +# Cleanup +rm -rf "${TEMP_DIR}" + +# Verify zip structure +cd "${SCRIPT_DIR}/.." +echo "" +echo "🔍 Verifying zip structure..." +EXPECTED_PATH="${PLUGIN_FOLDER}/${MAIN_FILE}" +if unzip -l "${ZIP_NAME}" | grep -q "${EXPECTED_PATH}"; then + echo " ✅ Correct structure verified: ${EXPECTED_PATH}" +else + echo " ❌ ERROR: Zip structure is incorrect!" + echo " Expected: ${EXPECTED_PATH}" + unzip -l "${ZIP_NAME}" | head -5 + exit 1 +fi + +FILE_COUNT=$(unzip -l "${ZIP_NAME}" | tail -1 | awk '{print $2}') +ZIP_SIZE=$(ls -lh "${ZIP_NAME}" | awk '{print $5}') + +echo "" +echo "============================================================" +echo "✅ SUCCESS: Plugin zip created with correct structure!" +echo "============================================================" +echo "📁 File: ${ZIP_NAME}" +echo "💾 Size: ${ZIP_SIZE}" +echo "📊 Files: ${FILE_COUNT}" +echo "" +echo "🔑 WordPress Update Identifiers:" +echo " 1. ✅ Folder name: ${PLUGIN_FOLDER}" +echo " 2. ✅ Main file: ${MAIN_FILE}" +echo " 3. ✅ Plugin Name: ${PLUGIN_NAME}" +echo "" +echo "💡 This zip will UPDATE existing plugin installations" +echo " Merchants will NOT see duplicate plugins!" +echo "============================================================" diff --git a/build.py b/build.py new file mode 100644 index 00000000..c7b29e24 --- /dev/null +++ b/build.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Single build script for WordPress plugin zip file. +Creates zip with correct structure for WordPress updates. +""" +import os +import shutil +import tempfile +import zipfile +from datetime import datetime + +# Configuration +PLUGIN_FOLDER = 'checkout-com-unified-payments-api' +MAIN_FILE = 'woocommerce-gateway-checkout-com.php' +PLUGIN_NAME = 'Checkout.com Payment Gateway' + +# Get script directory +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +os.chdir(SCRIPT_DIR) + +print('=' * 70) +print('📦 Building WordPress Plugin Zip File') +print('=' * 70) +print(f'Plugin Folder: {PLUGIN_FOLDER}') +print(f'Main File: {MAIN_FILE}') +print(f'Plugin Name: {PLUGIN_NAME}') +print('') + +# Verify main file exists +if not os.path.exists(MAIN_FILE): + print(f'❌ ERROR: Main plugin file not found: {MAIN_FILE}') + exit(1) + +# Verify Plugin Name in header +with open(MAIN_FILE, 'r', encoding='utf-8') as f: + content = f.read() + if f'Plugin Name: {PLUGIN_NAME}' not in content: + print('⚠️ WARNING: Plugin Name header might not match!') + else: + print('✅ Plugin Name header verified') + +# Create timestamp +timestamp = datetime.now().strftime('%Y%m%d-%H%M%S') +zip_name = f'{PLUGIN_FOLDER}-{timestamp}.zip' + +print('') +print(f'📦 Building zip file: {zip_name}') +print('') + +# Create temp directory with plugin folder structure +temp_dir = tempfile.mkdtemp() +plugin_dir = os.path.join(temp_dir, PLUGIN_FOLDER) +os.makedirs(plugin_dir, exist_ok=True) + +print('📁 Creating plugin folder structure...') + +# Files/directories to exclude +exclude_patterns = [ + '.git', '.gitignore', '.zip', '.md', 'tests', '.log', + 'node_modules', '.DS_Store', '__MACOSX', 'backups', + '*-backup-*', 'check-domain-association-file.php', + 'diagnose-*.php', 'generate-*.php', 'test-*.php', + 'terms-and-conditions-checkbox.php', 'create-zip.py', + 'build-zip.sh', 'build-webhook-queue-zip.sh', + 'build-plugin-zip.py', 'build-correct-zip.sh', + 'check-zip-structure.py', 'verify-and-fix-zip.py', + 'diagnose-header-error.py', 'build.py', 'php-uploads.ini' +] + +def should_exclude(path): + """Check if path should be excluded""" + path_lower = path.lower() + for pattern in exclude_patterns: + if pattern.lower() in path_lower: + return True + return False + +# Copy files +copied_count = 0 +for root, dirs, files in os.walk('.'): + # Filter excluded directories + dirs[:] = [d for d in dirs if not should_exclude(d)] + + for file in files: + if should_exclude(file): + continue + + src = os.path.join(root, file) + if should_exclude(src): + continue + + rel_path = os.path.relpath(src, '.') + dst = os.path.join(plugin_dir, rel_path) + + try: + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(src, dst) + copied_count += 1 + except Exception as e: + print(f'⚠️ Warning: Could not copy {src}: {e}') + +print(f'✅ Copied {copied_count} files') + +# Verify main file exists in plugin directory +main_file_path = os.path.join(plugin_dir, MAIN_FILE) +if not os.path.exists(main_file_path): + print(f'❌ ERROR: Main plugin file not found in plugin directory!') + shutil.rmtree(temp_dir) + exit(1) + +# Create zip from temp directory +print('📦 Creating zip archive...') +zip_path = os.path.join(SCRIPT_DIR, zip_name) + +with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(plugin_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, temp_dir) + zipf.write(file_path, arcname) + +# Cleanup +shutil.rmtree(temp_dir) + +# Verify zip structure +print('') +print('🔍 Verifying zip structure...') +expected_path = f'{PLUGIN_FOLDER}/{MAIN_FILE}' + +with zipfile.ZipFile(zip_path, 'r') as verify_zip: + files = verify_zip.namelist() + if expected_path in files: + print(f' ✅ Correct structure verified: {expected_path}') + else: + print(f' ❌ ERROR: Zip structure is incorrect!') + print(f' Expected: {expected_path}') + print(' First 5 files in zip:') + for f in files[:5]: + print(f' - {f}') + exit(1) + +file_count = len(files) +zip_size = os.path.getsize(zip_path) / 1024 / 1024 + +print('') +print('=' * 70) +print('✅ SUCCESS: Plugin zip created with correct structure!') +print('=' * 70) +print(f'📁 File: {zip_name}') +print(f'💾 Size: {zip_size:.2f} MB') +print(f'📊 Files: {file_count}') +print('') +print('🔑 WordPress Update Identifiers:') +print(f' 1. ✅ Folder name: {PLUGIN_FOLDER}') +print(f' 2. ✅ Main file: {MAIN_FILE}') +print(f' 3. ✅ Plugin Name: {PLUGIN_NAME}') +print('') +print('💡 This zip will UPDATE existing plugin installations') +print(' Merchants will NOT see duplicate plugins!') +print('=' * 70) + + + + diff --git a/check-domain-association-file.php b/check-domain-association-file.php new file mode 100644 index 00000000..bf72a357 --- /dev/null +++ b/check-domain-association-file.php @@ -0,0 +1,179 @@ + + + + + Domain Association File Diagnostic + + + +

Apple Pay Domain Association File Diagnostic

+ + '; + echo '

1. Directory Check

'; + if ( file_exists( $well_known_dir ) ) { + echo '

✓ .well-known directory exists at: ' . esc_html( $well_known_dir ) . '

'; + echo '

Directory permissions: ' . substr( sprintf( '%o', fileperms( $well_known_dir ) ), -4 ) . '

'; + } else { + echo '

✗ .well-known directory does NOT exist at: ' . esc_html( $well_known_dir ) . '

'; + echo '

Try creating it manually or re-upload the domain association file via WordPress admin.

'; + } + echo ''; + + // Check 2: File exists + echo '
'; + echo '

2. File Check

'; + if ( file_exists( $file_path ) ) { + echo '

✓ Domain association file exists at: ' . esc_html( $file_path ) . '

'; + echo '

File size: ' . size_format( filesize( $file_path ) ) . '

'; + echo '

File permissions: ' . substr( sprintf( '%o', fileperms( $file_path ) ), -4 ) . ' (should be 0644 or 644)

'; + echo '

File readable: ' . ( is_readable( $file_path ) ? 'Yes' : 'No' ) . '

'; + + // Show first few lines of file + $file_content = file_get_contents( $file_path ); + if ( $file_content ) { + echo '

File content preview (first 200 chars):

'; + echo '
' . esc_html( substr( $file_content, 0, 200 ) ) . '...
'; + } + } else { + echo '

✗ Domain association file does NOT exist at: ' . esc_html( $file_path ) . '

'; + echo '

Please upload the domain association file via WordPress admin (Apple Pay settings).

'; + } + echo '
'; + + // Check 3: File URL accessibility + echo '
'; + echo '

3. URL Accessibility Check

'; + echo '

Expected URL: ' . esc_html( $file_url ) . '

'; + + // Try to access the file via HTTP + $response = wp_remote_get( $file_url, array( 'timeout' => 10 ) ); + if ( ! is_wp_error( $response ) ) { + $status_code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + + if ( 200 === $status_code ) { + echo '

✓ File is accessible via HTTP (Status: 200 OK)

'; + echo '

Response length: ' . strlen( $body ) . ' bytes

'; + } else { + echo '

✗ File returned HTTP status: ' . $status_code . '

'; + if ( 404 === $status_code ) { + echo '

404 Not Found - WordPress rewrite rules may be blocking access to .well-known directory.

'; + } elseif ( 403 === $status_code ) { + echo '

403 Forbidden - File permissions or server configuration may be blocking access.

'; + } + } + } else { + echo '

✗ Could not access file via HTTP: ' . esc_html( $response->get_error_message() ) . '

'; + } + echo '
'; + + // Check 4: WordPress rewrite rules + echo '
'; + echo '

4. WordPress Rewrite Rules Check

'; + $rewrite_rules = get_option( 'rewrite_rules' ); + if ( is_array( $rewrite_rules ) ) { + $has_well_known_rule = false; + foreach ( $rewrite_rules as $pattern => $rewrite ) { + if ( strpos( $pattern, 'well-known' ) !== false || strpos( $rewrite, 'well-known' ) !== false ) { + $has_well_known_rule = true; + break; + } + } + if ( $has_well_known_rule ) { + echo '

✓ Found .well-known in rewrite rules

'; + } else { + echo '

⚠ No specific .well-known rule found in rewrite rules.

'; + echo '

WordPress may be redirecting .well-known requests to index.php.

'; + } + } + echo '
'; + + // Check 5: .htaccess file + echo '
'; + echo '

5. .htaccess Check

'; + $htaccess_file = ABSPATH . '.htaccess'; + if ( file_exists( $htaccess_file ) ) { + echo '

✓ .htaccess file exists

'; + $htaccess_content = file_get_contents( $htaccess_file ); + if ( strpos( $htaccess_content, 'well-known' ) !== false ) { + echo '

✓ Found .well-known reference in .htaccess

'; + } else { + echo '

⚠ No .well-known rule found in .htaccess

'; + echo '

You may need to add a rule to allow .well-known directory access.

'; + } + } else { + echo '

⚠ .htaccess file does not exist (this is normal for Nginx or if using default permalinks)

'; + } + echo '
'; + + // Recommendations + echo '
'; + echo '

Recommendations

'; + if ( ! file_exists( $file_path ) ) { + echo '

Action Required: Upload the domain association file via WordPress admin (WooCommerce → Settings → Payments → Apple Pay → Domain Association Setup)

'; + } + + if ( file_exists( $file_path ) && ! is_readable( $file_path ) ) { + echo '

Action Required: Fix file permissions. Run: chmod 644 ' . esc_html( $file_path ) . '

'; + } + + if ( file_exists( $htaccess_file ) && strpos( file_get_contents( $htaccess_file ), 'well-known' ) === false ) { + echo '

Action Required: Add this rule to your .htaccess file (before WordPress rules):

'; + echo '
# Allow .well-known directory
'; + echo 'RewriteRule ^\.well-known/ - [L]
'; + } + + echo '

Note: After making changes, flush rewrite rules: WordPress Admin → Settings → Permalinks → Save Changes

'; + echo '
'; + + // Test file access + echo '
'; + echo '

Test File Access

'; + echo '

Click this link to test direct file access: ' . esc_html( $file_url ) . '

'; + echo '

If you see a 404 or redirect, WordPress rewrite rules are likely blocking access.

'; + echo '
'; + ?> + +
+

This diagnostic script should be removed after troubleshooting for security.

+ + + + + + + diff --git a/checkout-com-unified-payments-api-backup-20251013-001041 b/checkout-com-unified-payments-api-backup-20251013-001041 new file mode 160000 index 00000000..2f4f985c --- /dev/null +++ b/checkout-com-unified-payments-api-backup-20251013-001041 @@ -0,0 +1 @@ +Subproject commit 2f4f985cd604c6b4a02af7b92f99a0479ec79dd1 diff --git a/checkout-com-unified-payments-api.zip b/checkout-com-unified-payments-api.zip new file mode 100644 index 00000000..739922b3 Binary files /dev/null and b/checkout-com-unified-payments-api.zip differ diff --git a/checkout-com-unified-payments-api/assets/css/admin-apple-pay-settings.css b/checkout-com-unified-payments-api/assets/css/admin-apple-pay-settings.css new file mode 100644 index 00000000..5369f805 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/css/admin-apple-pay-settings.css @@ -0,0 +1,739 @@ +/** + * Redesigned Apple Pay Settings - Modern UX + * + * Features: + * - Card-based layout for better visual grouping + * - Progress indicators for multi-step processes + * - Status badges and completion tracking + * - Improved visual hierarchy + * - Better mobile responsiveness + * + * @package wc_checkout_com + */ + +/* ============================================ + GLOBAL STYLES & RESET + ============================================ */ + +.woocommerce .form-table { + border-collapse: separate; + border-spacing: 0; +} + +/* ============================================ + CARD-BASED LAYOUT + ============================================ */ + +.cko-settings-card { + background: #ffffff; + border: 1px solid #dcdcdc; + border-radius: 8px; + padding: 24px; + margin: 0 0 24px 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; + position: relative; +} + +.cko-settings-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border-color: #c3c4c7; +} + +.cko-settings-card.completed { + border-left: 4px solid #00a32a; + background: #f0f9f4; +} + +.cko-settings-card.in-progress { + border-left: 4px solid #f0b849; + background: #fff8e5; +} + +.cko-settings-card.required { + border-left: 4px solid #2271b1; +} + +.cko-settings-card.error { + border-left: 4px solid #d63638; + background: #fcf0f1; +} + +/* Card Header */ +.cko-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 2px solid #f0f0f1; +} + +.cko-card-title { + font-size: 18px; + font-weight: 600; + color: #1d2327; + margin: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.cko-card-title-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: #f0f6fc; + border-radius: 6px; + font-size: 18px; +} + +.cko-card-title-icon::before { + content: "🔐"; +} + +.cko-settings-card.completed .cko-card-title-icon { + background: #d1e7dd; +} + +.cko-settings-card.completed .cko-card-title-icon::before { + content: "✓"; + color: #00a32a; + font-weight: bold; +} + +.cko-settings-card.in-progress .cko-card-title-icon { + background: #fff3cd; +} + +.cko-settings-card.in-progress .cko-card-title-icon::before { + content: "⏳"; +} + +.cko-settings-card.error .cko-card-title-icon { + background: #f8d7da; +} + +.cko-settings-card.error .cko-card-title-icon::before { + content: "⚠"; + color: #d63638; +} + +/* Status Badge */ +.cko-status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.cko-status-badge.completed { + background: #d1e7dd; + color: #0f5132; +} + +.cko-status-badge.in-progress { + background: #fff3cd; + color: #856404; +} + +.cko-status-badge.required { + background: #cfe2ff; + color: #084298; +} + +.cko-status-badge.error { + background: #f8d7da; + color: #842029; +} + +/* Card Description */ +.cko-card-description { + font-size: 14px; + color: #646970; + line-height: 1.6; + margin-bottom: 20px; +} + +/* ============================================ + PROGRESS INDICATORS + ============================================ */ + +.cko-progress-steps { + display: flex; + align-items: center; + gap: 12px; + margin: 24px 0; + padding: 20px; + background: #f9f9f9; + border-radius: 6px; +} + +.cko-progress-step { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.cko-progress-step::after { + content: ""; + position: absolute; + top: 16px; + left: calc(50% + 20px); + width: calc(100% - 40px); + height: 2px; + background: #dcdcdc; + z-index: 0; +} + +.cko-progress-step:last-child::after { + display: none; +} + +.cko-progress-step.completed::after { + background: #00a32a; +} + +.cko-progress-step-number { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dcdcdc; + color: #646970; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; + position: relative; + z-index: 1; + margin-bottom: 8px; +} + +.cko-progress-step.completed .cko-progress-step-number { + background: #00a32a; + color: #ffffff; +} + +.cko-progress-step.completed .cko-progress-step-number::before { + content: "✓"; +} + +.cko-progress-step.active .cko-progress-step-number { + background: #2271b1; + color: #ffffff; + box-shadow: 0 0 0 4px rgba(34, 113, 177, 0.1); +} + +.cko-progress-step-label { + font-size: 12px; + color: #646970; + text-align: center; + font-weight: 500; +} + +.cko-progress-step.completed .cko-progress-step-label, +.cko-progress-step.active .cko-progress-step-label { + color: #1d2327; + font-weight: 600; +} + +/* ============================================ + SECTION HEADERS + ============================================ */ + +.woocommerce .form-table th.titledesc { + padding: 0; + font-size: 0; + border: none; + background: transparent; +} + +.woocommerce .form-table th.titledesc + td { + padding: 0; +} + +/* Hide default section titles - we use cards instead */ +.woocommerce .form-table th.titledesc:has(+ td .cko-settings-card) { + display: none; +} + +/* ============================================ + INFO BOXES (REDESIGNED) + ============================================ */ + +.cko-info-box { + background: #f0f6fc; + border-left: 4px solid #2271b1; + border-radius: 6px; + padding: 16px 20px; + margin: 16px 0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); +} + +.cko-info-box.warning { + background: #fff8e5; + border-left-color: #f0b849; +} + +.cko-info-box.success { + background: #f0f9f4; + border-left-color: #00a32a; +} + +.cko-info-box.error { + background: #fcf0f1; + border-left-color: #d63638; +} + +.cko-info-box.info { + background: #e7f3ff; + border-left-color: #2271b1; +} + +.cko-info-box h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + color: #1d2327; + display: flex; + align-items: center; + gap: 8px; +} + +.cko-info-box p { + margin: 8px 0; + font-size: 13px; + line-height: 1.6; + color: #50575e; +} + +.cko-info-box ol, +.cko-info-box ul { + margin: 12px 0 12px 20px; + padding-left: 20px; +} + +.cko-info-box li { + margin: 6px 0; + font-size: 13px; + line-height: 1.6; + color: #50575e; +} + +/* ============================================ + STEP INSTRUCTIONS (REDESIGNED) + ============================================ */ + +.cko-step-instructions { + background: #ffffff; + border: 1px solid #dcdcdc; + border-left: 4px solid #2271b1; + border-radius: 6px; + padding: 20px; + margin: 20px 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.cko-step-instructions h4 { + margin: 0 0 16px 0; + font-size: 15px; + font-weight: 600; + color: #1d2327; + display: flex; + align-items: center; + gap: 10px; +} + +.cko-step-instructions ol { + margin: 0; + padding-left: 25px; + counter-reset: step-counter; +} + +.cko-step-instructions li { + margin: 12px 0; + font-size: 14px; + line-height: 1.7; + color: #50575e; + position: relative; + padding-left: 8px; +} + +.cko-step-instructions li strong { + color: #1d2327; + font-weight: 600; +} + +/* ============================================ + BUTTONS (REDESIGNED) + ============================================ */ + +.cko-action-button { + background: #2271b1; + color: #ffffff; + border: none; + padding: 12px 24px; + font-size: 14px; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(34, 113, 177, 0.2); + display: inline-flex; + align-items: center; + gap: 8px; +} + +.cko-action-button:hover { + background: #135e96; + box-shadow: 0 4px 8px rgba(34, 113, 177, 0.3); + transform: translateY(-1px); +} + +.cko-action-button:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(34, 113, 177, 0.2); +} + +.cko-action-button:disabled { + background: #c3c4c7; + cursor: not-allowed; + transform: none; + box-shadow: none; + opacity: 0.6; +} + +.cko-action-button::before { + content: "→"; + font-size: 16px; +} + +/* ============================================ + FILE UPLOAD AREAS (REDESIGNED) + ============================================ */ + +.cko-file-upload-area { + background: #f9f9f9; + border: 2px dashed #c3c4c7; + border-radius: 8px; + padding: 32px 20px; + margin: 20px 0; + text-align: center; + transition: all 0.2s ease; + position: relative; +} + +.cko-file-upload-area:hover { + border-color: #2271b1; + background: #f0f6fc; + border-style: solid; +} + +.cko-file-upload-area.drag-over { + border-color: #2271b1; + background: #e7f3ff; + border-style: solid; + transform: scale(1.02); +} + +.cko-file-upload-area::before { + content: "📁"; + font-size: 48px; + display: block; + margin-bottom: 12px; + opacity: 0.5; +} + +.cko-file-upload-area input[type="file"] { + margin: 16px auto; + padding: 10px; + border: 1px solid #c3c4c7; + border-radius: 6px; + width: 100%; + max-width: 500px; + background: #ffffff; + cursor: pointer; +} + +/* ============================================ + CERTIFICATE GENERATION (REDESIGNED) + ============================================ */ + +.cko-certificate-generation { + background: #f9f9f9; + border: 1px solid #dcdcdc; + border-radius: 8px; + padding: 24px; + margin: 20px 0; + text-align: center; +} + +.cko-certificate-generation .button { + margin-top: 16px; +} + +/* ============================================ + STATUS MESSAGES (REDESIGNED) + ============================================ */ + +.cko-status-message { + margin-top: 16px; + padding: 14px 18px; + border-radius: 6px; + font-size: 13px; + line-height: 1.6; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.cko-status-message::before { + font-size: 18px; + flex-shrink: 0; +} + +.cko-status-message.notice::before { + content: "ℹ️"; +} + +.cko-status-message.notice-success { + background: #f0f9f4; + border-left: 4px solid #00a32a; + color: #1d2327; +} + +.cko-status-message.notice-success::before { + content: "✓"; + color: #00a32a; + font-weight: bold; +} + +.cko-status-message.notice-error { + background: #fcf0f1; + border-left: 4px solid #d63638; + color: #1d2327; +} + +.cko-status-message.notice-error::before { + content: "✗"; + color: #d63638; + font-weight: bold; +} + +.cko-status-message.notice-warning { + background: #fff8e5; + border-left: 4px solid #f0b849; + color: #1d2327; +} + +.cko-status-message.notice-warning::before { + content: "⚠"; + color: #f0b849; +} + +/* ============================================ + FORM FIELDS (IMPROVED) + ============================================ */ + +.woocommerce .form-table td input[type="text"], +.woocommerce .form-table td input[type="email"], +.woocommerce .form-table td select, +.woocommerce .form-table td textarea { + width: 100%; + max-width: 600px; + padding: 10px 12px; + border: 1px solid #8c8f94; + border-radius: 4px; + font-size: 14px; + transition: all 0.2s ease; +} + +.woocommerce .form-table td input[type="text"]:focus, +.woocommerce .form-table td input[type="email"]:focus, +.woocommerce .form-table td select:focus, +.woocommerce .form-table td textarea:focus { + border-color: #2271b1; + box-shadow: 0 0 0 1px #2271b1; + outline: none; +} + +.woocommerce .form-table td .description { + display: block; + margin-top: 8px; + font-size: 13px; + line-height: 1.6; + color: #646970; + max-width: 800px; +} + +.woocommerce .form-table th.titledesc label { + font-weight: 600; + color: #1d2327; + font-size: 14px; +} + +/* ============================================ + FIELD GROUPS + ============================================ */ + +.cko-field-group { + margin: 20px 0; +} + +.cko-field-group label { + display: block; + font-weight: 600; + color: #1d2327; + margin-bottom: 8px; + font-size: 14px; +} + +.cko-field-group .description { + margin-top: 6px; + font-size: 13px; + color: #646970; +} + +/* ============================================ + QUICK ACTIONS BAR + ============================================ */ + +.cko-quick-actions { + display: flex; + gap: 12px; + margin: 24px 0; + padding: 16px; + background: #f9f9f9; + border-radius: 6px; + flex-wrap: wrap; +} + +.cko-quick-action { + padding: 8px 16px; + background: #ffffff; + border: 1px solid #dcdcdc; + border-radius: 4px; + text-decoration: none; + color: #2271b1; + font-size: 13px; + font-weight: 500; + transition: all 0.2s ease; +} + +.cko-quick-action:hover { + background: #f0f6fc; + border-color: #2271b1; + color: #135e96; +} + +/* ============================================ + RESPONSIVE DESIGN + ============================================ */ + +@media (max-width: 782px) { + .cko-settings-card { + padding: 16px; + margin-bottom: 16px; + } + + .cko-card-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .cko-progress-steps { + flex-direction: column; + gap: 20px; + } + + .cko-progress-step::after { + display: none; + } + + .cko-progress-step { + width: 100%; + } + + .cko-file-upload-area { + padding: 24px 16px; + } + + .cko-quick-actions { + flex-direction: column; + } + + .cko-quick-action { + width: 100%; + text-align: center; + } +} + +/* ============================================ + ANIMATIONS + ============================================ */ + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.cko-settings-card { + animation: fadeIn 0.3s ease; +} + +.cko-status-message { + animation: fadeIn 0.2s ease; +} + +/* ============================================ + UTILITY CLASSES + ============================================ */ + +.cko-text-muted { + color: #646970; +} + +.cko-text-success { + color: #00a32a; +} + +.cko-text-error { + color: #d63638; +} + +.cko-text-warning { + color: #f0b849; +} + +.cko-mt-16 { + margin-top: 16px; +} + +.cko-mb-16 { + margin-bottom: 16px; +} + +.cko-mt-24 { + margin-top: 24px; +} + +.cko-mb-24 { + margin-bottom: 24px; +} diff --git a/assets/css/checkoutcom-styles.css b/checkout-com-unified-payments-api/assets/css/checkoutcom-styles.css similarity index 100% rename from assets/css/checkoutcom-styles.css rename to checkout-com-unified-payments-api/assets/css/checkoutcom-styles.css diff --git a/assets/css/multi-iframe.css b/checkout-com-unified-payments-api/assets/css/multi-iframe.css similarity index 100% rename from assets/css/multi-iframe.css rename to checkout-com-unified-payments-api/assets/css/multi-iframe.css diff --git a/assets/css/normalize.css b/checkout-com-unified-payments-api/assets/css/normalize.css similarity index 100% rename from assets/css/normalize.css rename to checkout-com-unified-payments-api/assets/css/normalize.css diff --git a/assets/css/style.css b/checkout-com-unified-payments-api/assets/css/style.css similarity index 100% rename from assets/css/style.css rename to checkout-com-unified-payments-api/assets/css/style.css diff --git a/assets/images/addcard.svg b/checkout-com-unified-payments-api/assets/images/addcard.svg similarity index 100% rename from assets/images/addcard.svg rename to checkout-com-unified-payments-api/assets/images/addcard.svg diff --git a/assets/images/alipay.svg b/checkout-com-unified-payments-api/assets/images/alipay.svg similarity index 100% rename from assets/images/alipay.svg rename to checkout-com-unified-payments-api/assets/images/alipay.svg diff --git a/assets/images/amex.svg b/checkout-com-unified-payments-api/assets/images/amex.svg similarity index 100% rename from assets/images/amex.svg rename to checkout-com-unified-payments-api/assets/images/amex.svg diff --git a/assets/images/applepay.svg b/checkout-com-unified-payments-api/assets/images/applepay.svg similarity index 100% rename from assets/images/applepay.svg rename to checkout-com-unified-payments-api/assets/images/applepay.svg diff --git a/assets/images/bancontact.svg b/checkout-com-unified-payments-api/assets/images/bancontact.svg similarity index 100% rename from assets/images/bancontact.svg rename to checkout-com-unified-payments-api/assets/images/bancontact.svg diff --git a/assets/images/bank.svg b/checkout-com-unified-payments-api/assets/images/bank.svg similarity index 100% rename from assets/images/bank.svg rename to checkout-com-unified-payments-api/assets/images/bank.svg diff --git a/assets/images/boleto.svg b/checkout-com-unified-payments-api/assets/images/boleto.svg similarity index 100% rename from assets/images/boleto.svg rename to checkout-com-unified-payments-api/assets/images/boleto.svg diff --git a/assets/images/card-icons/american express.svg b/checkout-com-unified-payments-api/assets/images/card-icons/american express.svg similarity index 100% rename from assets/images/card-icons/american express.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/american express.svg diff --git a/assets/images/card-icons/card-error.svg b/checkout-com-unified-payments-api/assets/images/card-icons/card-error.svg similarity index 100% rename from assets/images/card-icons/card-error.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/card-error.svg diff --git a/assets/images/card-icons/card.svg b/checkout-com-unified-payments-api/assets/images/card-icons/card.svg similarity index 100% rename from assets/images/card-icons/card.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/card.svg diff --git a/assets/images/card-icons/cartes bancaires.svg b/checkout-com-unified-payments-api/assets/images/card-icons/cartes bancaires.svg similarity index 100% rename from assets/images/card-icons/cartes bancaires.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/cartes bancaires.svg diff --git a/assets/images/card-icons/cvv-error.svg b/checkout-com-unified-payments-api/assets/images/card-icons/cvv-error.svg similarity index 100% rename from assets/images/card-icons/cvv-error.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/cvv-error.svg diff --git a/assets/images/card-icons/cvv.svg b/checkout-com-unified-payments-api/assets/images/card-icons/cvv.svg similarity index 100% rename from assets/images/card-icons/cvv.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/cvv.svg diff --git a/assets/images/card-icons/diners club.svg b/checkout-com-unified-payments-api/assets/images/card-icons/diners club.svg similarity index 100% rename from assets/images/card-icons/diners club.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/diners club.svg diff --git a/assets/images/card-icons/discover.svg b/checkout-com-unified-payments-api/assets/images/card-icons/discover.svg similarity index 100% rename from assets/images/card-icons/discover.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/discover.svg diff --git a/assets/images/card-icons/error.svg b/checkout-com-unified-payments-api/assets/images/card-icons/error.svg similarity index 100% rename from assets/images/card-icons/error.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/error.svg diff --git a/assets/images/card-icons/exp-date-error.svg b/checkout-com-unified-payments-api/assets/images/card-icons/exp-date-error.svg similarity index 100% rename from assets/images/card-icons/exp-date-error.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/exp-date-error.svg diff --git a/assets/images/card-icons/exp-date.svg b/checkout-com-unified-payments-api/assets/images/card-icons/exp-date.svg similarity index 100% rename from assets/images/card-icons/exp-date.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/exp-date.svg diff --git a/assets/images/card-icons/jcb.svg b/checkout-com-unified-payments-api/assets/images/card-icons/jcb.svg similarity index 100% rename from assets/images/card-icons/jcb.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/jcb.svg diff --git a/assets/images/card-icons/loading.svg b/checkout-com-unified-payments-api/assets/images/card-icons/loading.svg similarity index 100% rename from assets/images/card-icons/loading.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/loading.svg diff --git a/assets/images/card-icons/mada.svg b/checkout-com-unified-payments-api/assets/images/card-icons/mada.svg similarity index 100% rename from assets/images/card-icons/mada.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/mada.svg diff --git a/assets/images/card-icons/maestro.svg b/checkout-com-unified-payments-api/assets/images/card-icons/maestro.svg similarity index 100% rename from assets/images/card-icons/maestro.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/maestro.svg diff --git a/assets/images/card-icons/mastercard.svg b/checkout-com-unified-payments-api/assets/images/card-icons/mastercard.svg similarity index 100% rename from assets/images/card-icons/mastercard.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/mastercard.svg diff --git a/assets/images/card-icons/visa.svg b/checkout-com-unified-payments-api/assets/images/card-icons/visa.svg similarity index 100% rename from assets/images/card-icons/visa.svg rename to checkout-com-unified-payments-api/assets/images/card-icons/visa.svg diff --git a/assets/images/card.svg b/checkout-com-unified-payments-api/assets/images/card.svg similarity index 100% rename from assets/images/card.svg rename to checkout-com-unified-payments-api/assets/images/card.svg diff --git a/assets/images/cartesbancaires.svg b/checkout-com-unified-payments-api/assets/images/cartesbancaires.svg similarity index 100% rename from assets/images/cartesbancaires.svg rename to checkout-com-unified-payments-api/assets/images/cartesbancaires.svg diff --git a/assets/images/dinersclub.svg b/checkout-com-unified-payments-api/assets/images/dinersclub.svg similarity index 100% rename from assets/images/dinersclub.svg rename to checkout-com-unified-payments-api/assets/images/dinersclub.svg diff --git a/assets/images/discover.svg b/checkout-com-unified-payments-api/assets/images/discover.svg similarity index 100% rename from assets/images/discover.svg rename to checkout-com-unified-payments-api/assets/images/discover.svg diff --git a/assets/images/eps.svg b/checkout-com-unified-payments-api/assets/images/eps.svg similarity index 100% rename from assets/images/eps.svg rename to checkout-com-unified-payments-api/assets/images/eps.svg diff --git a/assets/images/fawry.svg b/checkout-com-unified-payments-api/assets/images/fawry.svg similarity index 100% rename from assets/images/fawry.svg rename to checkout-com-unified-payments-api/assets/images/fawry.svg diff --git a/assets/images/googlepay.svg b/checkout-com-unified-payments-api/assets/images/googlepay.svg similarity index 100% rename from assets/images/googlepay.svg rename to checkout-com-unified-payments-api/assets/images/googlepay.svg diff --git a/assets/images/ideal.svg b/checkout-com-unified-payments-api/assets/images/ideal.svg similarity index 100% rename from assets/images/ideal.svg rename to checkout-com-unified-payments-api/assets/images/ideal.svg diff --git a/assets/images/images/addcard.svg b/checkout-com-unified-payments-api/assets/images/images/addcard.svg similarity index 100% rename from assets/images/images/addcard.svg rename to checkout-com-unified-payments-api/assets/images/images/addcard.svg diff --git a/assets/images/images/alipay.svg b/checkout-com-unified-payments-api/assets/images/images/alipay.svg similarity index 100% rename from assets/images/images/alipay.svg rename to checkout-com-unified-payments-api/assets/images/images/alipay.svg diff --git a/assets/images/images/amex.svg b/checkout-com-unified-payments-api/assets/images/images/amex.svg similarity index 100% rename from assets/images/images/amex.svg rename to checkout-com-unified-payments-api/assets/images/images/amex.svg diff --git a/assets/images/images/applepay.svg b/checkout-com-unified-payments-api/assets/images/images/applepay.svg similarity index 100% rename from assets/images/images/applepay.svg rename to checkout-com-unified-payments-api/assets/images/images/applepay.svg diff --git a/assets/images/images/bancontact.svg b/checkout-com-unified-payments-api/assets/images/images/bancontact.svg similarity index 100% rename from assets/images/images/bancontact.svg rename to checkout-com-unified-payments-api/assets/images/images/bancontact.svg diff --git a/assets/images/images/boleto.svg b/checkout-com-unified-payments-api/assets/images/images/boleto.svg similarity index 100% rename from assets/images/images/boleto.svg rename to checkout-com-unified-payments-api/assets/images/images/boleto.svg diff --git a/assets/images/images/card-icons/american express.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/american express.svg similarity index 100% rename from assets/images/images/card-icons/american express.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/american express.svg diff --git a/assets/images/images/card-icons/card-error.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/card-error.svg similarity index 100% rename from assets/images/images/card-icons/card-error.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/card-error.svg diff --git a/assets/images/images/card-icons/card.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/card.svg similarity index 100% rename from assets/images/images/card-icons/card.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/card.svg diff --git a/assets/images/images/card-icons/cvv-error.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/cvv-error.svg similarity index 100% rename from assets/images/images/card-icons/cvv-error.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/cvv-error.svg diff --git a/assets/images/images/card-icons/cvv.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/cvv.svg similarity index 100% rename from assets/images/images/card-icons/cvv.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/cvv.svg diff --git a/assets/images/images/card-icons/diners club.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/diners club.svg similarity index 100% rename from assets/images/images/card-icons/diners club.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/diners club.svg diff --git a/assets/images/images/card-icons/discover.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/discover.svg similarity index 100% rename from assets/images/images/card-icons/discover.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/discover.svg diff --git a/assets/images/images/card-icons/error.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/error.svg similarity index 100% rename from assets/images/images/card-icons/error.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/error.svg diff --git a/assets/images/images/card-icons/exp-date-error.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/exp-date-error.svg similarity index 100% rename from assets/images/images/card-icons/exp-date-error.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/exp-date-error.svg diff --git a/assets/images/images/card-icons/exp-date.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/exp-date.svg similarity index 100% rename from assets/images/images/card-icons/exp-date.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/exp-date.svg diff --git a/assets/images/images/card-icons/jcb.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/jcb.svg similarity index 100% rename from assets/images/images/card-icons/jcb.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/jcb.svg diff --git a/assets/images/images/card-icons/loading.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/loading.svg similarity index 100% rename from assets/images/images/card-icons/loading.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/loading.svg diff --git a/assets/images/images/card-icons/mada.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/mada.svg similarity index 100% rename from assets/images/images/card-icons/mada.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/mada.svg diff --git a/assets/images/images/card-icons/maestro.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/maestro.svg similarity index 100% rename from assets/images/images/card-icons/maestro.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/maestro.svg diff --git a/assets/images/images/card-icons/mastercard.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/mastercard.svg similarity index 100% rename from assets/images/images/card-icons/mastercard.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/mastercard.svg diff --git a/assets/images/images/card-icons/visa.svg b/checkout-com-unified-payments-api/assets/images/images/card-icons/visa.svg similarity index 100% rename from assets/images/images/card-icons/visa.svg rename to checkout-com-unified-payments-api/assets/images/images/card-icons/visa.svg diff --git a/assets/images/images/card.svg b/checkout-com-unified-payments-api/assets/images/images/card.svg similarity index 100% rename from assets/images/images/card.svg rename to checkout-com-unified-payments-api/assets/images/images/card.svg diff --git a/assets/images/images/dinersclub.svg b/checkout-com-unified-payments-api/assets/images/images/dinersclub.svg similarity index 100% rename from assets/images/images/dinersclub.svg rename to checkout-com-unified-payments-api/assets/images/images/dinersclub.svg diff --git a/assets/images/images/discover.svg b/checkout-com-unified-payments-api/assets/images/images/discover.svg similarity index 100% rename from assets/images/images/discover.svg rename to checkout-com-unified-payments-api/assets/images/images/discover.svg diff --git a/assets/images/images/eps.svg b/checkout-com-unified-payments-api/assets/images/images/eps.svg similarity index 100% rename from assets/images/images/eps.svg rename to checkout-com-unified-payments-api/assets/images/images/eps.svg diff --git a/assets/images/images/fawry.svg b/checkout-com-unified-payments-api/assets/images/images/fawry.svg similarity index 100% rename from assets/images/images/fawry.svg rename to checkout-com-unified-payments-api/assets/images/images/fawry.svg diff --git a/assets/images/images/googlepay.svg b/checkout-com-unified-payments-api/assets/images/images/googlepay.svg similarity index 100% rename from assets/images/images/googlepay.svg rename to checkout-com-unified-payments-api/assets/images/images/googlepay.svg diff --git a/assets/images/images/ideal.svg b/checkout-com-unified-payments-api/assets/images/images/ideal.svg similarity index 100% rename from assets/images/images/ideal.svg rename to checkout-com-unified-payments-api/assets/images/images/ideal.svg diff --git a/assets/images/images/jcb.svg b/checkout-com-unified-payments-api/assets/images/images/jcb.svg similarity index 100% rename from assets/images/images/jcb.svg rename to checkout-com-unified-payments-api/assets/images/images/jcb.svg diff --git a/assets/images/images/klarna.svg b/checkout-com-unified-payments-api/assets/images/images/klarna.svg similarity index 100% rename from assets/images/images/klarna.svg rename to checkout-com-unified-payments-api/assets/images/images/klarna.svg diff --git a/assets/images/images/knet.svg b/checkout-com-unified-payments-api/assets/images/images/knet.svg similarity index 100% rename from assets/images/images/knet.svg rename to checkout-com-unified-payments-api/assets/images/images/knet.svg diff --git a/assets/images/images/mada.svg b/checkout-com-unified-payments-api/assets/images/images/mada.svg similarity index 100% rename from assets/images/images/mada.svg rename to checkout-com-unified-payments-api/assets/images/images/mada.svg diff --git a/assets/images/images/maestro.svg b/checkout-com-unified-payments-api/assets/images/images/maestro.svg similarity index 100% rename from assets/images/images/maestro.svg rename to checkout-com-unified-payments-api/assets/images/images/maestro.svg diff --git a/assets/images/images/mastercard.svg b/checkout-com-unified-payments-api/assets/images/images/mastercard.svg similarity index 100% rename from assets/images/images/mastercard.svg rename to checkout-com-unified-payments-api/assets/images/images/mastercard.svg diff --git a/assets/images/images/paypal.svg b/checkout-com-unified-payments-api/assets/images/images/paypal.svg similarity index 100% rename from assets/images/images/paypal.svg rename to checkout-com-unified-payments-api/assets/images/images/paypal.svg diff --git a/assets/images/images/poli.svg b/checkout-com-unified-payments-api/assets/images/images/poli.svg similarity index 100% rename from assets/images/images/poli.svg rename to checkout-com-unified-payments-api/assets/images/images/poli.svg diff --git a/assets/images/images/sepa.svg b/checkout-com-unified-payments-api/assets/images/images/sepa.svg similarity index 100% rename from assets/images/images/sepa.svg rename to checkout-com-unified-payments-api/assets/images/images/sepa.svg diff --git a/assets/images/images/sofort.svg b/checkout-com-unified-payments-api/assets/images/images/sofort.svg similarity index 100% rename from assets/images/images/sofort.svg rename to checkout-com-unified-payments-api/assets/images/images/sofort.svg diff --git a/assets/images/images/visa.svg b/checkout-com-unified-payments-api/assets/images/images/visa.svg similarity index 100% rename from assets/images/images/visa.svg rename to checkout-com-unified-payments-api/assets/images/images/visa.svg diff --git a/assets/images/information.svg b/checkout-com-unified-payments-api/assets/images/information.svg similarity index 100% rename from assets/images/information.svg rename to checkout-com-unified-payments-api/assets/images/information.svg diff --git a/assets/images/jcb.svg b/checkout-com-unified-payments-api/assets/images/jcb.svg similarity index 100% rename from assets/images/jcb.svg rename to checkout-com-unified-payments-api/assets/images/jcb.svg diff --git a/assets/images/klarna.svg b/checkout-com-unified-payments-api/assets/images/klarna.svg similarity index 100% rename from assets/images/klarna.svg rename to checkout-com-unified-payments-api/assets/images/klarna.svg diff --git a/assets/images/knet.svg b/checkout-com-unified-payments-api/assets/images/knet.svg similarity index 100% rename from assets/images/knet.svg rename to checkout-com-unified-payments-api/assets/images/knet.svg diff --git a/assets/images/mada.svg b/checkout-com-unified-payments-api/assets/images/mada.svg similarity index 100% rename from assets/images/mada.svg rename to checkout-com-unified-payments-api/assets/images/mada.svg diff --git a/assets/images/maestro.svg b/checkout-com-unified-payments-api/assets/images/maestro.svg similarity index 100% rename from assets/images/maestro.svg rename to checkout-com-unified-payments-api/assets/images/maestro.svg diff --git a/assets/images/mastercard.svg b/checkout-com-unified-payments-api/assets/images/mastercard.svg similarity index 100% rename from assets/images/mastercard.svg rename to checkout-com-unified-payments-api/assets/images/mastercard.svg diff --git a/assets/images/multibanco.svg b/checkout-com-unified-payments-api/assets/images/multibanco.svg similarity index 100% rename from assets/images/multibanco.svg rename to checkout-com-unified-payments-api/assets/images/multibanco.svg diff --git a/assets/images/paypal.svg b/checkout-com-unified-payments-api/assets/images/paypal.svg similarity index 100% rename from assets/images/paypal.svg rename to checkout-com-unified-payments-api/assets/images/paypal.svg diff --git a/assets/images/poli.svg b/checkout-com-unified-payments-api/assets/images/poli.svg similarity index 100% rename from assets/images/poli.svg rename to checkout-com-unified-payments-api/assets/images/poli.svg diff --git a/assets/images/qpay.svg b/checkout-com-unified-payments-api/assets/images/qpay.svg similarity index 100% rename from assets/images/qpay.svg rename to checkout-com-unified-payments-api/assets/images/qpay.svg diff --git a/assets/images/sepa.svg b/checkout-com-unified-payments-api/assets/images/sepa.svg similarity index 100% rename from assets/images/sepa.svg rename to checkout-com-unified-payments-api/assets/images/sepa.svg diff --git a/assets/images/sofort.svg b/checkout-com-unified-payments-api/assets/images/sofort.svg similarity index 100% rename from assets/images/sofort.svg rename to checkout-com-unified-payments-api/assets/images/sofort.svg diff --git a/assets/images/visa.svg b/checkout-com-unified-payments-api/assets/images/visa.svg similarity index 100% rename from assets/images/visa.svg rename to checkout-com-unified-payments-api/assets/images/visa.svg diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-certificate-upload.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-certificate-upload.js new file mode 100644 index 00000000..3a67b3ae --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-certificate-upload.js @@ -0,0 +1,167 @@ +/** + * Admin Apple Pay Certificate Upload + * + * Handles certificate upload from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $uploadButton = $('#cko-upload-certificate-button'); + const $fileInput = $('#cko-certificate-upload'); + const $statusDiv = $('#cko-certificate-status'); + + if (!$uploadButton.length) { + return; // Exit if not on the right page + } + + $uploadButton.on('click', function(e) { + e.preventDefault(); + + const file = $fileInput[0].files[0]; + + if (!file) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCertificateData.i18n.error + ' ' + ckoCertificateData.i18n.noFile + '

') + .show(); + return; + } + + // Validate file type + if (!file.name.toLowerCase().endsWith('.cer')) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCertificateData.i18n.error + ' ' + 'Please select a valid .cer certificate file.' + '

') + .show(); + return; + } + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoCertificateData.i18n.uploading); + $statusDiv.hide().removeClass('notice notice-success notice-error'); + + // Read file as base64 + const reader = new FileReader(); + reader.onload = function(e) { + // Convert to base64 + const base64Content = e.target.result.split(',')[1]; // Remove data:application/x-x509-ca-cert;base64, prefix + + // Make AJAX request + $.ajax({ + url: ckoCertificateData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_upload_apple_pay_certificate', + nonce: ckoCertificateData.nonce, + certificate: base64Content, + filename: file.name + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success) { + // Show success message + $statusDiv + .removeClass('notice-error') + .addClass('cko-status-message notice notice-success notice-success') + .html('

' + ckoCertificateData.i18n.success + '

') + .show(); + + // Reset button and file input after 2 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + $fileInput.val(''); + }, 2000); + } else { + // Show error message with full details + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else if (response.data) { + errorMsg = JSON.stringify(response.data); + } else { + errorMsg = 'Unknown error'; + } + + // Show error data if available + let errorDetails = ''; + if (response.data && response.data.error_data) { + const errorData = response.data.error_data; + if (errorData.error_codes && Array.isArray(errorData.error_codes)) { + errorDetails += '
Error Codes: ' + errorData.error_codes.join(', '); + } + if (errorData.error_type) { + errorDetails += '
Error Type: ' + errorData.error_type; + } + if (errorData.details) { + errorDetails += '
Details: ' + (typeof errorData.details === 'string' ? errorData.details : JSON.stringify(errorData.details)); + } + } + + // Show debug info if available + let debugInfo = ''; + if (response.data && response.data.debug_info) { + debugInfo = '
Debug: ' + + 'URL: ' + response.data.debug_info.url + ', ' + + 'Account Type: ' + response.data.debug_info.account_type + ', ' + + 'Environment: ' + response.data.debug_info.environment; + if (response.data.debug_info.certificate_length) { + debugInfo += ', Certificate Length: ' + response.data.debug_info.certificate_length; + } + if (response.data.debug_info.certificate_format) { + debugInfo += ', Format: ' + response.data.debug_info.certificate_format; + } + debugInfo += ''; + } + + // Show full response details if available + if (response.data && response.data.details) { + errorDetails += '
Full Response: ' + response.data.details + ''; + } + + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCertificateData.i18n.error + ' ' + errorMsg + errorDetails + debugInfo + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCertificateData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }; + + reader.onerror = function() { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCertificateData.i18n.error + ' ' + 'Failed to read certificate file.' + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + }; + + // Read file as data URL (base64) + reader.readAsDataURL(file); + }); + }); +})(jQuery); + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-csr.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-csr.js new file mode 100644 index 00000000..1bf21e7a --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-csr.js @@ -0,0 +1,148 @@ +/** + * Admin Apple Pay CSR Generation + * + * Handles CSR generation from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $generateButton = $('#cko-generate-csr-button'); + const $statusDiv = $('#cko-csr-status'); + const $instructionsDiv = $('#cko-csr-instructions'); + + if (!$generateButton.length) { + return; // Exit if not on the right page + } + + $generateButton.on('click', function(e) { + e.preventDefault(); + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoCsrData.i18n.generating); + $statusDiv.hide().removeClass('cko-status-message notice notice-success notice-error notice-success'); + + // Make AJAX request + $.ajax({ + url: ckoCsrData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_generate_apple_pay_csr', + nonce: ckoCsrData.nonce, + protocol_version: 'ec_v1' + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success && response.data) { + // Download the CSR file + downloadCSR(response.data.csr_content, response.data.filename); + + // Show success message + $statusDiv + .removeClass('notice notice-error') + .addClass('cko-status-message notice notice-success notice-success') + .html('

' + ckoCsrData.i18n.success + '

') + .show(); + + // Show instructions + $instructionsDiv.slideDown(); + + // Reset button after 2 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + }, 2000); + } else { + // Show error message with full details + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else if (response.data) { + errorMsg = JSON.stringify(response.data); + } else { + errorMsg = 'Unknown error'; + } + + // If there's a details field, show it + let errorDetails = ''; + if (response.data && response.data.details) { + try { + const details = typeof response.data.details === 'string' + ? JSON.parse(response.data.details) + : response.data.details; + if (details.error_codes) { + errorDetails = '
Error Codes: ' + details.error_codes.join(', '); + } + if (details.error_type) { + errorDetails = '
Error Type: ' + details.error_type + errorDetails; + } + if (details.message && details.message !== errorMsg) { + errorDetails = '
Details: ' + details.message + errorDetails; + } + } catch (e) { + errorDetails = '
Raw Response: ' + response.data.details; + } + } + + // Show debug info if available + let debugInfo = ''; + if (response.data && response.data.debug_info) { + debugInfo = '
Debug: ' + + 'URL: ' + response.data.debug_info.url + ', ' + + 'Account Type: ' + response.data.debug_info.account_type + ', ' + + 'Environment: ' + response.data.debug_info.environment + + ''; + } + + $statusDiv + .removeClass('notice notice-success notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCsrData.i18n.error + ' ' + errorMsg + errorDetails + debugInfo + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice notice-success notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoCsrData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }); + + /** + * Download CSR file + * + * @param {string} content CSR file content + * @param {string} filename Filename for download + */ + function downloadCSR(content, filename) { + // Create a blob with the CSR content + const blob = new Blob([content], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + + // Create a temporary anchor element and trigger download + const link = document.createElement('a'); + link.href = url; + link.download = filename || 'cko.csr'; + document.body.appendChild(link); + link.click(); + + // Clean up + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } + }); +})(jQuery); + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-domain-association.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-domain-association.js new file mode 100644 index 00000000..295fbcb5 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-domain-association.js @@ -0,0 +1,139 @@ +/** + * Admin Apple Pay Domain Association Upload + * + * Handles domain association file upload from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $uploadButton = $('#cko-upload-domain-association-button'); + const $fileInput = $('#cko-domain-association-upload'); + const $statusDiv = $('#cko-domain-association-status'); + + if (!$uploadButton.length) { + return; // Exit if not on the right page + } + + $uploadButton.on('click', function(e) { + e.preventDefault(); + + const file = $fileInput[0].files[0]; + + if (!file) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoDomainAssociationData.i18n.error + ' ' + ckoDomainAssociationData.i18n.noFile + '

') + .show(); + return; + } + + // Validate file type + if (!file.name.toLowerCase().endsWith('.txt')) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoDomainAssociationData.i18n.error + ' Please select a valid .txt domain association file.' + '

') + .show(); + return; + } + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoDomainAssociationData.i18n.uploading); + $statusDiv.hide().removeClass('notice notice-success notice-error'); + + // Read file as text + const reader = new FileReader(); + reader.onload = function(e) { + const fileContent = e.target.result; + + // Make AJAX request + $.ajax({ + url: ckoDomainAssociationData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_upload_apple_pay_domain_association', + nonce: ckoDomainAssociationData.nonce, + file_content: fileContent, + filename: file.name + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success) { + // Show success message with file location + let successMsg = '

' + ckoDomainAssociationData.i18n.success + '

'; + if (response.data && response.data.file_url) { + successMsg += '

File URL: ' + response.data.file_url + '

'; + } + + $statusDiv + .removeClass('notice-error') + .addClass('cko-status-message notice notice-success') + .html(successMsg) + .show(); + + // Reset button and file input after 2 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + $fileInput.val(''); + }, 2000); + } else { + // Show error message + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else if (response.data) { + errorMsg = JSON.stringify(response.data); + } else { + errorMsg = 'Unknown error'; + } + + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoDomainAssociationData.i18n.error + ' ' + errorMsg + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoDomainAssociationData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }; + + reader.onerror = function() { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoDomainAssociationData.i18n.error + ' Failed to read domain association file.' + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + }; + + // Read file as text + reader.readAsText(file); + }); + }); +})(jQuery); + + + + + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-certificate.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-certificate.js new file mode 100644 index 00000000..7d38d415 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-certificate.js @@ -0,0 +1,128 @@ +/** + * Admin script for generating Apple Pay merchant certificate and private key. + * + * @package wc_checkout_com + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + // Handle Generate Certificate and Key button click + $('#cko-generate-merchant-certificate-button').on('click', function(e) { + e.preventDefault(); + + const $button = $(this); + const $statusDiv = $('#cko-merchant-certificate-status'); + const originalButtonText = $button.text(); + + // Disable button and show loading + $button.prop('disabled', true).text(ckoMerchantCertificateData.i18n.generating); + $statusDiv.hide().removeClass('cko-status-message notice notice-success notice-error notice-success'); + + // Make AJAX request + $.ajax({ + url: ckoMerchantCertificateData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_generate_apple_pay_merchant_certificate', + nonce: ckoMerchantCertificateData.nonce + }, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + success: function(response) { + if (response.success && response.data) { + // Decode base64 certificate and key + const certificate = atob(response.data.certificate); + const privateKey = atob(response.data.private_key); + + // Create download links + const certificateBlob = new Blob([certificate], { type: 'application/x-pem-file' }); + const privateKeyBlob = new Blob([privateKey], { type: 'application/x-pem-file' }); + + const certificateUrl = URL.createObjectURL(certificateBlob); + const privateKeyUrl = URL.createObjectURL(privateKeyBlob); + + // Create download links + const certificateLink = $('') + .attr('href', certificateUrl) + .attr('download', response.data.certificate_filename) + .text('Download Certificate') + .addClass('button button-secondary') + .css('margin-right', '10px'); + + const privateKeyLink = $('') + .attr('href', privateKeyUrl) + .attr('download', response.data.private_key_filename) + .text('Download Private Key') + .addClass('button button-secondary'); + + // Show success message with download links + $statusDiv + .addClass('cko-status-message notice notice-success notice-success') + .html('

' + response.data.message + '

' + + '

' + certificateLink[0].outerHTML + privateKeyLink[0].outerHTML + '

') + .show(); + + // Trigger downloads after a short delay + setTimeout(function() { + certificateLink[0].click(); + setTimeout(function() { + privateKeyLink[0].click(); + }, 500); + }, 100); + + // Clean up object URLs after downloads + setTimeout(function() { + URL.revokeObjectURL(certificateUrl); + URL.revokeObjectURL(privateKeyUrl); + }, 1000); + } else { + // Show error message + let errorMessage = ckoMerchantCertificateData.i18n.error + ' '; + if (response.data && response.data.message) { + errorMessage += response.data.message; + } else { + errorMessage += 'Unknown error occurred.'; + } + + // Show detailed error if available + if (response.data && response.data.error_codes) { + errorMessage += '
Error codes: ' + response.data.error_codes.join(', '); + } + if (response.data && response.data.error_type) { + errorMessage += '
Error type: ' + response.data.error_type; + } + if (response.data && response.data.debug_info) { + errorMessage += '
Debug: ' + JSON.stringify(response.data.debug_info); + } + + $statusDiv + .addClass('cko-status-message notice notice-error') + .html('

' + errorMessage + '

') + .show(); + } + }, + error: function(xhr, status, error) { + let errorMessage = ckoMerchantCertificateData.i18n.error + ' '; + if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { + errorMessage += xhr.responseJSON.data.message; + } else { + errorMessage += error || 'Network error occurred.'; + } + + $statusDiv + .addClass('notice notice-error') + .html('

' + errorMessage + '

') + .show(); + }, + complete: function() { + // Re-enable button + $button.prop('disabled', false).text(originalButtonText); + } + }); + }); + }); +})(jQuery); diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-certificate.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-certificate.js new file mode 100644 index 00000000..2813e6f1 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-certificate.js @@ -0,0 +1,147 @@ +/** + * Admin Apple Pay Merchant Identity Certificate Upload + * + * Handles merchant identity certificate upload and conversion from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $uploadButton = $('#cko-upload-merchant-identity-certificate-button'); + const $fileInput = $('#cko-merchant-identity-certificate-upload'); + const $statusDiv = $('#cko-merchant-identity-certificate-status'); + + if (!$uploadButton.length) { + return; // Exit if not on the right page + } + + $uploadButton.on('click', function(e) { + e.preventDefault(); + + const file = $fileInput[0].files[0]; + + if (!file) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCertificateData.i18n.error + ' ' + ckoMerchantIdentityCertificateData.i18n.noFile + '

') + .show(); + return; + } + + // Validate file type + if (!file.name.toLowerCase().endsWith('.cer')) { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCertificateData.i18n.error + ' Please select a valid .cer certificate file.' + '

') + .show(); + return; + } + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoMerchantIdentityCertificateData.i18n.uploading); + $statusDiv.hide().removeClass('notice notice-success notice-error'); + + // Read file as base64 + const reader = new FileReader(); + reader.onload = function(e) { + // Convert to base64 + const base64Content = e.target.result.split(',')[1]; // Remove data:application/x-x509-ca-cert;base64, prefix + + // Make AJAX request + $.ajax({ + url: ckoMerchantIdentityCertificateData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_upload_apple_pay_merchant_identity_certificate', + nonce: ckoMerchantIdentityCertificateData.nonce, + certificate: base64Content, + filename: file.name + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success) { + // Show success message with absolute paths + let successMsg = '

' + ckoMerchantIdentityCertificateData.i18n.success + '

'; + if (response.data && response.data.certificate_path) { + successMsg += '

Certificate (.pem) saved to: ' + response.data.certificate_path + '

'; + } + if (response.data && response.data.key_path) { + successMsg += '

Private Key (.key) saved to: ' + response.data.key_path + '

'; + successMsg += '

Configure these paths:

'; + successMsg += ''; + } else { + successMsg += '

Certificate (.pem) saved to: ' + response.data.certificate_path + '

'; + successMsg += '

Note: Make sure the key file (certificate_sandbox.key) from Step 1 is also saved on your server.

'; + } + + $statusDiv + .removeClass('notice-error') + .addClass('cko-status-message notice notice-success') + .html(successMsg) + .show(); + + // Reset button and file input after 2 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + $fileInput.val(''); + }, 2000); + } else { + // Show error message + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else if (response.data) { + errorMsg = JSON.stringify(response.data); + } else { + errorMsg = 'Unknown error'; + } + + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCertificateData.i18n.error + ' ' + errorMsg + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCertificateData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }; + + reader.onerror = function() { + $statusDiv + .removeClass('notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCertificateData.i18n.error + ' Failed to read certificate file.' + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + }; + + // Read file as data URL (base64) + reader.readAsDataURL(file); + }); + }); +})(jQuery); + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-csr.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-csr.js new file mode 100644 index 00000000..f95b0f16 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-merchant-identity-csr.js @@ -0,0 +1,134 @@ +/** + * Admin Apple Pay Merchant Identity CSR Generation + * + * Handles Merchant Identity CSR generation from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $generateButton = $('#cko-generate-merchant-identity-csr-button'); + const $statusDiv = $('#cko-merchant-identity-csr-status'); + const $instructionsDiv = $('#cko-merchant-identity-csr-instructions'); + + if (!$generateButton.length) { + return; // Exit if not on the right page + } + + $generateButton.on('click', function(e) { + e.preventDefault(); + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoMerchantIdentityCsrData.i18n.generating); + $statusDiv.hide().removeClass('cko-status-message notice notice-success notice-error'); + + // Make AJAX request + $.ajax({ + url: ckoMerchantIdentityCsrData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_generate_apple_pay_merchant_identity_csr', + nonce: ckoMerchantIdentityCsrData.nonce + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success && response.data) { + // Download CSR file + downloadFile( + atob(response.data.csr), + response.data.csr_filename || 'uploadMe.csr', + 'text/plain' + ); + + // Download private key file + downloadFile( + atob(response.data.private_key), + response.data.private_key_filename || 'certificate_sandbox.key', + 'text/plain' + ); + + // Show success message with server paths + let successMsg = '

' + ckoMerchantIdentityCsrData.i18n.success + '

'; + if (response.data && response.data.key_file_path) { + successMsg += '

Private Key saved to: ' + response.data.key_file_path + '

'; + successMsg += '

Note: The CSR file (uploadMe.csr) has been downloaded. Upload it to Apple Developer to get your signed certificate.

'; + } + + $statusDiv + .removeClass('notice notice-error') + .addClass('cko-status-message notice notice-success') + .html(successMsg) + .show(); + + // Show instructions + $instructionsDiv.slideDown(); + + // Reset button after 2 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + }, 2000); + } else { + // Show error message + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else if (response.data) { + errorMsg = JSON.stringify(response.data); + } else { + errorMsg = 'Unknown error'; + } + + $statusDiv + .removeClass('notice notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCsrData.i18n.error + ' ' + errorMsg + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoMerchantIdentityCsrData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }); + + /** + * Download file + * + * @param {string} content File content + * @param {string} filename Filename for download + * @param {string} mimeType MIME type + */ + function downloadFile(content, filename, mimeType) { + const blob = new Blob([content], { type: mimeType || 'text/plain' }); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + // Clean up + setTimeout(function() { + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }, 100); + } + }); +})(jQuery); + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-settings-ux.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-settings-ux.js new file mode 100644 index 00000000..a698360e --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-settings-ux.js @@ -0,0 +1,382 @@ +/** + * Apple Pay Settings UX Enhancements + * + * Features: + * - Progress tracking and status updates + * - Card state management + * - Setup completion detection + * - Visual feedback for user actions + * + * @package wc_checkout_com + */ + +(function($) { + 'use strict'; + + /** + * Apple Pay Settings UX Manager + */ + const ApplePaySettingsUX = { + + /** + * Initialize UX enhancements + */ + init: function() { + this.updateCardStates(); + this.setupProgressTracking(); + this.enhanceFileUploads(); + this.setupStatusUpdates(); + this.setupCollapsibleSections(); + this.addSecurityWarnings(); + }, + + /** + * Update card states based on field completion + */ + updateCardStates: function() { + $('.cko-settings-card').each(function() { + const $card = $(this); + const cardId = $card.data('card-id'); + + if (!cardId) return; + + let isCompleted = false; + let hasError = false; + let isInProgress = false; + + // Check completion based on card type + switch(cardId) { + case 'payment-processing-certificate': + isCompleted = ApplePaySettingsUX.checkPaymentProcessingCertificate(); + break; + case 'domain-association': + isCompleted = ApplePaySettingsUX.checkDomainAssociation(); + break; + case 'merchant-identity-certificate': + isCompleted = ApplePaySettingsUX.checkMerchantIdentityCertificate(); + break; + case 'apple-pay-configuration': + isCompleted = ApplePaySettingsUX.checkApplePayConfiguration(); + break; + case 'express-checkout': + isCompleted = ApplePaySettingsUX.checkExpressCheckout(); + break; + } + + // Update card classes + $card.removeClass('completed in-progress error required'); + + if (hasError) { + $card.addClass('error'); + } else if (isCompleted) { + $card.addClass('completed'); + } else if (isInProgress) { + $card.addClass('in-progress'); + } else { + $card.addClass('required'); + } + + // Update status badge + const $badge = $card.find('.cko-status-badge'); + if ($badge.length) { + $badge.removeClass('completed in-progress required error'); + if (hasError) { + $badge.addClass('error').text('Error'); + } else if (isCompleted) { + $badge.addClass('completed').text('Completed'); + } else if (isInProgress) { + $badge.addClass('in-progress').text('In Progress'); + } else { + $badge.addClass('required').text('Required'); + } + } + }); + }, + + /** + * Check if payment processing certificate is set up + */ + checkPaymentProcessingCertificate: function() { + // Check if certificate has been uploaded + const certificateUploaded = $('#cko-certificate-status').hasClass('notice-success') || + $('input[name="apple_pay_certificate"]').val() !== ''; + return certificateUploaded; + }, + + /** + * Check if domain association is set up + */ + checkDomainAssociation: function() { + // Check if domain association file has been uploaded + const domainFileUploaded = $('#cko-domain-association-status').hasClass('notice-success') || + $('input[name="apple_pay_domain_association"]').val() !== ''; + return domainFileUploaded; + }, + + /** + * Check if merchant identity certificate is set up + */ + checkMerchantIdentityCertificate: function() { + // Check if merchant identity certificate and key paths are configured + const certPath = $('input[name="ckocom_apple_certificate"]').val(); + const keyPath = $('input[name="ckocom_apple_key"]').val(); + return certPath !== '' && keyPath !== ''; + }, + + /** + * Check if Apple Pay configuration is complete + */ + checkApplePayConfiguration: function() { + const merchantId = $('input[name="ckocom_apple_mercahnt_id"]').val(); + const domainName = $('input[name="apple_pay_domain_name"]').val(); + const displayName = $('input[name="apple_pay_display_name"]').val(); + const certPath = $('input[name="ckocom_apple_certificate"]').val(); + const keyPath = $('input[name="ckocom_apple_key"]').val(); + + return merchantId !== '' && + domainName !== '' && + displayName !== '' && + certPath !== '' && + keyPath !== ''; + }, + + /** + * Check if express checkout is configured + */ + checkExpressCheckout: function() { + const expressEnabled = $('input[name="apple_pay_express"]').is(':checked'); + return expressEnabled; + }, + + /** + * Setup progress tracking + */ + setupProgressTracking: function() { + // Update progress steps based on completion + $('.cko-progress-step').each(function(index) { + const $step = $(this); + const stepNumber = index + 1; + + // Check if this step is completed + let isCompleted = false; + let isActive = false; + + switch(stepNumber) { + case 1: + isCompleted = ApplePaySettingsUX.checkPaymentProcessingCertificate(); + isActive = !isCompleted; + break; + case 2: + isCompleted = ApplePaySettingsUX.checkDomainAssociation(); + isActive = !isCompleted && ApplePaySettingsUX.checkPaymentProcessingCertificate(); + break; + case 3: + isCompleted = ApplePaySettingsUX.checkMerchantIdentityCertificate(); + isActive = !isCompleted && + ApplePaySettingsUX.checkPaymentProcessingCertificate() && + ApplePaySettingsUX.checkDomainAssociation(); + break; + } + + $step.removeClass('completed active'); + if (isCompleted) { + $step.addClass('completed'); + } else if (isActive) { + $step.addClass('active'); + } + }); + }, + + /** + * Enhance file upload areas with drag and drop + */ + enhanceFileUploads: function() { + $('.cko-file-upload-area').each(function() { + const $area = $(this); + const $input = $area.find('input[type="file"]'); + + // Drag and drop handlers + $area.on('dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $area.addClass('drag-over'); + }); + + $area.on('dragleave', function(e) { + e.preventDefault(); + e.stopPropagation(); + $area.removeClass('drag-over'); + }); + + $area.on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $area.removeClass('drag-over'); + + const files = e.originalEvent.dataTransfer.files; + if (files.length > 0) { + $input[0].files = files; + $input.trigger('change'); + } + }); + + // File input change handler + $input.on('change', function() { + const fileName = $(this).val().split('\\').pop(); + if (fileName) { + $area.find('.file-name').remove(); + $area.append('
Selected: ' + fileName + '
'); + } + }); + }); + }, + + /** + * Setup status update listeners + */ + setupStatusUpdates: function() { + // Listen for AJAX success/error events + $(document).on('ajaxSuccess', function(event, xhr, settings) { + if (settings.url && settings.url.indexOf('cko_upload') !== -1) { + setTimeout(function() { + ApplePaySettingsUX.updateCardStates(); + ApplePaySettingsUX.setupProgressTracking(); + }, 500); + } + }); + + // Listen for field changes + $('input[type="text"], input[type="checkbox"], select').on('change blur', function() { + setTimeout(function() { + ApplePaySettingsUX.updateCardStates(); + ApplePaySettingsUX.setupProgressTracking(); + }, 300); + }); + + // Listen for certificate generation + $(document).on('cko:certificate:generated', function() { + setTimeout(function() { + ApplePaySettingsUX.updateCardStates(); + ApplePaySettingsUX.setupProgressTracking(); + }, 500); + }); + }, + + /** + * Show completion animation + */ + showCompletionAnimation: function($card) { + $card.addClass('completed'); + $card.find('.cko-card-title-icon').html('✓'); + + // Add a subtle animation + $card.css({ + 'animation': 'pulse 0.5s ease' + }); + + setTimeout(function() { + $card.css('animation', ''); + }, 500); + }, + + /** + * Show error state + */ + showErrorState: function($card, message) { + $card.addClass('error'); + const $status = $card.find('.cko-status-message'); + if ($status.length) { + $status.removeClass('notice-success notice-warning') + .addClass('notice-error') + .html('

Error: ' + message + '

') + .show(); + } + }, + + /** + * Setup collapsible sections for existing configuration + */ + setupCollapsibleSections: function() { + // Check if existing config banner is present or if sections have data-collapsible attribute + const $existingBanner = $('.cko-existing-config-banner'); + const $collapsibleSections = $('.cko-settings-card[data-collapsible="true"]'); + + // Setup toggle functionality for collapsible sections + $collapsibleSections.each(function() { + const $section = $(this); + const $content = $section.find('.cko-collapsible-content'); + const $toggleButton = $section.find('.cko-toggle-section'); + + // Only setup if content exists and toggle button exists + if ($content.length && $toggleButton.length) { + // Toggle functionality + $toggleButton.on('click', function(e) { + e.preventDefault(); + const $toggleText = $(this).find('.toggle-text'); + const $toggleIcon = $(this).find('.toggle-icon'); + + if ($content.is(':visible')) { + $content.slideUp(200); + $toggleText.text('Show'); + $toggleIcon.text('▼'); + } else { + $content.slideDown(200); + $toggleText.text('Hide'); + $toggleIcon.text('▲'); + } + }); + } + }); + }, + + /** + * Add security warnings for private key field + */ + addSecurityWarnings: function() { + // Find the private key field + const $keyField = $('input[name="woocommerce_wc_checkout_com_apple_pay[ckocom_apple_key]"]'); + + if ($keyField.length) { + // Add security warning after the field + const $fieldRow = $keyField.closest('tr'); + const $description = $fieldRow.find('.description'); + + if ($description.length) { + // Add security warning after description + const $warning = $('
' + + '

' + + '⚠️ Security Warning: ' + + 'The private key (.key) file must be stored outside of your website\'s public access folder (web root). ' + + 'Never place it in a publicly accessible directory.

' + + '
'); + + $description.after($warning); + } + } + } +}; + + /** + * Initialize on document ready + */ + $(document).ready(function() { + // Only initialize on Apple Pay settings page + if ($('.woocommerce-settings-form').length && + window.location.href.indexOf('wc_checkout_com_apple_pay') !== -1) { + ApplePaySettingsUX.init(); + } + }); + + // Add pulse animation + const style = document.createElement('style'); + style.textContent = ` + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.02); } + 100% { transform: scale(1); } + } + `; + document.head.appendChild(style); + +})(jQuery); + diff --git a/checkout-com-unified-payments-api/assets/js/admin-apple-pay-test-certificate.js b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-test-certificate.js new file mode 100644 index 00000000..6a6ccd83 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/admin-apple-pay-test-certificate.js @@ -0,0 +1,91 @@ +/** + * Admin Apple Pay Test Certificate + * + * Handles certificate and key testing from the WordPress admin interface. + */ +(function($) { + 'use strict'; + + $(document).ready(function() { + const $testButton = $('#cko-test-certificate-button'); + const $statusDiv = $('#cko-test-certificate-status'); + + if (!$testButton.length) { + return; // Exit if not on the right page + } + + $testButton.on('click', function(e) { + e.preventDefault(); + + const $button = $(this); + const originalText = $button.text(); + + // Disable button and show loading state + $button.prop('disabled', true).text(ckoTestCertificateData.i18n.testing); + $statusDiv.hide().removeClass('cko-status-message notice notice-success notice-error'); + + // Make AJAX request + $.ajax({ + url: ckoTestCertificateData.ajaxUrl, + type: 'POST', + dataType: 'json', + data: { + action: 'cko_test_apple_pay_certificate', + nonce: ckoTestCertificateData.nonce + }, + beforeSend: function(xhr) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }, + success: function(response) { + if (response.success) { + // Show simplified success message + let successMsg = '

' + ckoTestCertificateData.i18n.success + '

'; + successMsg += '

' + ckoTestCertificateData.i18n.successDetails + '

'; + + $statusDiv + .removeClass('notice notice-error') + .addClass('cko-status-message notice notice-success') + .html(successMsg) + .show(); + + // Reset button after 3 seconds + setTimeout(function() { + $button.prop('disabled', false).text(originalText); + }, 3000); + } else { + // Show simplified error message + let errorMsg = ''; + if (response.data && response.data.message) { + errorMsg = response.data.message; + } else { + errorMsg = ckoTestCertificateData.i18n.errorDefault; + } + + $statusDiv + .removeClass('notice notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoTestCertificateData.i18n.error + ' ' + errorMsg + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }, + error: function(xhr, status, error) { + // Show error message + $statusDiv + .removeClass('notice notice-success') + .addClass('cko-status-message notice notice-error') + .html('

' + ckoTestCertificateData.i18n.error + ' ' + error + '

') + .show(); + + $button.prop('disabled', false).text(originalText); + } + }); + }); + }); +})(jQuery); + + + + + diff --git a/assets/js/admin.js b/checkout-com-unified-payments-api/assets/js/admin.js similarity index 82% rename from assets/js/admin.js rename to checkout-com-unified-payments-api/assets/js/admin.js index 3384f6bb..f3409f12 100644 --- a/assets/js/admin.js +++ b/checkout-com-unified-payments-api/assets/js/admin.js @@ -3,20 +3,39 @@ jQuery( function ( $ ) { var admin_functions = { hidePaymentMethods: function () { + const classicCheckout = $( '[data-gateway_id="wc_checkout_com_cards"]' ); const applePay = $( '[data-gateway_id="wc_checkout_com_apple_pay"]' ); const googlePay = $( '[data-gateway_id="wc_checkout_com_google_pay"]' ); + const payPal = $( '[data-gateway_id="wc_checkout_com_paypal"]' ); + const flowPay = $( '[data-gateway_id="wc_checkout_com_flow"]' ); const alternativePay = $( '[data-gateway_id*="wc_checkout_com_alternative_payments"]' ); if ( applePay.length > 0 ) { applePay.hide(); } - if ( googlePay.length > 0 ) { + // Google Pay is now available in Flow mode - don't hide it + // Only hide Google Pay in classic mode, show it in Flow mode + if ( googlePay.length > 0 && ! cko_admin_vars.flow_enabled ) { googlePay.hide(); } + if ( flowPay.length > 0 ) { + flowPay.hide(); + } + if ( alternativePay.length > 0 ) { alternativePay.hide(); } + + if( cko_admin_vars.flow_enabled ) { + classicCheckout.hide(); + payPal.hide(); + flowPay.show(); + // Show Google Pay when Flow mode is enabled + if ( googlePay.length > 0 ) { + googlePay.show(); + } + } }, disableRefundForZero: function () { @@ -307,6 +326,59 @@ jQuery( function ( $ ) { } ); } ); + }, + + toggleSecretKeyVisibility: function () { + const field = $('#woocommerce_wc_checkout_com_cards_ckocom_sk'); + if (!field.length) return; + + const wrapper = field.closest('td'); + + // Create the toggle button + const toggleBtn = $(''); + toggleBtn.on('click', function (e) { + e.preventDefault(); + const currentType = field.attr('type'); + if (currentType === 'password') { + field.attr('type', 'text'); + toggleBtn.text('Hide'); + } else { + field.attr('type', 'password'); + toggleBtn.text('View'); + } + }); + + // Append button after the input field + field.after(toggleBtn); + }, + + expressButtonSizeSettings: function () { + // Handle button size preset changes for all three payment methods + const paymentMethods = ['apple_pay', 'google_pay', 'paypal']; + + paymentMethods.forEach(function(paymentMethod) { + const presetField = $('#woocommerce_wc_checkout_com_' + paymentMethod + '_' + paymentMethod + '_express_button_size_preset'); + const customHeightField = $('#woocommerce_wc_checkout_com_' + paymentMethod + '_' + paymentMethod + '_express_button_custom_height'); + + if (!presetField.length || !customHeightField.length) { + return; + } + + // Show/hide custom height field based on preset + function toggleCustomHeight() { + if (presetField.val() === 'custom') { + customHeightField.closest('tr').show(); + } else { + customHeightField.closest('tr').hide(); + } + } + + // Initial state + toggleCustomHeight(); + + // On change + presetField.on('change', toggleCustomHeight); + }); } } @@ -329,4 +401,9 @@ jQuery( function ( $ ) { admin_functions.cardSettings(); admin_functions.webhookSettings(); + + admin_functions.toggleSecretKeyVisibility(); + + // Handle express button size settings + admin_functions.expressButtonSizeSettings(); } ); diff --git a/checkout-com-unified-payments-api/assets/js/blocks/cards-blocks.js b/checkout-com-unified-payments-api/assets/js/blocks/cards-blocks.js new file mode 100644 index 00000000..34d50ec1 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/cards-blocks.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { registerPaymentMethod } from '@woocommerce/blocks-registry'; +import { getSetting } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import { CheckoutComCardsCheckout } from './checkout-com-cards-checkout'; + +const settings = getSetting( 'wc_checkout_com_cards_data', {} ); + +// Register Checkout.com Cards +registerPaymentMethod( { + name: 'wc_checkout_com_cards', + label: settings.title || 'Checkout.com Cards', + content: , + edit: , + canMakePayment: () => true, + ariaLabel: settings.description || 'Checkout.com Credit/Debit Cards', + supports: { + features: settings.supports || [], + }, +} ); diff --git a/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-checkout.js b/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-checkout.js new file mode 100644 index 00000000..c3e6a0a7 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-checkout.js @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { usePaymentMethodDataContext } from '@woocommerce/base-context'; + +/** + * Internal dependencies + */ +import { CheckoutComCardsForm } from './checkout-com-cards-form'; + +/** + * Checkout.com Cards Checkout Component + */ +export const CheckoutComCardsCheckout = () => { + const { + paymentMethodData, + setPaymentMethodData, + shouldSavePayment, + setShouldSavePayment + } = usePaymentMethodDataContext(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Handle form submission + const handleSubmit = async (event) => { + event.preventDefault(); + setIsLoading(true); + setError(null); + + try { + // Process payment with Checkout.com + const result = await processCheckoutComPayment(); + + if (result.success) { + setPaymentMethodData({ + ...paymentMethodData, + paymentMethod: 'wc_checkout_com_cards', + paymentData: result.paymentData + }); + } else { + setError(result.error || 'Payment failed'); + } + } catch (err) { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+ ); +}; + +/** + * Process payment with Checkout.com API + */ +const processCheckoutComPayment = async () => { + // This would integrate with your existing Checkout.com API + // For now, return a placeholder + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + success: true, + paymentData: { + paymentMethod: 'wc_checkout_com_cards', + timestamp: Date.now() + } + }); + }, 1000); + }); +}; diff --git a/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-form.js b/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-form.js new file mode 100644 index 00000000..3d48b5bf --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/checkout-com-cards-form.js @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; + +/** + * Checkout.com Cards Form Component + */ +export const CheckoutComCardsForm = ({ + onSubmit, + isLoading, + error, + shouldSavePayment, + setShouldSavePayment +}) => { + const [cardData, setCardData] = useState({ + cardNumber: '', + expiryDate: '', + cvv: '', + cardholderName: '' + }); + + const handleInputChange = (field, value) => { + setCardData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSavePaymentChange = (event) => { + setShouldSavePayment(event.target.checked); + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + handleInputChange('cardholderName', e.target.value)} + required + /> +
+ +
+ + handleInputChange('cardNumber', e.target.value)} + placeholder="1234 5678 9012 3456" + required + /> +
+ +
+ + handleInputChange('expiryDate', e.target.value)} + placeholder="MM/YY" + required + /> +
+ +
+ + handleInputChange('cvv', e.target.value)} + placeholder="123" + required + /> +
+ +
+ +
+ +
+ +
+
+ ); +}; diff --git a/checkout-com-unified-payments-api/assets/js/blocks/paypal-blocks.js b/checkout-com-unified-payments-api/assets/js/blocks/paypal-blocks.js new file mode 100644 index 00000000..3984ce8a --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/paypal-blocks.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { registerPaymentMethod } from '@woocommerce/blocks-registry'; +import { getSetting } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import { PayPalExpressCheckout } from './paypal-express-checkout'; + +const settings = getSetting( 'wc_checkout_com_paypal_data', {} ); + +// Register PayPal Express Checkout +registerPaymentMethod( { + name: 'wc_checkout_com_paypal', + label: settings.title || 'PayPal', + content: , + edit: , + canMakePayment: () => true, + ariaLabel: settings.description || 'PayPal Express Checkout', + supports: { + features: settings.supports || [], + }, +} ); diff --git a/checkout-com-unified-payments-api/assets/js/blocks/paypal-button.js b/checkout-com-unified-payments-api/assets/js/blocks/paypal-button.js new file mode 100644 index 00000000..d4e228c4 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/paypal-button.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef } from '@wordpress/element'; + +/** + * PayPal Button Component + */ +export const PayPalButton = ({ onApprove, onError, isLoading }) => { + const buttonRef = useRef(null); + + useEffect(() => { + if (!window.paypal || !buttonRef.current) { + return; + } + + // Clear previous button + buttonRef.current.innerHTML = ''; + + // Create PayPal button + window.paypal.Buttons({ + createOrder: (data, actions) => { + return actions.order.create({ + purchase_units: [{ + amount: { + currency_code: 'USD', + value: '10.00' // This should come from cart total + } + }] + }); + }, + onApprove: (data, actions) => { + return actions.order.capture().then((details) => { + onApprove(details); + }); + }, + onError: (err) => { + onError(err); + }, + style: { + layout: 'vertical', + color: 'blue', + shape: 'rect', + label: 'paypal' + } + }).render(buttonRef.current); + + }, [onApprove, onError]); + + return ( +
+
+ {isLoading && ( +
+ {__('Processing PayPal payment...', 'checkout-com-unified-payments-api')} +
+ )} +
+ ); +}; diff --git a/checkout-com-unified-payments-api/assets/js/blocks/paypal-express-checkout.js b/checkout-com-unified-payments-api/assets/js/blocks/paypal-express-checkout.js new file mode 100644 index 00000000..e4475301 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/blocks/paypal-express-checkout.js @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { usePaymentMethodDataContext } from '@woocommerce/base-context'; + +/** + * Internal dependencies + */ +import { PayPalButton } from './paypal-button'; + +/** + * PayPal Express Checkout Component + */ +export const PayPalExpressCheckout = () => { + const { + paymentMethodData, + setPaymentMethodData + } = usePaymentMethodDataContext(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [paypalLoaded, setPaypalLoaded] = useState(false); + + // Load PayPal SDK + useEffect(() => { + if (window.paypal) { + setPaypalLoaded(true); + return; + } + + const script = document.createElement('script'); + script.src = 'https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD'; + script.async = true; + script.onload = () => setPaypalLoaded(true); + document.head.appendChild(script); + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, []); + + const handlePayPalApprove = async (data) => { + setIsLoading(true); + setError(null); + + try { + // Process PayPal payment + const result = await processPayPalPayment(data); + + if (result.success) { + setPaymentMethodData({ + ...paymentMethodData, + paymentMethod: 'wc_checkout_com_paypal', + paymentData: result.paymentData + }); + } else { + setError(result.error || 'PayPal payment failed'); + } + } catch (err) { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handlePayPalError = (err) => { + setError('PayPal payment failed'); + setIsLoading(false); + }; + + if (!paypalLoaded) { + return ( +
+ {__('Loading PayPal...', 'checkout-com-unified-payments-api')} +
+ ); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + + +
+ ); +}; + +/** + * Process PayPal payment + */ +const processPayPalPayment = async (paypalData) => { + // This would integrate with your existing PayPal API + // For now, return a placeholder + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + success: true, + paymentData: { + paymentMethod: 'wc_checkout_com_paypal', + paypalOrderId: paypalData.orderID, + timestamp: Date.now() + } + }); + }, 1000); + }); +}; diff --git a/checkout-com-unified-payments-api/assets/js/cko-apple-pay-express-integration.js b/checkout-com-unified-payments-api/assets/js/cko-apple-pay-express-integration.js new file mode 100644 index 00000000..1cb9a977 --- /dev/null +++ b/checkout-com-unified-payments-api/assets/js/cko-apple-pay-express-integration.js @@ -0,0 +1,911 @@ +/* global cko_apple_pay_vars */ + +jQuery( function ( $ ) { + + const formSelector = 'form.cart'; + + const onFormChange = function ( e ) { + const form = document.querySelector( formSelector ); + + const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null; + + const isEnabled = ( null === addToCartButton ) || ! addToCartButton.classList.contains( 'disabled' ); + + const element = jQuery( cko_apple_pay_vars.apple_pay_button_selector ); + + if ( isEnabled ) { + jQuery(element) + .removeClass('cko-disabled') + .off('mouseup') + .find('> *') + .css('pointer-events', ''); + } else { + jQuery(element) + .addClass('cko-disabled') + .on('mouseup', function(event) { + event.stopImmediatePropagation(); + }) + .find('> *') + .css('pointer-events', 'none'); + } + + }; + + const getAttributes = function() { + var select = $( '.variations_form' ).find( '.variations select' ), + data = {}, + count = 0, + chosen = 0; + + select.each( function() { + var attribute_name = $( this ).data( 'attribute_name' ) || $( this ).attr( 'name' ); + var value = $( this ).val() || ''; + + if ( value.length > 0 ) { + chosen ++; + } + + count ++; + data[ attribute_name ] = value; + }); + + return { + 'count' : count, + 'chosenCount': chosen, + 'data' : data + }; + }; + + let showError = function ( error_message ) { + + if ( 'string' === typeof error_message ) { + error_message = [ error_message ]; + } + + let ulWrapper = jQuery( '