Compare commits
13 Commits
d8c81a4022
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
622c9f8eb7 | ||
|
|
7a6765a579 | ||
|
|
008188b790 | ||
|
|
0094a3571c | ||
|
|
a36e71ed56 | ||
|
|
1a10c18c31 | ||
|
|
7ba92022d5 | ||
|
|
c103e368be | ||
|
|
99912a9335 | ||
|
|
862abc8d74 | ||
|
|
fe9efdfeec | ||
|
|
d1de0015be | ||
|
|
bde43d8c66 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -9,4 +9,17 @@ coverage
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
# Build output
|
||||
build/
|
||||
|
||||
# Development notes
|
||||
FINDINGS.md
|
||||
MIGRATION_STRATEGY.md
|
||||
TASKLIST.md
|
||||
docs/
|
||||
|
||||
# npm
|
||||
package-lock.json
|
||||
node_modules/.package-lock.json
|
||||
|
||||
196
FINDINGS.md
196
FINDINGS.md
@@ -1,196 +0,0 @@
|
||||
# 🔍 Formipay Plugin — Comprehensive Audit Report
|
||||
|
||||
**Date:** April 17, 2026
|
||||
**Auditor:** GitHub Copilot
|
||||
**Plugin Version:** 1.0.0
|
||||
**Files Analyzed:** ~60+ files, ~15,000+ lines of PHP, JS, CSS, HTML
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [1. Bugs & Defects](#1-bugs--defects)
|
||||
- [1.1 Critical Bugs](#11-critical-bugs)
|
||||
- [1.2 Moderate Bugs](#12-moderate-bugs)
|
||||
- [2. Security Concerns](#2-security-concerns)
|
||||
- [3. Architecture & Code Quality Issues](#3-architecture--code-quality-issues)
|
||||
- [4. Missing Features & Modules](#4-missing-features--modules)
|
||||
- [5. Performance Issues](#5-performance-issues)
|
||||
- [6. Missing Admin Pages / Settings](#6-missing-admin-pages--settings)
|
||||
- [7. Code Cleanup Needed](#7-code-cleanup-needed)
|
||||
- [8. Opportunities & Nice-to-Haves](#8-opportunities--nice-to-haves)
|
||||
- [9. Summary Priority Matrix](#9-summary-priority-matrix)
|
||||
- [10. Architectural Recommendation](#10-architectural-recommendation)
|
||||
|
||||
---
|
||||
|
||||
## 1. Bugs & Defects
|
||||
|
||||
### 1.1 Critical Bugs
|
||||
|
||||
| # | Location | Issue | Detail |
|
||||
|---|----------|-------|--------|
|
||||
| 1 | `includes/Customer.php` ~line 172 `update()` | **Undefined variable — fatal error** | Method builds `$insert_data` and `$where`, but the `$wpdb->update()` call references undefined `$table_name` and `$new_args`. Every customer update will throw a PHP fatal error. |
|
||||
| 2 | `includes/Order.php` `delete()` | **Undefined `$id` variable** | Uses `$id` in the `$wpdb->delete()` where clause instead of the method parameter `$order_id`. Every order deletion call will fail. |
|
||||
| 3 | `includes/Order.php` `formipay_bulk_delete_order()` | **Iterates wrong variable** | Loops `foreach($ids as $id)` but calls `$this->delete($order_id)` — `$order_id` comes from the outer scope (nonce check), not the loop variable. Bulk delete will repeatedly delete the same (or zero) orders. |
|
||||
| 4 | `includes/Notification/Email.php` `send_email()` | **Wrong class reference — fatal error** | Calls `\Formipay_Notification::update_notification_data()` — this class does not exist. Should use `parent::update_notification_data()`. Email status tracking will crash. |
|
||||
| 5 | `includes/Integration/Paypal.php` `auto_cancel_order_on_timeout()` | **Undefined `Order` class** | Calls `Order::update(...)` but unlike `BankTransfer.php`, `Paypal.php` does not import `use Formipay\Order as Order`. This will throw a class-not-found error on timeout. |
|
||||
| 6 | `includes/Integration/Paypal.php` `process_payment()` | **Undefined `self::paypal_settings`** | The PayPal class never declares a `$paypal_settings` property. Accessing it leads to undefined property notices and broken payment flow. |
|
||||
|
||||
### 1.2 Moderate Bugs
|
||||
|
||||
| # | Location | Issue |
|
||||
|---|----------|-------|
|
||||
| 7 | `includes/Payment/BankTransfer.php` `check_unique_code()` | Uses `MAX(id)+1` for unique codes — predictable and subject to race conditions. Two concurrent orders can receive the same unique code. |
|
||||
| 8 | `includes/Payment/BankTransfer.php` `add_unique_code_details()` | Calls `$this->check_unique_code()` **three times** per request (once for `'item'`, once for `'amount'`, once for `'subtotal'`). Each call queries the DB independently and may return different values. Displayed unique code may not match the stored one. |
|
||||
| 9 | `admin/functions.php` `formipay_field_type_collection()` | Color field label says `'Number'` instead of `'Color'` — copy-paste error. |
|
||||
| 10 | `includes/Render.php` field rendering | No `default` fallback rendered when field type doesn't match any case in the switch — unknown field types silently produce no output. |
|
||||
| 11 | `includes/Order.php` `render_form_submit()` | `$field_value` is sometimes an array (from checkbox fields) but the code assumes string context. Nested `if(is_array($field_value))` only handles one level. |
|
||||
| 12 | `includes/Thankyou.php` `check_parse_query()` vs `formipay_get_order()` | Old cookie-based URL (`base64_encode`) co-exists with new Token-based validation. `formipay_get_order()` generates the old-format URL for the `thankyou` link, but the new Token system expects a different format. Inconsistent access path. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Security Concerns
|
||||
|
||||
| # | Severity | Location | Issue |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | **High** | `includes/Order.php` `retrieve_form_data()` | Cookie `fp_access` uses `maybe_serialize()` — **PHP object injection** risk if an attacker can manipulate cookie values. Should use `json_encode()`/`json_decode()`. |
|
||||
| 2 | **High** | `includes/Thankyou.php` `set_endpoint()` | Calls `flush_rewrite_rules()` on **every `init`** hook — extremely expensive and causes race conditions under concurrent load. Should only flush on activation/deactivation or settings save. |
|
||||
| 3 | **High** | `includes/Payment/Payment.php` `set_endpoint()` | Same `flush_rewrite_rules()` issue — fires on every page load. |
|
||||
| 4 | **Medium** | `includes/Order.php` `retrieve_form_data()` | Thank-you URL uses `base64_encode(form_id:::order_id:::session_id)` — base64 is not encryption. Sequential order IDs can be guessed. The `Token` class provides proper tokens but the old path remains active. |
|
||||
| 5 | **Medium** | `includes/LicenseAPI.php` | All REST endpoints use `permission_callback => '__return_true'`. The `revoke` endpoint has a stub permission callback that always returns `true`. Anyone can revoke or manipulate licenses without authentication. |
|
||||
| 6 | **Medium** | `includes/Integration/Paypal.php` `webhook_endpoint()` | No PayPal webhook signature verification. An attacker could forge webhook calls to mark orders as paid without actual payment. |
|
||||
| 7 | **Medium** | `includes/Customer.php` `formipay_tabledata_customers()` | No nonce check (`check_ajax_referer`). Any authenticated user can dump all customer data via direct AJAX call. |
|
||||
| 8 | **Low** | `includes/Render.php` | Inline `<style>` blocks injected into `<body>` — Content Security Policy (CSP) headers may block these. Should use `wp_add_inline_style()`. |
|
||||
| 9 | **Low** | `includes/Token.php` `validate()` | Uses MySQL `NOW()` for expiry comparison while token generation uses PHP timezone. Server timezone mismatch could cause valid tokens to be rejected or expired tokens to pass. |
|
||||
| 10 | **Medium** | `includes/Render.php` (default timezone) | Hardcoded default timezone `'Asia/Jakarta'` in Render.php instead of using `wp_timezone_string()`. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture & Code Quality Issues
|
||||
|
||||
| # | Issue | Detail |
|
||||
|---|-------|--------|
|
||||
| 1 | **No Composer dependency management** | Custom `spl_autoload_register` works but there's no `composer.json`. The `vendor/` folder contains manually copied libraries with no version locking, no autoload optimization, and no security update path. |
|
||||
| 2 | **SingletonTrait incomplete** | `__wakeup()` is `public` (should be `private`). Missing `__sleep()` to prevent serialization-based singleton bypass. |
|
||||
| 3 | **No database migration system** | `dbDelta()` runs on every `init` for all custom tables. No version tracking for schema changes. Adding columns later requires manual SQL or relying on `dbDelta` diff detection. |
|
||||
| 4 | **Inconsistent class instantiation** | Some classes use Singleton trait, others (`Token`) don't. `new \Formipay\Token` in `Init.php` bypasses the singleton pattern entirely. |
|
||||
| 5 | **Global functions vs OOP** | `admin/functions.php` has ~40 global functions (e.g., `formipay_price_format`, `formipay_get_order`, `formipay_currency_array`). Should be in utility/service classes for testability and namespace hygiene. |
|
||||
| 6 | **Insufficient capability checks** | Several admin-ajax handlers verify nonce but don't check `current_user_can('manage_options')`. Any authenticated user (even subscriber) with a valid nonce could potentially access admin functions. |
|
||||
| 7 | **Hardcoded English strings** | Many UI strings in JS-localized data and some PHP output are hardcoded in English without `__()` translation functions. |
|
||||
| 8 | **No proper REST API** | Form submission goes through `admin-ajax.php`. A proper REST API endpoint would be more cache-friendly, better structured, and follow WordPress standards. |
|
||||
| 9 | **Static version constant** | `FORMIPAY_VERSION` is hardcoded to `'1.0.0'` and never updated programmatically. All cache-busting relies on this value. |
|
||||
| 10 | **Backup file in production** | `includes/Integration/Paypal.phpbak` exists — should be removed from production codebase. |
|
||||
| 11 | **No deactivation/uninstall cleanup** | No `uninstall.php` or deactivation hook to clean up custom tables, scheduled events, or options. |
|
||||
| 12 | **No `.distignore` or build process** | No `.distignore` file for WordPress.org packaging. Development files would be shipped to production. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Missing Features & Modules
|
||||
|
||||
| # | Module / Area | Status | Priority |
|
||||
|---|---------------|--------|----------|
|
||||
| 1 | **ExchangeRateAPI** | File exists but is **completely empty** — just an empty class extending Payment | 🔴 High |
|
||||
| 2 | **License API implementation** | All 4 REST endpoints (`verify`, `activate`, `deactivate`, `revoke`) are **stubs returning hardcoded `ok: true`** | 🔴 High |
|
||||
| 3 | **Subscription / Recurring payments** | Listed in `readme.txt` as "Planned" but no code exists | 🟡 Medium |
|
||||
| 4 | **Donation forms** | `formipay_is_donation()` and `donation_config` filter exist, but no donation-specific frontend UI (no "pay what you want" input, no suggested amounts) | 🟡 Medium |
|
||||
| 5 | **Inventory / Stock management** | Product has `stock_config` in settings but no stock decrement on order, no stock validation before submission, no "out of stock" frontend message | 🟡 Medium |
|
||||
| 6 | **Product variations on frontend** | Variation config exists in Product admin, but `Order.php` doesn't process variation selections — no variation dropdown rendered on frontend | 🟡 Medium |
|
||||
| 7 | **Tax system** | No tax calculation anywhere. Products have price, shipping has fees, but there is no tax engine. Essential for physical product sales in most jurisdictions. | 🟡 Medium |
|
||||
| 8 | **Customer portal / dashboard** | Referenced in readme and `single-formipay.php` template exists, but no customer-facing order history page, no login/registration flow, no "My Orders" view | 🟡 Medium |
|
||||
| 9 | **Refund system** | `refunded` status exists in status list, but no refund workflow, no partial refund, no payment reversal integration | 🟡 Medium |
|
||||
| 10 | **Form analytics / reporting** | No form view tracking, no conversion rate calculation, no abandonment tracking, no dashboard charts | 🟢 Low |
|
||||
| 11 | **Export / Import** | No CSV/Excel export for orders, customers, or products. No bulk import for products. | 🟢 Low |
|
||||
| 12 | **Outgoing webhook system** | No way to notify external systems (Zapier, Slack, custom endpoints) on order events | 🟢 Low |
|
||||
| 13 | **Form template library** | No pre-built form templates for common use cases (simple product, donation, registration) | 🟢 Low |
|
||||
| 14 | **Multi-step form navigation** | Page break config exists in admin, step indicators render, but no frontend JS to actually navigate between steps | 🟡 Medium |
|
||||
| 15 | **Email log admin page** | `formipay_notification_log` table exists but there's no admin page to view email history, resend failed emails, or debug delivery | 🟡 Medium |
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Issues
|
||||
|
||||
| # | Location | Issue |
|
||||
|---|----------|-------|
|
||||
| 1 | `includes/Thankyou.php` `set_endpoint()` | `flush_rewrite_rules()` on **every page load** — one of the most expensive WordPress operations. Should only run on plugin activation and settings save. |
|
||||
| 2 | `includes/Payment/Payment.php` `set_endpoint()` | Same `flush_rewrite_rules()` issue. |
|
||||
| 3 | `includes/Order.php` `formipay_tabledata_orders()` | Runs **two separate full-table queries** — one fetching ALL orders just to count statuses (`O(n)` in memory), plus the paginated query. Should use `COUNT(*) ... GROUP BY status` for the report. |
|
||||
| 4 | `includes/Customer.php` `formipay_tabledata_customers()` | **No pagination** — loads ALL customers into memory and returns them. Will crash or timeout with large datasets. |
|
||||
| 5 | `admin/functions.php` `formipay_currency_array()` | `file_get_contents()` of `currencies.json` on **every call** with no caching. Should use a static variable or WordPress transient. |
|
||||
| 6 | `admin/functions.php` `formipay_country_array()` | Same issue — reads JSON file from disk on every function call. |
|
||||
| 7 | `includes/Init.php` `default_config()` | The entire ~200-line default config array is loaded on every `plugin_loaded` action. Should be lazy-loaded or only when saving defaults. |
|
||||
| 8 | `includes/Render.php` | All form CSS is output as inline `<style>` in the HTML `<body>` — not cacheable by browsers, duplicated on every page with the form shortcode. |
|
||||
| 9 | `includes/Token.php` `create_db()` | Runs `dbDelta()` on every `init` — redundant after first install. |
|
||||
| 10 | `includes/Customer.php` `create_db()` | Same `dbDelta()` on every `init`. |
|
||||
| 11 | `includes/Order.php` `create_db()` | Same issue. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Missing Admin Pages / Settings
|
||||
|
||||
| # | Missing Page | Description |
|
||||
|---|-------------|-------------|
|
||||
| 1 | **Notification Log** | Table `formipay_notification_log` exists but there's no admin page to view email history, resend failed emails, or debug delivery issues. |
|
||||
| 2 | **License detail/edit** | License list page exists but there's no detail or edit view (can't manually activate, revoke, or extend a license). |
|
||||
| 3 | **Dashboard / Analytics** | No overview dashboard with form views, submissions, revenue charts, or conversion rates. |
|
||||
| 4 | **System Status / Tools** | No page to check PayPal connectivity, email delivery test, database health, or scheduled events. |
|
||||
| 5 | **Customer order history** | Customer detail page exists but doesn't show linked order history or purchase timeline. |
|
||||
| 6 | **Import/Export tools** | No admin UI to bulk import products or export order/customer data. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Code Cleanup Needed
|
||||
|
||||
| # | Item | Detail |
|
||||
|---|------|--------|
|
||||
| 1 | **Commented-out code** | `Init.php` has commented-out PayPal init, taxonomy, and defer-attribute filter. Should be removed or tracked as TODOs. |
|
||||
| 2 | **`Paypal.phpbak`** | Backup file in production directory `includes/Integration/Paypal.phpbak` — must be deleted. |
|
||||
| 3 | **Dead code in `Notification.php`** | `$last_notification_field = count($notification_fields) - 1; $notification_fields[$last_notification_field]['group'] = 'ended'` — uses numeric array index instead of the key name, which breaks if filter order changes. |
|
||||
| 4 | **Inconsistent nonce naming** | Different pages use different nonce names: `formipay-order-details`, `formipay-admin-coupon-page`, `formipay-admin-licenses`, `formipay-admin-product-page`, `formipay-admin-access-nonce`, `formipay-form-editor`, `formipay-thankyou-nonce`. Should follow a single convention. |
|
||||
| 5 | **Deprecated `FILTER_SANITIZE_STRING`** | Used in `Customer.php` `customers_page()` — deprecated since PHP 8.1. Should use `FILTER_SANITIZE_SPECIAL_CHARS` or `sanitize_text_field()`. |
|
||||
| 6 | **Mixed indentation** | Some files use tabs, some spaces, some mix both within the same file. |
|
||||
| 7 | **Missing PHPDoc blocks** | Most methods have no docblocks. Return types are rarely declared. Makes IDE support and static analysis poor. |
|
||||
| 8 | **No `.editorconfig`** | No project-wide formatting standard file. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Opportunities & Nice-to-Haves
|
||||
|
||||
| # | Opportunity | Business Impact |
|
||||
|---|------------|-----------------|
|
||||
| 1 | **Composer package management** | Replace manual vendor copies with proper Composer dependencies for auto-updates, security patches, and smaller distribution |
|
||||
| 2 | **React-based form builder** | Replace the partial Vue editor canvas with a full React drag-and-drop form builder for better UX and extensibility |
|
||||
| 3 | **Webhook/API integrations** | Zapier, Make (Integromat), Slack notifications — extend the notification system for 3rd-party automation |
|
||||
| 4 | **Stripe payment gateway** | Most requested gateway after PayPal. The abstract `Payment` class makes it architecturally straightforward to add |
|
||||
| 5 | **PDF invoice generation** | Auto-generate PDF invoices/receipts for orders — high-value feature for business users |
|
||||
| 6 | **Google Analytics / Facebook Pixel** | E-commerce event tracking (`purchase`, `add_to_cart`) for conversion optimization |
|
||||
| 7 | **Multi-vendor / marketplace** | Architecture already supports per-form products — extend to multi-vendor marketplace |
|
||||
| 8 | **Form A/B testing** | Duplicate form feature already exists — add conversion tracking and statistical comparison |
|
||||
| 9 | **Cart / checkout recovery** | Store partial submissions in `localStorage` + send recovery emails for abandoned forms |
|
||||
| 10 | **Full localization** | Generate `.pot` file, translate to top 10 languages. Currently only partially translatable |
|
||||
| 11 | **WP-CLI commands** | `wp formipay order list`, `wp formipay product create`, `wp formipay license verify`, etc. |
|
||||
| 12 | **Headless / REST API** | Full REST API for headless WordPress + Next.js / Nuxt frontends |
|
||||
| 13 | **Gutenberg block** | Register a `formipay/form` block for native Gutenberg integration instead of shortcode-only |
|
||||
| 14 | **Unit & integration tests** | Zero test coverage currently. Critical for a payment plugin handling money |
|
||||
| 15 | **Rate limiting on public endpoints** | `retrieve_form_data` and `check_coupon_code` have no rate limiting. Vulnerable to brute-force and spam |
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary Priority Matrix
|
||||
|
||||
| Priority | Count | Key Items |
|
||||
|----------|-------|-----------|
|
||||
| 🔴 **Critical (fix immediately)** | 6 | Customer update fatal error, Order delete undefined variable, Bulk delete wrong variable, Email send wrong class, flush_rewrite_rules performance, License API stubs |
|
||||
| 🟡 **High (next sprint)** | 12 | Unique code race condition, PayPal webhook verification, ExchangeRateAPI empty class, Stock management, Donation UI, Notification log page, Customer portal, Tax system |
|
||||
| 🟢 **Medium (backlog)** | 15 | Refund workflow, Analytics dashboard, Export/Import, Form templates, Gutenber block, Localization, Rate limiting |
|
||||
| ⚪ **Nice-to-have (roadmap)** | 12 | WP-CLI, A/B testing, Headless API, PDF invoices, Multi-vendor, Cart recovery, CI/CD pipeline |
|
||||
|
||||
---
|
||||
|
||||
## 10. Architectural Recommendation
|
||||
|
||||
> See `RECOMMENDATION.md` for the detailed technical recommendation.
|
||||
|
||||
---
|
||||
|
||||
*End of audit report.*
|
||||
@@ -1,282 +0,0 @@
|
||||
# Formipay — Migration Strategy
|
||||
|
||||
**Date:** April 18, 2026 (Updated)
|
||||
**Context:** Phase 2 — React Admin Foundation
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains how to approach migrating existing Formipay admin interfaces to React while **ensuring zero feature loss**.
|
||||
|
||||
**Key Principle:** **Coexistence until Feature Parity** — New React versions must match or exceed old functionality before deprecating old code. No "delete and replace" without validation.
|
||||
|
||||
---
|
||||
|
||||
## Existing Technology Inventory
|
||||
|
||||
| Technology | Location | Purpose | Feature Count | Migration Priority |
|
||||
|-------------|----------|---------|---------------|-------------------|
|
||||
| **WPCFTO Framework** | `vendor/` | Settings form builder | N/A | Low (replaced by React settings) |
|
||||
| **Grid.js** | `admin/assets/js/page-*.js` | All admin tables | ~20 features per page | **HIGH** (current gap) |
|
||||
| **SweetAlert2** | `vendor/SweetAlert2/` | Modal dialogs | N/A | None (keep using) |
|
||||
| **Custom Vue 2 App** | `admin/assets/js/admin-product-editor.js` | Product variation pricing | ~7 features | Medium |
|
||||
| **jQuery** | Core WP dependency | DOM manipulation | N/A | Phase out gradually |
|
||||
|
||||
---
|
||||
|
||||
## Critical: Grid.js Migration Strategy
|
||||
|
||||
### Current Grid.js Features (Must Preserve)
|
||||
|
||||
**Every admin page with Grid.js has these features:**
|
||||
|
||||
| Feature | Forms | Coupons | Access | Orders | Products |
|
||||
|---------|-------|--------|-------|--------|----------|
|
||||
| Checkbox column + "Select All" | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Bulk delete button | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Inline row actions (hover) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Status filter tabs (All/Published/Draft) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Search input | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Sort dropdown | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Order (ASC/DESC) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Server-side pagination | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| "Add New" modal (SweetAlert2) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Inline delete action | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Inline duplicate action | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Shortcode copy button | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Multi-currency display | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
|
||||
**Total: ~20 table features per page**
|
||||
|
||||
### Migration Approach: Coexistence
|
||||
|
||||
**DO NOT:** Delete old Grid.js code until React replacement is feature-complete
|
||||
|
||||
**DO:** Use query param or feature flag to run both versions side-by-side
|
||||
|
||||
```
|
||||
Phase 1: Coexistence (Required)
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Formipay Admin │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Old: Grid.js Tables → Fully functional │
|
||||
│ New: React Tables → Under development, feature-incomplete │
|
||||
│ │
|
||||
│ Access: ?old=1 → Grid.js | ?old=0 → React (default when ready) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
**Step 1: Feature Parity Checklist**
|
||||
|
||||
Create `MIGRATION_CHECKLIST.md` with per-page feature requirements (see template below).
|
||||
|
||||
**Step 2: Dual-Mode Rendering**
|
||||
|
||||
In PHP page callback, check for query param:
|
||||
|
||||
```php
|
||||
public function formipay_form() {
|
||||
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||
|
||||
if ($use_react) {
|
||||
\Formipay\Admin\ReactAdmin::render_mount_point('forms');
|
||||
} else {
|
||||
include_once FORMIPAY_PATH . 'admin/page-forms.php';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Feature Flag for Default**
|
||||
|
||||
```php
|
||||
// In settings or option
|
||||
public function use_react_admin() {
|
||||
return get_option('formipay_use_react_admin', false);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Toggle Link in Admin**
|
||||
|
||||
```php
|
||||
// Add to admin footer or menu bar
|
||||
<?php if (!get_option('formipay_use_react_admin')) : ?>
|
||||
<a href="<?php echo admin_url('admin.php?page=formipay&react=1'); ?>">
|
||||
Try React Admin (Beta)
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo admin_url('admin.php?page=formipay&react=0'); ?>">
|
||||
Use Classic Admin
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Testing Protocol
|
||||
|
||||
Before setting `use_react_admin` to true as default:
|
||||
|
||||
1. **Manual Feature Testing**: Go through checklist item by item
|
||||
2. **Regression Testing**: Old Grid.js version still works
|
||||
3. **Data Compatibility**: Both versions read/write same data format
|
||||
4. **Performance**: React version not significantly slower
|
||||
5. **Browser Testing**: Test in Chrome, Firefox, Safari
|
||||
|
||||
**Only when ALL checkboxes pass → Enable React by default**
|
||||
|
||||
---
|
||||
|
||||
## React Component Library Strategy
|
||||
|
||||
### Approved Libraries
|
||||
|
||||
| Library | Purpose | Why |
|
||||
|---------|---------|-----|
|
||||
| **@wordpress/components** | UI primitives (Button, Modal, SelectControl, TextControl) | Native WP styling, already bundled |
|
||||
| **@tanstack/react-table** (v8) | Headless table engine | Flexible, performant, React 18 compatible |
|
||||
| **SweetAlert2** (existing) | Modals, confirmations, toasts | Already in use, keep as-is |
|
||||
| **@wordpress/icons** | Icons | Already bundled, correct WP styling |
|
||||
|
||||
### Libraries to AVOID
|
||||
|
||||
| Library | Reason to Avoid |
|
||||
|---------|-----------------|
|
||||
| **shadcn/ui** | Wrong styling (Tailwind vs WP), requires Tailwind setup |
|
||||
| **Material UI (@mui/x-data-grid)** | Wrong styling, 100KB+ bundle, overkill |
|
||||
| **react-table** (v7) | Deprecated, use @tanstack/react-table |
|
||||
| **react-data-table** | Heavy bundle, opinionated styling |
|
||||
|
||||
### Building the Table Component
|
||||
|
||||
```jsx
|
||||
// Use @tanstack/react-table for the engine
|
||||
import { useReactTable } from '@tanstack/react-table';
|
||||
|
||||
// Use @wordpress/components for UI
|
||||
import { Modal, Button, CheckboxControl } from '@wordpress/components';
|
||||
|
||||
// Style with WordPress classes
|
||||
<table className="wp-list-table widefat fixed striped">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Compatibility Requirements
|
||||
|
||||
### AJAX Endpoints (Must Preserve)
|
||||
|
||||
| Endpoint | Request | Response Format |
|
||||
|----------|---------|------------------|
|
||||
| `formipay-tabledata-forms` | GET + params | `{results, total, posts_report}` |
|
||||
| `formipay-create-form-post` | POST + title | `{success, data: {edit_post_url}}` |
|
||||
| `formipay-delete-form` | POST + id | `{success, data: {title, message, icon}}` |
|
||||
| `formipay-duplicate-form` | POST + id | `{success, data: {title, message, icon}}` |
|
||||
| `formipay-bulk-delete-form` | POST + ids[] | `{success, data: {title, message, icon}}` |
|
||||
| `get_product_variables` | GET + post_id | Variation data object |
|
||||
| `formipay-tabledata-coupons` | GET + params | `{results, total, posts_report}` |
|
||||
| `formipay-tabledata-access-items` | GET + params | `{results, total, posts_report}` |
|
||||
| `formipay-tabledata-orders` | GET + params | `{results, total, posts_report}` |
|
||||
| `formipay-tabledata-customers` | GET + params | `{results, total, posts_report}` |
|
||||
|
||||
**Critical:** Response formats must remain identical for compatibility!
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If React version has issues:
|
||||
|
||||
1. **Immediate:** Set `formipay_use_react_admin` to false
|
||||
2. **Users see:** Classic Grid.js version (fully functional)
|
||||
3. **Fix React:** Debug and fix in development
|
||||
4. **Retest:** Go through checklist again
|
||||
5. **Re-enable:** Set flag back to true
|
||||
|
||||
**Critical:** Never deploy without working fallback!
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist Template
|
||||
|
||||
Copy this template for each page migration:
|
||||
|
||||
### [Page Name] Migration Checklist
|
||||
|
||||
#### Table Core Features
|
||||
- [ ] Data loads and displays correctly
|
||||
- [ ] Loading spinner shown during fetch
|
||||
- [ ] Empty state shown when no data
|
||||
- [ ] Error state handled gracefully
|
||||
|
||||
#### Selection Features
|
||||
- [ ] Checkbox column renders
|
||||
- [ ] "Select All" checkbox works
|
||||
- [ ] Individual row checkboxes work
|
||||
- [ ] Checkboxes persist across page changes
|
||||
- [ ] Bulk delete button appears when rows selected
|
||||
- [ ] Bulk delete confirmation modal
|
||||
- [ ] Bulk delete refreshes table
|
||||
|
||||
#### Row Actions
|
||||
- [ ] Hover shows action links
|
||||
- [ ] Edit action navigates correctly
|
||||
- [ ] Delete action shows confirmation
|
||||
- [ ] Delete action removes row and refreshes
|
||||
- [ ] Duplicate action shows confirmation
|
||||
- [ ] Duplicate action adds new row and refreshes
|
||||
|
||||
#### Filtering & Sorting
|
||||
- [ ] Status filter tabs work
|
||||
- [ ] Status counts display correctly
|
||||
- [ ] Active filter highlighted
|
||||
- [ ] Search input filters results
|
||||
- [ ] Search debounce (don't spam server)
|
||||
- [ ] Sort dropdown works
|
||||
- [ ] Order toggle works
|
||||
- [ ] Combined filters work (search + status + sort)
|
||||
|
||||
#### Pagination
|
||||
- [ ] Pagination controls display
|
||||
- [ ] Page numbers correct
|
||||
- [ ] Next/Previous buttons work
|
||||
- [ ] Limit per page respected
|
||||
- [ ] Total count accurate
|
||||
|
||||
#### Create Features
|
||||
- [ ] "Add New" button visible
|
||||
- [ ] Modal/dialog opens on click
|
||||
- [ ] Modal has required fields
|
||||
- [ ] Modal validation works
|
||||
- [ ] Create action succeeds
|
||||
- [ ] Post creation redirects to edit
|
||||
- [ ] Error handling in modal
|
||||
|
||||
#### UX Details
|
||||
- [ ] Row hover effects work
|
||||
- [ ] Selected row highlighting
|
||||
- [ ] Toast notifications for actions
|
||||
- [ ] Confirmation dialogs for destructive actions
|
||||
- [ ] Keyboard accessibility (Enter, Escape)
|
||||
- [ ] Loading states during actions
|
||||
|
||||
#### Data Compatibility
|
||||
- [ ] Same AJAX endpoints as old version
|
||||
- [ ] Same request format
|
||||
- [ ] Same response format handling
|
||||
- [ ] Hidden inputs updated (if applicable)
|
||||
- [ ] WordPress nonces handled correctly
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Grid.js Library:** ~20KB, well-tested, handles server-side pagination well
|
||||
- **@tanstack/react-table:** ~9KB, headless, requires more setup but more flexible
|
||||
- **jQuery:** Still used by WordPress core, will remain for now
|
||||
- **SweetAlert2:** Keep using, integrates well with React
|
||||
- **Migration is NOT a race:** Take time to get it right
|
||||
|
||||
---
|
||||
|
||||
*End of Migration Strategy.*
|
||||
193
TASKLIST.md
193
TASKLIST.md
@@ -1,193 +0,0 @@
|
||||
# Formipay — Implementation Task List
|
||||
|
||||
**Last Updated:** April 18, 2026 — Phase 1 Complete, Phase 2 Complete
|
||||
**Reference:** PRD.md, FINDINGS.md, RECOMMENDATION.md
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Critical Fixes & Stabilization
|
||||
|
||||
### Week 1: Critical Bug Fixes ✅ COMPLETE
|
||||
|
||||
- [x] **F1.1** Fix `Customer::update()` fatal error — define `$table_name` and fix `$new_args` reference
|
||||
- `includes/Customer.php` ~line 172
|
||||
- [x] **F1.2** Fix `Order::delete()` — change `$id` to `$order_id` in `$wpdb->delete()` where clause
|
||||
- `includes/Order.php`
|
||||
- [x] **F1.3** Fix `Order::formipay_bulk_delete_order()` — use loop variable `$id` instead of outer `$order_id`
|
||||
- `includes/Order.php`
|
||||
- [x] **F1.4** Fix `Email::send_email()` — change `\Formipay_Notification::` to `parent::`
|
||||
- `includes/Notification/Email.php`
|
||||
- [x] **F1.5** Fix `Paypal::auto_cancel_order_on_timeout()` — add `use Formipay\Order as Order` import
|
||||
- `includes/Integration/Paypal.php`
|
||||
- [x] **F1.6** Fix `Paypal::paypal_settings` — declare property or load from config
|
||||
- `includes/Integration/Paypal.php`
|
||||
- [x] **F1.7** Fix `BankTransfer::add_unique_code_details()` — call `check_unique_code()` once, reuse result
|
||||
- `includes/Payment/BankTransfer.php`
|
||||
- [x] **F1.8** Fix color field label "Number" → "Color"
|
||||
- `admin/functions.php` `formipay_field_type_collection()`
|
||||
- [x] **F1.9** Add `check_ajax_referer` nonce check to `Customer::formipay_tabledata_customers()`
|
||||
- `includes/Customer.php`
|
||||
|
||||
### Week 2: Performance & Security ✅ COMPLETE
|
||||
|
||||
- [x] **F1.10** Replace `maybe_serialize()` in cookie handling with `json_encode()`/`json_decode()`
|
||||
- `includes/Order.php` `retrieve_form_data()`
|
||||
- `includes/Thankyou.php` `get_cookie()`, `request_access_link()`
|
||||
- [x] **F1.11** Move `flush_rewrite_rules()` from `init` to activation hook
|
||||
- `includes/Thankyou.php` `set_endpoint()`
|
||||
- `includes/Payment/Payment.php` `set_endpoint()`
|
||||
- `formipay.php` (add register_activation_hook)
|
||||
- [x] **F1.12** Add PayPal webhook signature verification
|
||||
- `includes/Integration/Paypal.php` `webhook_endpoint()`
|
||||
- [x] **F1.13** Cache JSON file reads in static variables
|
||||
- `admin/functions.php` `formipay_currency_array()`
|
||||
- `admin/functions.php` `formipay_country_array()`
|
||||
- `admin/functions.php` `formipay_get_flag_by_currency()`
|
||||
- [x] **F1.14** Add server-side pagination to `Customer::formipay_tabledata_customers()`
|
||||
- `includes/Customer.php`
|
||||
- [x] **F1.15** Optimize `Order::formipay_tabledata_orders()` — replace two queries with `COUNT(*) GROUP BY status`
|
||||
- `includes/Order.php`
|
||||
- [x] **F1.16** Delete backup file `includes/Integration/Paypal.phpbak`
|
||||
- [x] **F1.17** Create `uninstall.php` — clean up options, custom tables, scheduled events
|
||||
- `uninstall.php` (new file)
|
||||
- [x] **F1.18** Add capability checks (`current_user_can('manage_options')`) to admin-ajax handlers
|
||||
- [x] **F1.19** Fix timezone hardcode — replace `'Asia/Jakarta'` with `wp_timezone_string()`
|
||||
- `includes/Render.php`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — React Admin Foundation (Weeks 3-6 ✅ COMPLETE)
|
||||
|
||||
> ⚠️ **IMPORTANT:** Read `MIGRATION_STRATEGY.md` before starting Phase 2
|
||||
> - Explains Vue → React coexistence strategy
|
||||
> - Documents custom Vue app in `admin-product-editor.js` that needs recreation
|
||||
> - Provides data compatibility requirements and testing strategy
|
||||
|
||||
> **See `MIGRATION_STRATEGY.md`** for detailed Vue → React migration approach
|
||||
|
||||
### Week 3: Build Pipeline ✅ COMPLETE
|
||||
|
||||
- [x] **F2.1** Initialize `package.json` with `@wordpress/scripts`
|
||||
- [x] **F2.2** Create `webpack.config.js` extending wp-scripts
|
||||
- [x] **F2.3** Set up `src/admin/` directory structure
|
||||
- [x] **F2.4** Create API client (`src/admin/api/client.js`) with nonce handling
|
||||
- [x] **F2.5** Create admin page shell component (sidebar + routing)
|
||||
- [x] **F2.6** Register admin menu pages in PHP that render React mount points
|
||||
|
||||
### Week 4: Form Builder ✅ COMPLETE
|
||||
|
||||
- [x] **F2.7** Build field palette component (drag-and-drop source)
|
||||
- [x] **F2.8** Build form canvas component (drop target)
|
||||
- [x] **F2.9** Build field settings panel
|
||||
- [x] **F2.10** Build live preview renderer
|
||||
- [x] **F2.11** Connect to existing PHP save/load endpoints
|
||||
- [x] **F2.12** Replace Vue/Classic Editor metabox with React form builder
|
||||
|
||||
### Week 5: Order Management & Dashboard ✅ COMPLETE
|
||||
|
||||
- [x] **F2.13** Build order list page with filters (status, date, search)
|
||||
- [x] **F2.14** Build order detail view (replace Handlebars templates)
|
||||
- [x] **F2.15** Build status change workflow with timeline
|
||||
- [x] **F2.16** Build analytics dashboard (order count, revenue, charts)
|
||||
- [x] **F2.17** Build notification log viewer
|
||||
|
||||
### Week 6: Settings & Editors ✅ COMPLETE
|
||||
|
||||
- [x] **F2.18** Build global settings page (replace WPCFTO)
|
||||
- [x] **F2.19** Build product editor page (includes variation pricing table)
|
||||
- **See `MIGRATION_STRATEGY.md`** — custom Vue app in `admin-product-editor.js` must be recreated in React
|
||||
- Features: multi-currency flat/expanded pricing, attribute repeater sync, validation
|
||||
- [x] **F2.20** Build coupon editor page
|
||||
- [x] **F2.21** Build access items manager
|
||||
- [x] **F2.22** Build license management page
|
||||
- [x] **F2.23** Remove/deprecate Vue admin code
|
||||
- After all React admin pages are working, remove `admin/assets/vue/` if exists
|
||||
- Remove WPCFTO framework dependency after F2.18
|
||||
- Remove Vue.js from enqueued scripts after F2.19 complete
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Frontend Enhancements
|
||||
|
||||
### Week 7-8: React Island Architecture
|
||||
|
||||
- [ ] **F3.1** Add "Render Mode" setting to form admin (SSR / React)
|
||||
- [ ] **F3.2** Update `Render.php` — output `<div>` mount point when React mode selected
|
||||
- [ ] **F3.3** Build React form renderer component (`src/frontend/widgets/FormRenderer.jsx`)
|
||||
- [ ] **F3.4** Implement island hydration — React attaches to SSR HTML
|
||||
- [ ] **F3.5** Build multi-step form navigation component
|
||||
- [ ] **F3.6** Add real-time field validation in React mode
|
||||
|
||||
### Week 9: Gutenberg Block
|
||||
|
||||
- [ ] **F3.7** Create `block.json` for `formipay/form`
|
||||
- [ ] **F3.8** Build `edit.jsx` — form selector + live preview in editor
|
||||
- [ ] **F3.9** Build `render.php` — server-side rendering for frontend
|
||||
- [ ] **F3.10** Register block in PHP
|
||||
- [ ] **F3.11** Test block in Gutenberg editor
|
||||
|
||||
### Week 10: Customer Portal
|
||||
|
||||
- [ ] **F3.12** Build customer order history page (React)
|
||||
- [ ] **F3.13** Build order detail / download access page (React)
|
||||
- [ ] **F3.14** Build access link request form (React)
|
||||
- [ ] **F3.15** Integrate with WordPress user accounts
|
||||
- [ ] **F3.16** Register `[formipay_my_orders]` shortcode
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Missing Features
|
||||
|
||||
### Week 11-12: Payment & Commerce
|
||||
|
||||
- [ ] **F4.1** Implement `ExchangeRateAPI` — fetch rates, cache, convert
|
||||
- `includes/Payment/ExchangeRateAPI.php`
|
||||
- [ ] **F4.2** Implement License API endpoints (verify, activate, deactivate, revoke)
|
||||
- `includes/LicenseAPI.php`
|
||||
- [ ] **F4.3** Add Stripe payment gateway
|
||||
- `includes/Integration/Stripe.php` (new)
|
||||
- [ ] **F4.4** Add tax calculation engine
|
||||
- `includes/Tax.php` (new)
|
||||
- [ ] **F4.5** Add product variations on frontend (render dropdown, process selection)
|
||||
- `includes/Render.php`, `includes/Order.php`
|
||||
|
||||
### Week 13-14: Stock & Shipping
|
||||
|
||||
- [ ] **F4.6** Implement stock management — decrement on order, validate before submit
|
||||
- `includes/Product.php` (new or extend)
|
||||
- [ ] **F4.7** Add "out of stock" frontend messaging
|
||||
- `includes/Render.php`
|
||||
- [ ] **F4.8** Add refund workflow (status change + payment reversal)
|
||||
- `includes/Order.php`, gateway integrations
|
||||
|
||||
### Week 15-16: Advanced Features
|
||||
|
||||
- [ ] **F4.9** Build donation form mode (pay-what-you-want, suggested amounts)
|
||||
- `includes/Render.php`
|
||||
- [ ] **F4.10** Add PDF invoice generation
|
||||
- `includes/Invoice.php` (new)
|
||||
- [ ] **F4.11** Add CSV export for orders, customers, products
|
||||
- Admin pages
|
||||
- [ ] **F4.12** Add outgoing webhook system
|
||||
- `includes/Webhook.php` (new)
|
||||
- [ ] **F4.13** Add rate limiting on public endpoints
|
||||
- `includes/Order.php`, `includes/Coupon.php`
|
||||
- [ ] **F4.14** Wrap all hardcoded English strings in `__()` translation functions
|
||||
- All PHP + JS files
|
||||
- [ ] **F4.15** Generate `.pot` file for translators
|
||||
- [ ] **F4.16** Add form analytics (view tracking, conversion rate)
|
||||
- `includes/Analytics.php` (new)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (per task)
|
||||
|
||||
- [x] Code compiles without errors
|
||||
- [x] No PHP warnings/notices in debug mode
|
||||
- [x] Manual test passes for the specific fix/feature
|
||||
- [x] No regression in existing functionality
|
||||
- [x] Security-sensitive changes reviewed
|
||||
|
||||
---
|
||||
|
||||
*End of task list.*
|
||||
@@ -64,6 +64,28 @@ jQuery(function($){
|
||||
$('#order-total').html(res.total_formatted);
|
||||
$('#order_status').val(res.status);
|
||||
|
||||
// Populate shipping info if available
|
||||
var shippingInfo = [];
|
||||
if(res.form_data){
|
||||
$.each(res.form_data, function(key, data){
|
||||
if(data.name === 'shipping_country' || data.name === 'shipping_method'){
|
||||
shippingInfo.push(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
if(shippingInfo.length > 0){
|
||||
var source = $("#shipping-info-template").html();
|
||||
var template = Handlebars.compile(source);
|
||||
var context = {
|
||||
datas: shippingInfo
|
||||
};
|
||||
var html = template(context);
|
||||
$("#shipping-info-list").html(html);
|
||||
} else {
|
||||
$("#shipping-info-list").addClass('d-none');
|
||||
$('#no-shipping-info').removeClass('d-none');
|
||||
}
|
||||
|
||||
var source = $("#form-data-item-template").html();
|
||||
var template = Handlebars.compile(source);
|
||||
var context = {
|
||||
|
||||
@@ -122,15 +122,21 @@ function get_global_currency_array() {
|
||||
if(false === $ifSingleCurrency){
|
||||
// $currency_sort = [];
|
||||
$default_sort_key = null;
|
||||
// Extract currency code from default_currency for comparison (handles case where default has symbol but multicurrencies don't)
|
||||
$default_currency_code = explode(':::', $default_currency)[0];
|
||||
foreach($global_currencies as $key => $currency){
|
||||
$currency_value = $currency['currency'];
|
||||
if($currency_value === $default_currency){
|
||||
// Compare by currency code only (before first :::)
|
||||
$currency_code = explode(':::', $currency_value)[0];
|
||||
if($currency_code === $default_currency_code){
|
||||
$default_sort_key = $key;
|
||||
}
|
||||
}
|
||||
$currency_sort = [$default_sort_key => $global_currencies[$default_sort_key]];
|
||||
unset($global_currencies[$default_sort_key]);
|
||||
$global_currencies = $currency_sort + $global_currencies;
|
||||
// Convert associative array to indexed array for JavaScript
|
||||
$global_currencies = array_values($global_currencies);
|
||||
}else{
|
||||
if(false === boolval($multicurrency)){
|
||||
$global_currencies = [
|
||||
@@ -176,6 +182,31 @@ function formipay_get_flag_by_currency($currency) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all currency flags from flags.json
|
||||
* Returns an array mapping currency codes to base64 flag images
|
||||
* This is the single source of truth for currency flags - never duplicate this data
|
||||
*
|
||||
* @return array Array of currency code => flag image mapping
|
||||
*/
|
||||
function formipay_get_all_currency_flags() {
|
||||
static $currency_flags = null;
|
||||
|
||||
if ($currency_flags !== null) {
|
||||
return $currency_flags;
|
||||
}
|
||||
|
||||
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/flags.json');
|
||||
$flags = json_decode($json, true);
|
||||
|
||||
$currency_flags = [];
|
||||
foreach ($flags as $item) {
|
||||
$currency_flags[$item['code']] = $item['flag'];
|
||||
}
|
||||
|
||||
return $currency_flags;
|
||||
}
|
||||
|
||||
function formipay_price_format($num = 0, $post_id = 0){
|
||||
|
||||
$decimal_digits = 2;
|
||||
@@ -361,7 +392,15 @@ function formipay_get_order($order_id) {
|
||||
case 'payment_gateway':
|
||||
$label = esc_html__( 'Payment Gateway', 'formipay' );
|
||||
break;
|
||||
|
||||
|
||||
case 'shipping_country':
|
||||
$label = esc_html__( 'Shipping Country', 'formipay' );
|
||||
break;
|
||||
|
||||
case 'shipping_method':
|
||||
$label = esc_html__( 'Shipping Method', 'formipay' );
|
||||
break;
|
||||
|
||||
default:
|
||||
if(!empty($all_fields[$name.'_config'])){
|
||||
$label = $all_fields[$name.'_config']['label'];
|
||||
|
||||
@@ -55,6 +55,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-detail-card shipping-info-card">
|
||||
<div class="card-title mt-3 mb-0"><?php echo esc_html__( 'Shipping Information', 'formipay' ); ?></div>
|
||||
<div class="card mt-1 border-0 rounded-4 shadow-sm">
|
||||
<div class="card-body p-0 placeholder-glow">
|
||||
<ul class="list-group list-group-flush" id="shipping-info-list">
|
||||
<li class="list-group-item">
|
||||
<b><span class="placeholder col-3"></span></b>
|
||||
<p class="mb-0"><span class="placeholder col-8"></span></p>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b><span class="placeholder col-3"></span></b>
|
||||
<p class="mb-0"><span class="placeholder col-8"></span></p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-none" id="no-shipping-info">
|
||||
<p class="text-center text-muted my-3"><?php echo esc_html__( 'No shipping information available', 'formipay' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-detail-card form-data-card">
|
||||
<div class="card-title mt-3 w-100 mb-0 d-flex justify-content-between align-items-center">
|
||||
<?php echo esc_html__( 'Form Data', 'formipay' ); ?>
|
||||
@@ -273,4 +293,12 @@
|
||||
<b><?php echo esc_html__('Access Password', 'formipay'); ?></b>
|
||||
<p class="mb-0">******</p>
|
||||
</li>
|
||||
</script>
|
||||
<script id="shipping-info-template" type="text/x-handlebars-template">
|
||||
{{#each datas as |data|}}
|
||||
<li class="list-group-item px-0">
|
||||
<b class="field-name">{{data.label}}</b>
|
||||
<p class="field-value mt-1 mb-0">{{data.value}}</p>
|
||||
</li>
|
||||
{{/each}}
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '4e4bf3366b83c9df1a24');
|
||||
<?php return array('dependencies' => array('react', 'react-dom', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash'), 'version' => 'e564b3f018fca608f7b7');
|
||||
|
||||
648
build/admin.css
648
build/admin.css
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/admin/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "hugeicons"
|
||||
}
|
||||
@@ -24,6 +24,11 @@ class Access {
|
||||
add_filter( 'formipay/access-config', [$this, 'source_config'] );
|
||||
add_filter( 'formipay/access-config', [$this, 'details_config'] );
|
||||
|
||||
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
|
||||
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'save_post', [$this, 'save_access_metabox_fields'], 10, 2 );
|
||||
|
||||
// Admin Page
|
||||
add_action( 'wp_ajax_formipay_access_items_get_products', [$this, 'formipay_access_items_get_products'] );
|
||||
add_action( 'wp_ajax_formipay-tabledata-access-items', [$this, 'formipay_tabledata_access_items'] );
|
||||
@@ -90,82 +95,77 @@ class Access {
|
||||
|
||||
public function enqueue_admin() {
|
||||
// Assets now handled by ReactAdmin class
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
public function add_react_metabox() {
|
||||
add_meta_box(
|
||||
'formipay_access_settings',
|
||||
__('Settings', 'formipay'),
|
||||
[$this, 'render_react_metabox'],
|
||||
'formipay-access',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
// Check that we are on the 'Checker' post editor screen
|
||||
if ( $screen->post_type === 'formipay-access' && $screen->base === 'post' ) {
|
||||
|
||||
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION );
|
||||
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
||||
wp_enqueue_script( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||
public function render_react_metabox($post) {
|
||||
echo '<div data-formipay-field-renderer="access" data-post-id="' . esc_attr($post->ID) . '"></div>';
|
||||
}
|
||||
|
||||
wp_localize_script( 'sweetalert2', 'formipay_admin', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'site_url' => site_url(),
|
||||
] );
|
||||
public function render_react_metabox_template() {
|
||||
global $post;
|
||||
|
||||
if (!$post || $post->post_type !== 'formipay-access') {
|
||||
return;
|
||||
}
|
||||
|
||||
if($current_screen->id == 'formipay_page_formipay-access-items') {
|
||||
|
||||
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], 'all');
|
||||
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION, 'all' );
|
||||
wp_enqueue_style( 'gridjs', FORMIPAY_URL . 'vendor/GridJS/gridjs.mermaid.min.css', [], '6.2.0', 'all' );
|
||||
wp_enqueue_style( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.css', [], FORMIPAY_VERSION, 'all' );
|
||||
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION, 'all' );
|
||||
wp_enqueue_style( 'page-access-items', FORMIPAY_URL . 'admin/assets/css/page-access-items.css', [], FORMIPAY_VERSION, 'all' );
|
||||
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
wp_enqueue_script( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.5', true);
|
||||
wp_enqueue_script( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/js/admin-pages.js', ['jquery'], FORMIPAY_VERSION, true );
|
||||
wp_enqueue_script( 'gridjs', FORMIPAY_URL . 'vendor/GridJS/gridjs.production.min.js', ['jquery'], '6.2.0', true );
|
||||
wp_enqueue_script( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.js', [], FORMIPAY_VERSION, true );
|
||||
wp_enqueue_script( 'page-access-items', FORMIPAY_URL . 'admin/assets/js/page-access-items.js', ['jquery', 'gridjs'], FORMIPAY_VERSION, true );
|
||||
|
||||
wp_localize_script( 'page-access-items', 'formipay_access_page', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'site_url' => site_url(),
|
||||
'columns' => [
|
||||
'id' => esc_html__( 'ID', 'formipay' ),
|
||||
'title' => esc_html__( 'Title', 'formipay' ),
|
||||
'type' => esc_html__( 'Type', 'formipay' ),
|
||||
'products' => esc_html__( 'Product Relation', 'formipay' ),
|
||||
'status' => esc_html__( 'Status', 'formipay' ),
|
||||
],
|
||||
'filter_form' => [
|
||||
'products' => [
|
||||
'placeholder' => esc_html__( 'Filter by Product', 'formipay' ),
|
||||
'noresult_text' => esc_html__( 'No results found', 'formipay' )
|
||||
]
|
||||
],
|
||||
'modal' => [
|
||||
'add' => [
|
||||
'title' => esc_html__( 'Your New Item Title', 'formipay' ),
|
||||
'validation' => esc_html__( 'Item\'s title is still empty. Please input the title before continue', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Create New Item', 'formipay' )
|
||||
],
|
||||
'delete' => [
|
||||
'question' => esc_html__( 'Do you want to delete the item?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Delete Permanently', 'formipay' )
|
||||
],
|
||||
'bulk_delete' => [
|
||||
'question' => esc_html__( 'Do you want to delete the selected the form(s)?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
||||
],
|
||||
'duplicate' => [
|
||||
'question' => esc_html__( 'Do you want to duplicate the item?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
||||
],
|
||||
],
|
||||
'nonce' => wp_create_nonce('formipay-admin-access-nonce')
|
||||
] );
|
||||
public function save_access_metabox_fields($post_id, $post) {
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-post_' . $post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if ($post->post_type !== 'formipay-access') {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
$meta_fields = [
|
||||
'access_type',
|
||||
'access_document',
|
||||
'access_redirect_url',
|
||||
'access_download_source',
|
||||
'access_attachment',
|
||||
'access_url',
|
||||
'button_text',
|
||||
'details_icon',
|
||||
'details_filetype',
|
||||
'details_filesize',
|
||||
'details_short_description',
|
||||
];
|
||||
|
||||
foreach ($meta_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$value = wp_unslash($_POST[$field]);
|
||||
update_post_meta($post_id, $field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function cpt_post_fields_box($boxes) {
|
||||
|
||||
460
includes/Admin/FieldConfigBridge.php
Normal file
460
includes/Admin/FieldConfigBridge.php
Normal file
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
namespace Formipay\Admin;
|
||||
|
||||
use Formipay\Traits\SingletonTrait;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* FieldConfigBridge - Transforms WPCFTO field config arrays to React-ready JSON
|
||||
*
|
||||
* Bridges the gap between PHP field configurations and the React FieldRenderer:
|
||||
* - Collects field configs from filter system
|
||||
* - Loads saved post meta / option values
|
||||
* - Transforms PHP config format to React-ready structure
|
||||
* - Handles dynamic currency field expansion
|
||||
* - Handles dependency transformation
|
||||
*/
|
||||
class FieldConfigBridge {
|
||||
|
||||
use SingletonTrait;
|
||||
|
||||
/**
|
||||
* Map CPT types to their filter names
|
||||
*/
|
||||
private static $cpt_filter_map = [
|
||||
'formipay-coupon' => 'formipay/coupon-config',
|
||||
'formipay-product' => 'formipay/product-config',
|
||||
'formipay-form' => 'formipay/form-config',
|
||||
'formipay-access' => 'formipay/access-config',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get complete field configuration for a post with saved values
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @param string $post_type Post type
|
||||
* @return array React-ready field configuration
|
||||
*/
|
||||
public static function get_config_for_post($post_id, $post_type) {
|
||||
$filter_name = self::$cpt_filter_map[$post_type] ?? null;
|
||||
|
||||
if (!$filter_name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get field configuration from filters
|
||||
$fields = apply_filters($filter_name, []);
|
||||
|
||||
// Load saved values from post meta
|
||||
$values = self::load_post_meta_values($post_id, $fields);
|
||||
|
||||
// Transform to React-ready format
|
||||
return self::transform_fields_config($fields, $values, $post_id, $post_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for settings page (wp_options based)
|
||||
*
|
||||
* @param string $option_name Option name
|
||||
* @return array React-ready field configuration
|
||||
*/
|
||||
public static function get_config_for_settings($option_name = 'formipay_settings') {
|
||||
// Get settings values
|
||||
$settings = get_option($option_name, []);
|
||||
|
||||
// Get field definitions directly from Settings class
|
||||
$settings_instance = \Formipay\Settings::get_instance();
|
||||
$tabs_config = $settings_instance->get_settings_fields();
|
||||
|
||||
// Transform to React-ready format
|
||||
return self::transform_settings_config($settings, $tabs_config, $option_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform settings config to React-ready format
|
||||
*
|
||||
* @param array $settings Settings values
|
||||
* @param array $tabs_config Tab configuration from Settings class
|
||||
* @param string $option_name Option name
|
||||
* @return array React-ready configuration
|
||||
*/
|
||||
private static function transform_settings_config($settings, $tabs_config, $option_name) {
|
||||
$tabs = [];
|
||||
|
||||
foreach ($tabs_config as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['name']) || !isset($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tab = [
|
||||
'id' => self::slugify($tab_data['name']),
|
||||
'label' => $tab_data['name'],
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$current_group = null;
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Handle group_title (section headers)
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
$group_value = $field['group'] ?? '';
|
||||
|
||||
if ($group_value === 'started') {
|
||||
$current_group = [
|
||||
'type' => 'section',
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
];
|
||||
} elseif ($group_value === 'ended') {
|
||||
$current_group = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add section if we have one
|
||||
if ($current_group !== null) {
|
||||
$tab['fields'][] = $current_group;
|
||||
$current_group = null;
|
||||
}
|
||||
|
||||
// Transform field configuration
|
||||
$transformed_field = self::transform_settings_field($field_name, $field, $settings);
|
||||
|
||||
// Add to fields
|
||||
$tab['fields'][] = $transformed_field;
|
||||
}
|
||||
|
||||
$tabs[] = $tab;
|
||||
}
|
||||
|
||||
return [
|
||||
'tabs' => $tabs,
|
||||
'optionName' => $option_name,
|
||||
'globalCurrencies' => self::get_global_currencies_data(),
|
||||
'nonce' => wp_create_nonce('formipay-field-config'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load saved post meta values for all fields
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @param array $fields Field configuration array
|
||||
* @return array Field values keyed by field name
|
||||
*/
|
||||
private static function load_post_meta_values($post_id, $fields) {
|
||||
$values = [];
|
||||
|
||||
// The config structure is: $fields['formipay_{cpt}_settings'][tab_key] = ['name' => ..., 'fields' => [...]]
|
||||
foreach ($fields as $container_key => $container_data) {
|
||||
if (!is_array($container_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($container_data as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['fields']) || !is_array($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Skip group_title fields
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$meta_value = get_post_meta($post_id, $field_name, true);
|
||||
$values[$field_name] = $meta_value !== '' ? $meta_value : ($field['value'] ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform fields config to React-ready format
|
||||
*
|
||||
* @param array $fields Raw field configuration from PHP
|
||||
* @param array $values Saved field values
|
||||
* @param int $post_id Post ID
|
||||
* @param string $post_type Post type
|
||||
* @return array React-ready configuration
|
||||
*/
|
||||
private static function transform_fields_config($fields, $values, $post_id, $post_type) {
|
||||
$tabs = [];
|
||||
|
||||
// The config structure is: $fields['formipay_{cpt}_settings'][tab_key] = ['name' => ..., 'fields' => [...]]
|
||||
foreach ($fields as $container_key => $container_data) {
|
||||
if (!is_array($container_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($container_data as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['name']) || !isset($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tab = [
|
||||
'id' => $tab_key,
|
||||
'label' => $tab_data['name'],
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$current_group = null;
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Handle group_title (section headers)
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
$group_value = $field['group'] ?? '';
|
||||
|
||||
if ($group_value === 'started') {
|
||||
$current_group = [
|
||||
'type' => 'section',
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
];
|
||||
} elseif ($group_value === 'ended') {
|
||||
$current_group = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add section if we have one
|
||||
if ($current_group !== null) {
|
||||
$tab['fields'][] = $current_group;
|
||||
$current_group = null;
|
||||
}
|
||||
|
||||
// Transform field configuration
|
||||
$transformed_field = self::transform_single_field($field_name, $field, $values);
|
||||
|
||||
// Add to fields
|
||||
$tab['fields'][] = $transformed_field;
|
||||
}
|
||||
|
||||
$tabs[] = $tab;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'tabs' => $tabs,
|
||||
'postId' => $post_id,
|
||||
'postType' => $post_type,
|
||||
'globalCurrencies' => self::get_global_currencies_data(),
|
||||
'nonce' => wp_create_nonce('formipay-field-config'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single field configuration
|
||||
*
|
||||
* @param string $field_name Field name
|
||||
* @param array $field Field configuration
|
||||
* @param array $values Saved values
|
||||
* @return array Transformed field config
|
||||
*/
|
||||
private static function transform_single_field($field_name, $field, $values) {
|
||||
$transformed = [
|
||||
'name' => $field_name,
|
||||
'type' => self::transform_field_type($field['type'] ?? 'text'),
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
'value' => $values[$field_name] ?? $field['value'] ?? '',
|
||||
'required' => !empty($field['required']),
|
||||
'dependency' => isset($field['dependency']) ? self::transform_dependency($field['dependency']) : null,
|
||||
];
|
||||
|
||||
// Add options for select/radio fields
|
||||
if (isset($field['options'])) {
|
||||
$transformed['options'] = $field['options'];
|
||||
}
|
||||
|
||||
// Add subtype info for currency fields
|
||||
if (isset($field['step'])) {
|
||||
$transformed['step'] = $field['step'];
|
||||
}
|
||||
if (isset($field['min'])) {
|
||||
$transformed['min'] = $field['min'];
|
||||
}
|
||||
if (isset($field['placeholder'])) {
|
||||
$transformed['placeholder'] = $field['placeholder'];
|
||||
}
|
||||
|
||||
// Handle special field types
|
||||
if (isset($field['repeater_fields'])) {
|
||||
$transformed['fields'] = $field['repeater_fields'];
|
||||
}
|
||||
|
||||
// Pass post_type for autocomplete fields (CPTs)
|
||||
if (isset($field['post_type'])) {
|
||||
$transformed['post_type'] = $field['post_type'];
|
||||
}
|
||||
|
||||
// Pass object_type for autocomplete fields (e.g., 'user' for WP Users)
|
||||
if (isset($field['object_type'])) {
|
||||
$transformed['object_type'] = $field['object_type'];
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform field type from PHP to React format
|
||||
*
|
||||
* @param string $type PHP field type
|
||||
* @return string React field type
|
||||
*/
|
||||
private static function transform_field_type($type) {
|
||||
$type_map = [
|
||||
'checkbox' => 'switch', // Use toggle switch UI
|
||||
'tinymce' => 'editor', // Rich text editor
|
||||
'editor' => 'editor', // Map 'editor' type to tinymce field
|
||||
'image' => 'image', // WordPress Media Library picker
|
||||
'notification_message' => 'notification', // Notification banner component
|
||||
];
|
||||
|
||||
return $type_map[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform dependency from PHP to React format
|
||||
*
|
||||
* @param array $dependency PHP dependency config
|
||||
* @return array React dependency config
|
||||
*/
|
||||
private static function transform_dependency($dependency) {
|
||||
// Handle multiple dependencies (AND logic)
|
||||
if (isset($dependency[0]) && is_array($dependency[0])) {
|
||||
return [
|
||||
'mode' => 'and',
|
||||
'rules' => array_map([self::class, 'transform_single_dependency'], $dependency),
|
||||
];
|
||||
}
|
||||
|
||||
return self::transform_single_dependency($dependency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single dependency rule
|
||||
*
|
||||
* @param array $dependency PHP dependency
|
||||
* @return array React dependency rule
|
||||
*/
|
||||
private static function transform_single_dependency($dependency) {
|
||||
$rule = [
|
||||
'field' => $dependency['key'] ?? '',
|
||||
];
|
||||
|
||||
$value = $dependency['value'] ?? '';
|
||||
|
||||
// Handle special values
|
||||
if ($value === 'not_empty') {
|
||||
$rule['operator'] = 'not_empty';
|
||||
} elseif ($value === 'empty') {
|
||||
$rule['operator'] = 'empty';
|
||||
} else {
|
||||
$rule['value'] = $value;
|
||||
$rule['operator'] = 'eq';
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global currencies data for React
|
||||
*
|
||||
* @return array Currencies data
|
||||
*/
|
||||
private static function get_global_currencies_data() {
|
||||
$currencies = get_global_currency_array();
|
||||
|
||||
return array_map(function($currency) {
|
||||
$parts = explode(':::', $currency['currency']);
|
||||
|
||||
return [
|
||||
'currency' => $currency['currency'],
|
||||
'code' => $parts[0] ?? '',
|
||||
'title' => $parts[1] ?? '',
|
||||
'symbol' => $parts[2] ?? $parts[1] ?? '',
|
||||
'decimalDigits' => intval($currency['decimal_digits'] ?? 2),
|
||||
'decimalSymbol' => $currency['decimal_symbol'] ?? '.',
|
||||
'thousandSeparator' => $currency['thousand_separator'] ?? ',',
|
||||
'flag' => formipay_get_flag_by_currency($currency['currency']),
|
||||
];
|
||||
}, $currencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single settings field configuration
|
||||
*
|
||||
* @param string $field_name Field name
|
||||
* @param array $field Field configuration
|
||||
* @param array $settings Saved settings values
|
||||
* @return array Transformed field config
|
||||
*/
|
||||
private static function transform_settings_field($field_name, $field, $settings) {
|
||||
$transformed = [
|
||||
'name' => $field_name,
|
||||
'type' => self::transform_field_type($field['type'] ?? 'text'),
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
'value' => $settings[$field_name] ?? $field['value'] ?? '',
|
||||
'required' => !empty($field['required']),
|
||||
'dependency' => isset($field['dependency']) ? self::transform_dependency($field['dependency']) : null,
|
||||
];
|
||||
|
||||
// Add options for select/radio/multi_checkbox fields
|
||||
if (isset($field['options'])) {
|
||||
$transformed['options'] = $field['options'];
|
||||
}
|
||||
|
||||
// Add options for image_select
|
||||
if (isset($field['options']) && $field['type'] === 'image_select') {
|
||||
$transformed['width'] = $field['width'] ?? 100;
|
||||
$transformed['height'] = $field['height'] ?? 100;
|
||||
}
|
||||
|
||||
// Add hints for hint_textarea
|
||||
if (isset($field['hints'])) {
|
||||
$transformed['hints'] = $field['hints'];
|
||||
}
|
||||
|
||||
// Add step/min for number fields
|
||||
if (isset($field['step'])) {
|
||||
$transformed['step'] = $field['step'];
|
||||
}
|
||||
if (isset($field['min'])) {
|
||||
$transformed['min'] = $field['min'];
|
||||
}
|
||||
if (isset($field['placeholder'])) {
|
||||
$transformed['placeholder'] = $field['placeholder'];
|
||||
}
|
||||
if (isset($field['rows'])) {
|
||||
$transformed['rows'] = $field['rows'];
|
||||
}
|
||||
|
||||
// Handle repeater fields
|
||||
if (isset($field['fields'])) {
|
||||
// Transform sub-fields to include name property
|
||||
$sub_fields = [];
|
||||
foreach ($field['fields'] as $sub_field_name => $sub_field) {
|
||||
$sub_field['name'] = $sub_field_name;
|
||||
$sub_fields[] = $sub_field;
|
||||
}
|
||||
$transformed['fields'] = $sub_fields;
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to URL-safe slug
|
||||
*
|
||||
* @param string $text Text to slugify
|
||||
* @return string URL-safe slug
|
||||
*/
|
||||
private static function slugify($text) {
|
||||
return strtolower(sanitize_title($text));
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,20 @@ class ReactAdmin {
|
||||
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
|
||||
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
|
||||
|
||||
// AJAX endpoint for field configuration
|
||||
add_action( 'wp_ajax_formipay-get-field-config', [$this, 'ajax_get_field_config'] );
|
||||
|
||||
}
|
||||
|
||||
public function enqueue_assets() {
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
// Only load React assets on Formipay admin pages
|
||||
if ( strpos( $screen->id, 'formipay' ) === false ) {
|
||||
// Load React assets on Formipay admin pages OR on post edit screens for our CPTs
|
||||
$is_formipay_admin = strpos($screen->id, 'formipay') !== false;
|
||||
$is_formipay_cpt = $screen->base === 'post' && in_array($screen->post_type, ['formipay-coupon', 'formipay-product', 'formipay-form']);
|
||||
|
||||
if (!$is_formipay_admin && !$is_formipay_cpt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,7 +34,7 @@ class ReactAdmin {
|
||||
$build_dir = FORMIPAY_PATH . 'build';
|
||||
$build_url = FORMIPAY_URL . 'build';
|
||||
|
||||
if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) {
|
||||
if (!file_exists($build_dir . '/admin.asset.php')) {
|
||||
error_log('[Formipay] Build files not found at: ' . $build_dir . '/admin.asset.php');
|
||||
return; // Build not generated yet
|
||||
}
|
||||
@@ -37,18 +43,16 @@ class ReactAdmin {
|
||||
$dependencies = $assets_file['dependencies'] ?? [];
|
||||
|
||||
// Filter out icon build dependencies - they're bundled, not separate scripts
|
||||
$original_count = count($dependencies);
|
||||
$dependencies = array_values(array_filter($dependencies, function($dep) {
|
||||
return strpos($dep, 'wp-icons/build/') === false;
|
||||
}));
|
||||
error_log('[Formipay] Filtered dependencies: ' . $original_count . ' -> ' . count($dependencies));
|
||||
|
||||
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
|
||||
|
||||
wp_enqueue_style(
|
||||
'formipay-admin-style',
|
||||
$build_url . '/admin.css',
|
||||
[],
|
||||
['wp-admin', 'colors', 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus'],
|
||||
$version
|
||||
);
|
||||
|
||||
@@ -61,19 +65,15 @@ class ReactAdmin {
|
||||
);
|
||||
|
||||
// Localize script with required data
|
||||
$data = apply_filters( 'formipay/admin/data', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'restUrl' => rest_url( 'formipay/v1' ),
|
||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
||||
$data = apply_filters('formipay/admin/data', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'restUrl' => rest_url('formipay/v1'),
|
||||
'nonce' => wp_create_nonce('formipay-admin'),
|
||||
'pluginUrl' => FORMIPAY_URL,
|
||||
'siteUrl' => site_url(),
|
||||
] );
|
||||
]);
|
||||
|
||||
// Debug logging
|
||||
error_log('[Formipay] Enqueuing React assets on screen: ' . $screen->id);
|
||||
error_log('[Formipay] Page data: ' . wp_json_encode($data));
|
||||
|
||||
wp_localize_script( 'formipay-admin', 'formipayAdmin', $data );
|
||||
wp_localize_script('formipay-admin', 'formipayAdmin', $data);
|
||||
|
||||
}
|
||||
|
||||
@@ -158,4 +158,27 @@ class ReactAdmin {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for getting field configuration
|
||||
*/
|
||||
public function ajax_get_field_config() {
|
||||
|
||||
check_ajax_referer( 'formipay-admin', '_wpnonce', '', true );
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('Unauthorized', 'formipay')]);
|
||||
}
|
||||
|
||||
$post_id = isset($_REQUEST['post_id']) ? intval($_REQUEST['post_id']) : 0;
|
||||
$post_type = isset($_REQUEST['post_type']) ? sanitize_text_field($_REQUEST['post_type']) : '';
|
||||
|
||||
if (!$post_id || !$post_type) {
|
||||
wp_send_json_error(['message' => __('Invalid request', 'formipay')]);
|
||||
}
|
||||
|
||||
$config = FieldConfigBridge::get_config_for_post($post_id, $post_type);
|
||||
|
||||
wp_send_json_success($config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,17 @@ class Coupon {
|
||||
add_action( 'wp_ajax_formipay-delete-coupon', [$this, 'formipay_delete_coupon'] );
|
||||
add_action( 'wp_ajax_formipay-bulk-delete-coupon', [$this, 'formipay_bulk_delete_coupon'] );
|
||||
add_action( 'wp_ajax_formipay-duplicate-coupon', [$this, 'formipay_duplicate_coupon'] );
|
||||
add_action( 'wp_ajax_formipay-get-coupon', [$this, 'formipay_get_coupon'] );
|
||||
add_action( 'wp_ajax_formipay-save-coupon', [$this, 'formipay_save_coupon'] );
|
||||
add_action( 'wp_ajax_formipay-autocomplete-search', [$this, 'formipay_autocomplete_search'] );
|
||||
|
||||
// React Metabox
|
||||
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
|
||||
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
|
||||
|
||||
// Save coupon data via WordPress save_post hook (regular hook with post type check inside)
|
||||
add_action( 'save_post', [$this, 'save_coupon_on_post_update'], 10, 2 );
|
||||
|
||||
// Order
|
||||
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
||||
@@ -99,79 +110,22 @@ class Coupon {
|
||||
}
|
||||
|
||||
public function enqueue_admin() {
|
||||
// Assets now handled by ReactAdmin class
|
||||
return;
|
||||
|
||||
if($current_screen->id == 'formipay_page_formipay-coupons') {
|
||||
|
||||
wp_enqueue_style( 'page-coupons', FORMIPAY_URL . 'admin/assets/css/page-coupons.css', [], FORMIPAY_VERSION, 'all' );
|
||||
wp_enqueue_script( 'page-coupons', FORMIPAY_URL . 'admin/assets/js/page-coupons.js', ['jquery', 'gridjs'], FORMIPAY_VERSION, true );
|
||||
|
||||
wp_localize_script( 'page-coupons', 'formipay_coupons_page', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'site_url' => site_url(),
|
||||
'columns' => [
|
||||
'id' => esc_html__( 'ID', 'formipay' ),
|
||||
'code' => esc_html__( 'Coupon Code', 'formipay' ),
|
||||
'usages' => esc_html__( 'Usages', 'formipay' ),
|
||||
'date_limit' => esc_html__( 'Date Limit', 'formipay' ),
|
||||
'status' => esc_html__( 'Status', 'formipay' ),
|
||||
'type' => esc_html__( 'Type', 'formipay' ),
|
||||
'amount' => esc_html__( 'Amount', 'formipay' )
|
||||
],
|
||||
'filter_form' => [
|
||||
'products' => [
|
||||
'placeholder' => esc_html__( 'Filter by Product', 'formipay' ),
|
||||
'noresult_text' => esc_html__( 'No results found', 'formipay' )
|
||||
]
|
||||
],
|
||||
'modal' => [
|
||||
'add' => [
|
||||
'title' => esc_html__( 'Your New Coupon Code', 'formipay' ),
|
||||
'validation' => esc_html__( 'Coupon code is still empty. Please input the code before continue', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Create New Coupon', 'formipay' )
|
||||
],
|
||||
'delete' => [
|
||||
'question' => esc_html__( 'Do you want to delete the coupon?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Delete Permanently', 'formipay' )
|
||||
],
|
||||
'bulk_delete' => [
|
||||
'question' => esc_html__( 'Do you want to delete the selected the coupon(s)?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
||||
],
|
||||
'duplicate' => [
|
||||
'question' => esc_html__( 'Do you want to duplicate the coupon?', 'formipay' ),
|
||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
||||
],
|
||||
],
|
||||
'nonce' => wp_create_nonce('formipay-admin-coupon-page')
|
||||
] );
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( $screen->post_type === 'formipay-coupon' && $screen->base === 'post' ) {
|
||||
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
||||
wp_enqueue_script( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||
|
||||
wp_localize_script( 'sweetalert2', 'formipay_admin', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'site_url' => site_url(),
|
||||
] );
|
||||
|
||||
// Enqueue SweetAlert2 for coupon post edit screen
|
||||
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
|
||||
wp_enqueue_style('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
||||
wp_enqueue_script('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function cpt_post_fields_box($boxes) {
|
||||
$boxes['formipay_coupon_settings'] = array(
|
||||
'post_type' => array('formipay-coupon'),
|
||||
'label' => __('Details', 'formipay'),
|
||||
);
|
||||
|
||||
// Disabled - using React metabox instead
|
||||
// $boxes['formipay_coupon_settings'] = array(
|
||||
// 'post_type' => array('formipay-coupon'),
|
||||
// 'label' => __('Details', 'formipay'),
|
||||
// );
|
||||
|
||||
return $boxes;
|
||||
}
|
||||
|
||||
@@ -197,6 +151,7 @@ class Coupon {
|
||||
$rules_group_1 = array(
|
||||
'rules_general_group' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'General', 'formipay' ),
|
||||
'group' => 'started'
|
||||
),
|
||||
'active' => array(
|
||||
@@ -212,6 +167,7 @@ class Coupon {
|
||||
'fixed' => __( 'Fixed', 'formipay' ),
|
||||
'percentage' => __( 'Percentage', 'formipay' )
|
||||
),
|
||||
'value' => 'fixed'
|
||||
),
|
||||
'amount_percentage' => array(
|
||||
'type' => 'number',
|
||||
@@ -252,8 +208,7 @@ class Coupon {
|
||||
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
||||
$decimal_digits = intval($currency['decimal_digits']);
|
||||
$step = $decimal_digits * 10;
|
||||
$step = $step > 0 ? 1 / $step : 1;
|
||||
$step = $decimal_digits > 0 ? pow(10, -$decimal_digits) : 1;
|
||||
|
||||
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
|
||||
'type' => 'number',
|
||||
@@ -281,7 +236,11 @@ class Coupon {
|
||||
),
|
||||
'step' => $step,
|
||||
'min' => 0,
|
||||
'placeholder' => __( 'Enter Max Amount...', 'formipay' )
|
||||
'placeholder' => __( 'Enter Max Amount...', 'formipay' ),
|
||||
'dependency' => array(
|
||||
'key' => 'type',
|
||||
'value' => 'percentage'
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
@@ -367,7 +326,8 @@ class Coupon {
|
||||
'use_limit' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Usage Limit', 'formipay' ),
|
||||
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' )
|
||||
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' ),
|
||||
'placeholder' => __( 'Set limit...', 'formipay' )
|
||||
),
|
||||
'date_limit' => array(
|
||||
'type' => 'date',
|
||||
@@ -386,11 +346,11 @@ class Coupon {
|
||||
'label' => __( 'Products', 'formipay' ),
|
||||
'description' => __( 'Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay' )
|
||||
),
|
||||
'customers' => array(
|
||||
'users' => array(
|
||||
'type' => 'autocomplete',
|
||||
'post_type' => array('formipay-product'),
|
||||
'object_type' => 'user',
|
||||
'label' => __( 'Customers', 'formipay' ),
|
||||
'description' => __( 'Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay' )
|
||||
'description' => __( 'Only selected registered customer(s) can use this coupon. Leave empty to apply to all customers.', 'formipay' )
|
||||
)
|
||||
);
|
||||
|
||||
@@ -631,6 +591,14 @@ class Coupon {
|
||||
}
|
||||
|
||||
$date_limit = formipay_get_post_meta($coupon->ID, 'date_limit');
|
||||
$date_limit_display = 'none';
|
||||
if ($date_limit && $date_limit !== '') {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $date_limit)) {
|
||||
$date_limit_display = $date_limit;
|
||||
} elseif (is_numeric($date_limit)) {
|
||||
$date_limit_display = formipay_date('Y-m-d', intval($date_limit) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
$type = formipay_get_post_meta($coupon->ID, 'type');
|
||||
$amount_meta_key = "amount_$type";
|
||||
@@ -665,7 +633,7 @@ class Coupon {
|
||||
'case_sensitive' => formipay_get_post_meta($coupon->ID, 'case_sensitive'),
|
||||
'usage_count' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
|
||||
'usages' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
|
||||
'date_limit' => false !== $date_limit ? formipay_date('Y-m-d', intval(formipay_get_post_meta($coupon->ID, 'date_limit')) / 1000) : 'none',
|
||||
'date_limit' => $date_limit_display,
|
||||
'active' => $is_active ? 'on' : 'off',
|
||||
'post_status' => $is_active ? 'active' : 'inactive',
|
||||
'status' => $is_active ? 'active' : 'inactive'
|
||||
@@ -888,4 +856,436 @@ class Coupon {
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon data for React editor
|
||||
*/
|
||||
public function formipay_get_coupon() {
|
||||
|
||||
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||
}
|
||||
|
||||
$post_id = isset($_REQUEST['id']) ? intval($_REQUEST['id']) : 0;
|
||||
|
||||
if ( empty($post_id) ) {
|
||||
wp_send_json_error( [ 'message' => esc_html__( 'Coupon ID is required.', 'formipay' ) ] );
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
if ( ! $post || $post->post_type !== 'formipay-coupon' ) {
|
||||
wp_send_json_error( [ 'message' => esc_html__( 'Coupon not found.', 'formipay' ) ] );
|
||||
}
|
||||
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Build coupon data
|
||||
$coupon_data = [
|
||||
'ID' => $post_id,
|
||||
'post_title' => $post->post_title,
|
||||
'active' => formipay_get_post_meta($post_id, 'active'),
|
||||
'type' => formipay_get_post_meta($post_id, 'type'),
|
||||
'amount_percentage' => formipay_get_post_meta($post_id, 'amount_percentage'),
|
||||
'case_sensitive' => formipay_get_post_meta($post_id, 'case_sensitive'),
|
||||
'free_shipping' => formipay_get_post_meta($post_id, 'free_shipping'),
|
||||
'quantity_active' => formipay_get_post_meta($post_id, 'quantity_active'),
|
||||
'use_limit' => formipay_get_post_meta($post_id, 'use_limit'),
|
||||
'date_limit' => formipay_get_post_meta($post_id, 'date_limit'),
|
||||
'amounts_fixed' => [],
|
||||
'max_amounts' => [],
|
||||
'forms' => formipay_get_post_meta($post_id, 'forms') ?: [],
|
||||
'products' => formipay_get_post_meta($post_id, 'products') ?: [],
|
||||
'users' => formipay_get_post_meta($post_id, 'users') ?: [],
|
||||
];
|
||||
|
||||
// Get fixed amounts for each currency
|
||||
foreach ($global_currencies as $currency) {
|
||||
$currency_raw = $currency['currency'];
|
||||
$currency_parts = explode(':::', $currency_raw);
|
||||
$currency_code = $currency_parts[0] ?? '';
|
||||
$symbol = formipay_get_currency_data_by_value($currency_raw, 'symbol');
|
||||
|
||||
$amount_fixed = formipay_get_post_meta($post_id, 'amount_fixed_' . $symbol);
|
||||
if ( $amount_fixed ) {
|
||||
$coupon_data['amounts_fixed'][] = [
|
||||
'currency' => $currency_code,
|
||||
'symbol' => $symbol,
|
||||
'amount' => $amount_fixed,
|
||||
];
|
||||
}
|
||||
|
||||
$max_amount = formipay_get_post_meta($post_id, 'max_amount_' . $symbol);
|
||||
if ( $max_amount ) {
|
||||
$coupon_data['max_amounts'][] = [
|
||||
'currency' => $currency_code,
|
||||
'symbol' => $symbol,
|
||||
'amount' => $max_amount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( $coupon_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save coupon data from React editor
|
||||
*/
|
||||
public function formipay_save_coupon() {
|
||||
|
||||
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||
}
|
||||
|
||||
$post_id = isset($_REQUEST['id']) ? intval($_REQUEST['id']) : 0;
|
||||
$title = isset($_REQUEST['title']) ? sanitize_text_field(wp_unslash($_REQUEST['title'])) : '';
|
||||
|
||||
if ( empty($title) ) {
|
||||
wp_send_json_error( [ 'message' => esc_html__( 'Coupon code is required.', 'formipay' ) ] );
|
||||
}
|
||||
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Prepare post data
|
||||
$post_data = [
|
||||
'post_title' => $title,
|
||||
'post_type' => 'formipay-coupon',
|
||||
'post_status' => 'publish',
|
||||
];
|
||||
|
||||
if ( $post_id > 0 ) {
|
||||
$post_data['ID'] = $post_id;
|
||||
$post_data['post_status'] = get_post_status($post_id);
|
||||
$new_post_id = wp_update_post($post_data);
|
||||
} else {
|
||||
$new_post_id = wp_insert_post($post_data);
|
||||
}
|
||||
|
||||
if ( is_wp_error($new_post_id) ) {
|
||||
wp_send_json_error( [ 'message' => $new_post_id->get_error_message() ] );
|
||||
}
|
||||
|
||||
// Save meta fields
|
||||
$meta_fields = [
|
||||
'active',
|
||||
'type',
|
||||
'amount_percentage',
|
||||
'case_sensitive',
|
||||
'free_shipping',
|
||||
'quantity_active',
|
||||
'use_limit',
|
||||
'date_limit',
|
||||
];
|
||||
|
||||
foreach ($meta_fields as $field) {
|
||||
$value = isset($_REQUEST[$field]) ? wp_unslash($_REQUEST[$field]) : '';
|
||||
update_post_meta($new_post_id, $field, $value);
|
||||
}
|
||||
|
||||
// Save fixed amounts
|
||||
foreach ($global_currencies as $currency) {
|
||||
$symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
|
||||
if ( isset($_REQUEST['amount_fixed_' . $symbol]) ) {
|
||||
update_post_meta($new_post_id, 'amount_fixed_' . $symbol, wp_unslash($_REQUEST['amount_fixed_' . $symbol]));
|
||||
}
|
||||
|
||||
if ( isset($_REQUEST['max_amount_' . $symbol]) ) {
|
||||
update_post_meta($new_post_id, 'max_amount_' . $symbol, wp_unslash($_REQUEST['max_amount_' . $symbol]));
|
||||
}
|
||||
}
|
||||
|
||||
// Save relation fields
|
||||
$relation_fields = ['forms', 'products', 'users'];
|
||||
foreach ($relation_fields as $field) {
|
||||
if ( isset($_REQUEST[$field]) ) {
|
||||
$values = is_array($_REQUEST[$field]) ? array_map('intval', $_REQUEST[$field]) : [];
|
||||
update_post_meta($new_post_id, $field, $values);
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
'message' => esc_html__( 'Coupon saved successfully.', 'formipay' ),
|
||||
'id' => $new_post_id,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add React metabox for coupon editor
|
||||
*/
|
||||
public function add_react_metabox() {
|
||||
add_meta_box(
|
||||
'formipay_coupon_settings',
|
||||
__( 'Coupon Settings', 'formipay' ),
|
||||
[$this, 'render_react_metabox'],
|
||||
'formipay-coupon',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render React metabox container
|
||||
*/
|
||||
public function render_react_metabox($post) {
|
||||
?>
|
||||
<div
|
||||
data-formipay-field-renderer="coupon"
|
||||
data-post-id="<?php echo esc_attr($post->ID); ?>"
|
||||
class="formipay-field-renderer-container"
|
||||
>
|
||||
<div class="formipay-loading">
|
||||
<div class="formipay-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render React metabox template (hidden)
|
||||
*/
|
||||
public function render_react_metabox_template() {
|
||||
global $post;
|
||||
|
||||
if (!$post || $post->post_type !== 'formipay-coupon') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add Toaster container for toast notifications
|
||||
echo '<div id="formipay-toaster-container" class="formipay-design-system" style="position:fixed;bottom:20px;right:20px;z-index:99999;"></div>';
|
||||
|
||||
// Get field configuration for this post
|
||||
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
|
||||
|
||||
// Pass config to JavaScript
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
|
||||
</script>
|
||||
<?php
|
||||
|
||||
// Keep the legacy global currencies for backward compatibility during transition
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Fallback: if no multicurrencies configured, use default currency
|
||||
if (empty($global_currencies)) {
|
||||
$formipay_settings = get_option('formipay_settings');
|
||||
$default_currency_raw = formipay_default_currency('raw');
|
||||
|
||||
if (!empty($default_currency_raw)) {
|
||||
$global_currencies = [
|
||||
[
|
||||
'currency' => $default_currency_raw,
|
||||
'decimal_digits' => isset($formipay_settings['default_currency_decimal_digits']) ? intval($formipay_settings['default_currency_decimal_digits']) : 2,
|
||||
'decimal_symbol' => isset($formipay_settings['default_currency_decimal_symbol']) ? $formipay_settings['default_currency_decimal_symbol'] : '.',
|
||||
'thousand_separator' => isset($formipay_settings['default_currency_thousand_separator']) ? $formipay_settings['default_currency_thousand_separator'] : ',',
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
|
||||
window.formipayGetFlag = function(currencyRaw) {
|
||||
<?php
|
||||
foreach ($global_currencies as $currency) {
|
||||
echo 'if (currencyRaw === "' . esc_js($currency['currency']) . '") return "' . esc_url(formipay_get_flag_by_currency($currency['currency'])) . '";';
|
||||
}
|
||||
?>
|
||||
return '';
|
||||
};
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete search for relation fields
|
||||
* Supports CPTs (post_type) and WP Users (object_type=user)
|
||||
*/
|
||||
public function formipay_autocomplete_search() {
|
||||
|
||||
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||
}
|
||||
|
||||
$post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : '';
|
||||
$object_type = isset($_REQUEST['object_type']) ? sanitize_text_field(wp_unslash($_REQUEST['object_type'])) : '';
|
||||
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
|
||||
$include = isset($_REQUEST['include']) ? array_map('intval', (array) $_REQUEST['include']) : [];
|
||||
|
||||
// Handle WP Users
|
||||
if ($object_type === 'user') {
|
||||
// Resolve labels for specific IDs (pre-selected items)
|
||||
if (!empty($include)) {
|
||||
$users = get_users(['include' => $include, 'fields' => ['ID', 'display_name', 'user_email']]);
|
||||
|
||||
$results = [];
|
||||
if (!empty($users)) {
|
||||
foreach ($users as $user) {
|
||||
$results[] = [
|
||||
'value' => $user->ID,
|
||||
'label' => $user->display_name . ' (' . $user->user_email . ')',
|
||||
];
|
||||
}
|
||||
}
|
||||
wp_send_json_success($results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search by keyword
|
||||
if (strlen($search) < 2) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||
}
|
||||
|
||||
$users = get_users([
|
||||
'search' => '*' . $search . '*',
|
||||
'search_columns' => ['display_name', 'user_email', 'user_login'],
|
||||
'number' => 20,
|
||||
'fields' => ['ID', 'display_name', 'user_email'],
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
if (!empty($users)) {
|
||||
foreach ($users as $user) {
|
||||
$results[] = [
|
||||
'value' => $user->ID,
|
||||
'label' => $user->display_name . ' (' . $user->user_email . ')',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success($results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle CPTs (posts)
|
||||
if (empty($post_type)) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||
}
|
||||
|
||||
// Resolve labels for specific IDs (pre-selected items)
|
||||
if (!empty($include)) {
|
||||
$query = get_posts([
|
||||
'post_type' => $post_type,
|
||||
'post__in' => $include,
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'any',
|
||||
]);
|
||||
$results = [];
|
||||
if (!empty($query)) {
|
||||
foreach ($query as $post) {
|
||||
$results[] = [
|
||||
'value' => $post->ID,
|
||||
'label' => $post->post_title,
|
||||
];
|
||||
}
|
||||
}
|
||||
wp_send_json_success($results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search by keyword
|
||||
if (strlen($search) < 2) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||
}
|
||||
|
||||
$query = get_posts([
|
||||
'post_type' => $post_type,
|
||||
's' => $search,
|
||||
'posts_per_page' => 20,
|
||||
'post_status' => ['publish', 'draft'],
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
if (!empty($query)) {
|
||||
foreach ($query as $post) {
|
||||
$results[] = [
|
||||
'value' => $post->ID,
|
||||
'label' => $post->post_title,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save coupon data via WordPress save_post hook
|
||||
* Called when user clicks WordPress Update button
|
||||
*/
|
||||
public function save_coupon_on_post_update($post_id, $post) {
|
||||
// Check if this is an autosave
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Verify nonce
|
||||
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-post_' . $post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Only save for our coupon post type
|
||||
if ($post->post_type !== 'formipay-coupon') {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Get global currencies for currency field processing
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Save basic meta fields
|
||||
$meta_fields = [
|
||||
'active',
|
||||
'type',
|
||||
'amount_percentage',
|
||||
'case_sensitive',
|
||||
'free_shipping',
|
||||
'quantity_active',
|
||||
'use_limit',
|
||||
'date_limit',
|
||||
];
|
||||
|
||||
foreach ($meta_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$value = wp_unslash($_POST[$field]);
|
||||
update_post_meta($post_id, $field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Save fixed amounts for each currency
|
||||
foreach ($global_currencies as $currency) {
|
||||
$symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
|
||||
if (isset($_POST['amount_fixed_' . $symbol])) {
|
||||
update_post_meta($post_id, 'amount_fixed_' . $symbol, wp_unslash($_POST['amount_fixed_' . $symbol]));
|
||||
}
|
||||
|
||||
if (isset($_POST['max_amount_' . $symbol])) {
|
||||
update_post_meta($post_id, 'max_amount_' . $symbol, wp_unslash($_POST['max_amount_' . $symbol]));
|
||||
}
|
||||
}
|
||||
|
||||
// Save relation fields
|
||||
$relation_fields = ['forms', 'products', 'users'];
|
||||
foreach ($relation_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$values = is_array($_POST[$field]) ? array_map('intval', $_POST[$field]) : [];
|
||||
update_post_meta($post_id, $field, $values);
|
||||
} else {
|
||||
delete_post_meta($post_id, $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ class Product {
|
||||
|
||||
add_action( 'wp_ajax_formipay_product_get_currencies', [$this, 'formipay_product_get_currencies'] );
|
||||
add_action( 'wp_ajax_get_product_variables', [$this, 'get_product_variables'] );
|
||||
add_action( 'wp_ajax_get_product_attributes', [$this, 'get_product_attributes'] );
|
||||
|
||||
add_action('save_post', [$this, 'save_product'], 10, 2);
|
||||
|
||||
@@ -85,19 +86,26 @@ class Product {
|
||||
|
||||
public function add_submenu() {
|
||||
|
||||
add_submenu_page(
|
||||
'formipay',
|
||||
__( 'Products', 'formipay' ),
|
||||
__( 'Products', 'formipay' ),
|
||||
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
|
||||
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'save_post', [$this, 'save_product_metabox_fields'], 10, 2 );
|
||||
|
||||
add_submenu_page(
|
||||
'formipay',
|
||||
__('Products', 'formipay'),
|
||||
__('Products', 'formipay'),
|
||||
'manage_options',
|
||||
'formipay-products',
|
||||
[$this, 'formipay_products'],
|
||||
'formipay-products',
|
||||
[$this, 'formipay_product'],
|
||||
2
|
||||
);
|
||||
add_submenu_page(
|
||||
'formipay',
|
||||
__('Categories', 'formipay'),
|
||||
'└ ' . __('Categories', 'formipay'),
|
||||
'manage_options',
|
||||
|
||||
add_submenu_page(
|
||||
'formipay',
|
||||
__('Categories', 'formipay'),
|
||||
'└ ' . __('Categories', 'formipay'),
|
||||
'manage_options',
|
||||
'edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product',
|
||||
null,
|
||||
5
|
||||
@@ -105,6 +113,10 @@ class Product {
|
||||
|
||||
}
|
||||
|
||||
public function formipay_product() {
|
||||
\Formipay\Admin\ReactAdmin::render_mount_point('products');
|
||||
}
|
||||
|
||||
public function enqueue_admin() {
|
||||
// Assets now handled by ReactAdmin class
|
||||
return;
|
||||
@@ -227,9 +239,71 @@ class Product {
|
||||
|
||||
}
|
||||
|
||||
public function formipay_products_react() {
|
||||
|
||||
ReactAdmin::render_mount_point('products');
|
||||
}
|
||||
|
||||
public function add_react_metabox() {
|
||||
add_meta_box(
|
||||
'formipay_product_settings',
|
||||
__('Settings', 'formipay'),
|
||||
[$this, 'render_react_metabox'],
|
||||
'formipay-product',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
public function render_react_metabox($post) {
|
||||
echo '<div data-formipay-field-renderer="product" data-post-id="' . esc_attr($post->ID) . '"></div>';
|
||||
}
|
||||
|
||||
public function render_react_metabox_template() {
|
||||
global $post;
|
||||
|
||||
if (!$post || $post->post_type !== 'formipay-product') {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
|
||||
|
||||
// Get multi-currency settings from formipay_settings option
|
||||
$settings = get_option('formipay_settings', []);
|
||||
$is_multicurrency = !empty($settings['enable_multicurrency']);
|
||||
$multicurrencies = $settings['multicurrencies'] ?? [];
|
||||
|
||||
// Build global_selected_currencies from multicurrencies array
|
||||
$global_selected = [];
|
||||
foreach ($multicurrencies as $currency) {
|
||||
if (isset($currency['currency'])) {
|
||||
$code = explode(':::', $currency['currency'])[0];
|
||||
$global_selected[$code] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same helper functions for consistency
|
||||
$global_currencies = get_global_currency_array();
|
||||
$default_currency = formipay_default_currency();
|
||||
|
||||
$product_details = [
|
||||
'multicurrency' => $is_multicurrency,
|
||||
'default_currency' => $default_currency,
|
||||
'global_currencies' => $global_currencies,
|
||||
'global_selected_currencies' => $global_selected,
|
||||
'currency_flags' => formipay_get_all_currency_flags(),
|
||||
];
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
|
||||
window.formipayProductDetails = <?php echo wp_json_encode($product_details); ?>;
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function formipay_products() {
|
||||
// React admin
|
||||
\Formipay\Admin\ReactAdmin::render_mount_point('products');
|
||||
|
||||
ReactAdmin::render_mount_point('products');
|
||||
}
|
||||
|
||||
public function cpt_post_fields_box($boxes) {
|
||||
@@ -242,13 +316,112 @@ class Product {
|
||||
}
|
||||
|
||||
public function cpt_post_fields_content($fields) {
|
||||
|
||||
|
||||
$fields['formipay_product_settings'] = array();
|
||||
|
||||
|
||||
$fields = apply_filters( 'formipay/product-config', $fields );
|
||||
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function save_product_metabox_fields($post_id, $post) {
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-post_' . $post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
if ($post->post_type !== 'formipay-product') {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
$meta_fields = [
|
||||
'product_type',
|
||||
'setting_product_price_regular',
|
||||
'setting_product_price_sale',
|
||||
'free_shipping',
|
||||
// Shipping fields
|
||||
'shipping_method',
|
||||
'flat_rate_type',
|
||||
'flat_rate_amount',
|
||||
'flat_rate_label',
|
||||
'free_shipping_label',
|
||||
'free_shipping_add_to_order_review',
|
||||
// Shipping dimensions (for carrier API calculation)
|
||||
'product_weight',
|
||||
'product_length',
|
||||
'product_width',
|
||||
'product_height',
|
||||
// Access and status
|
||||
'product_accesses',
|
||||
'product_access_to_email',
|
||||
'active',
|
||||
];
|
||||
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
foreach ($meta_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$value = wp_unslash($_POST[$field]);
|
||||
update_post_meta($post_id, $field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($global_currencies as $currency) {
|
||||
$symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
|
||||
if (isset($_POST['setting_product_price_regular_' . $symbol])) {
|
||||
update_post_meta($post_id, 'setting_product_price_regular_' . $symbol, wp_unslash($_POST['setting_product_price_regular_' . $symbol]));
|
||||
}
|
||||
|
||||
if (isset($_POST['max_amount_' . $symbol])) {
|
||||
update_post_meta($post_id, 'max_amount_' . $symbol, wp_unslash($_POST['max_amount_' . $symbol]));
|
||||
}
|
||||
|
||||
// Save flat rate amounts per currency
|
||||
if (isset($_POST['flat_rate_amount_' . $symbol])) {
|
||||
update_post_meta($post_id, 'flat_rate_amount_' . $symbol, wp_unslash($_POST['flat_rate_amount_' . $symbol]));
|
||||
}
|
||||
}
|
||||
|
||||
// Save product variations (JSON from VariationField)
|
||||
if (isset($_POST['product_variations'])) {
|
||||
$variations_json = wp_unslash($_POST['product_variations']);
|
||||
$variations_data = json_decode($variations_json, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($variations_data)) {
|
||||
update_post_meta($post_id, 'product_variations', $variations_json);
|
||||
|
||||
// Also save the legacy format for backward compatibility
|
||||
if (isset($variations_data['variations']) && is_array($variations_data['variations'])) {
|
||||
$legacy_variations = $variations_data['variations'];
|
||||
update_post_meta($post_id, 'product_variables', wp_json_encode($legacy_variations));
|
||||
}
|
||||
|
||||
// Save attributes separately for legacy compatibility
|
||||
if (isset($variations_data['attributes']) && is_array($variations_data['attributes'])) {
|
||||
$legacy_attributes = [];
|
||||
foreach ($variations_data['attributes'] as $attr) {
|
||||
$legacy_attributes[] = [
|
||||
'attribute_name' => $attr['attribute_name'] ?? '',
|
||||
'attribute_type' => $attr['attribute_type'] ?? 'select',
|
||||
'attribute_variations' => $attr['attribute_variations'] ?? [],
|
||||
];
|
||||
}
|
||||
update_post_meta($post_id, 'product_variation_attributes', wp_json_encode($legacy_attributes));
|
||||
}
|
||||
} else {
|
||||
delete_post_meta($post_id, 'product_variations');
|
||||
}
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function general_config($fields) {
|
||||
@@ -434,7 +607,68 @@ class Product {
|
||||
|
||||
$product_currency_group = apply_filters( 'formipay/product-settings/tab:general/group:product-currency', $product_currency_group );
|
||||
|
||||
$general_all_fields = array_merge($product_details_group, $product_currency_group);
|
||||
// Shipping Dimensions Group (for physical products only)
|
||||
$shipping_dimensions_group = array(
|
||||
'setting_product_shipping_dimensions' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Shipping Dimensions', 'formipay' ),
|
||||
'description' => __( 'Weight and dimensions for carrier shipping calculation.', 'formipay' ),
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical'
|
||||
),
|
||||
'group' => 'started'
|
||||
),
|
||||
'product_weight' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Weight (kg)', 'formipay' ),
|
||||
'step' => 0.01,
|
||||
'min' => 0,
|
||||
'placeholder' => '0.00',
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical'
|
||||
)
|
||||
),
|
||||
'product_length' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Length (cm)', 'formipay' ),
|
||||
'step' => 0.1,
|
||||
'min' => 0,
|
||||
'placeholder' => '0.0',
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical'
|
||||
)
|
||||
),
|
||||
'product_width' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Width (cm)', 'formipay' ),
|
||||
'step' => 0.1,
|
||||
'min' => 0,
|
||||
'placeholder' => '0.0',
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical'
|
||||
)
|
||||
),
|
||||
'product_height' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Height (cm)', 'formipay' ),
|
||||
'step' => 0.1,
|
||||
'min' => 0,
|
||||
'placeholder' => '0.0',
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical'
|
||||
),
|
||||
'group' => 'ended'
|
||||
),
|
||||
);
|
||||
|
||||
$shipping_dimensions_group = apply_filters( 'formipay/product-settings/tab:general/group:shipping-dimensions', $shipping_dimensions_group );
|
||||
|
||||
$general_all_fields = array_merge($product_details_group, $product_currency_group, $shipping_dimensions_group);
|
||||
|
||||
$general_all_fields = apply_filters( 'formipay/product-settings/tab:general', $general_all_fields );
|
||||
|
||||
@@ -448,77 +682,28 @@ class Product {
|
||||
}
|
||||
|
||||
public function variations_config($fields) {
|
||||
// Product Variations Attribute Group
|
||||
$product_attributes_group = array(
|
||||
// Product Variations - Unified React field with attributes and variations table
|
||||
$product_variations_group = array(
|
||||
'setting_product_variations' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Attributes', 'formipay' ),
|
||||
'description' => __( 'First we need to build the attribute of this product. For example Color, Size, License Site Count.', 'formipay' ),
|
||||
'label' => __( 'Product Variations', 'formipay' ),
|
||||
'description' => __( 'Configure attributes and their combinations to generate product variations with multi-currency pricing.', 'formipay' ),
|
||||
'group' => 'started'
|
||||
),
|
||||
'product_has_variation' => array(
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Product has variations', 'formipay' ),
|
||||
),
|
||||
'product_variation_attributes' => array(
|
||||
'type' => 'repeater',
|
||||
'label' => __('Attributes', 'formipay'),
|
||||
'description' => __( 'Your attributes will generate variation automatically.', 'formipay' ),
|
||||
'fields' => [
|
||||
'attribute_name' => [
|
||||
'type' => 'text',
|
||||
'label' => __( 'Attribute Name', 'formipay' ),
|
||||
'description' => __( 'e.g. Color, Size, etc', 'formipay' ),
|
||||
'is_group_title' => true
|
||||
],
|
||||
'attribute_variations' => [
|
||||
'type' => 'repeater',
|
||||
'label' => esc_html__( 'Variation', 'formipay' ),
|
||||
'fields' => [
|
||||
'variation_label' => [
|
||||
'type' => 'text',
|
||||
'label' => __( 'Title', 'formipay' ),
|
||||
'description' => __( 'e.g. Red, XL, etc', 'formipay' ),
|
||||
'required' => true,
|
||||
'is_group_title' => true
|
||||
],
|
||||
'variation_value' => [
|
||||
'type' => 'text',
|
||||
'label' => __( 'Value', 'formipay' ),
|
||||
'description' => __( 'e.g. red, xl, etc', 'formipay' ),
|
||||
'required' => true
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
'dependency' => array(
|
||||
'key' => 'product_has_variation',
|
||||
'value' => 'not_empty'
|
||||
),
|
||||
'product_variations' => array(
|
||||
'type' => 'variation',
|
||||
'label' => __( 'Variations', 'formipay' ),
|
||||
'description' => __( 'Add attributes (e.g., Color, Size) and their values. Variations will be generated automatically from all combinations.', 'formipay' ),
|
||||
'name' => 'product_variations',
|
||||
),
|
||||
);
|
||||
|
||||
$product_attributes_group = apply_filters( 'formipay/product-settings/tab:general/group:product-attributes', $product_attributes_group );
|
||||
$product_variations_group = apply_filters( 'formipay/product-settings/tab:variations/group:product-variations', $product_variations_group );
|
||||
|
||||
$last_product_attributes_group = array_key_last($product_attributes_group);
|
||||
$product_attributes_group[$last_product_attributes_group]['group'] = 'ended';
|
||||
$last_product_variations_group = array_key_last($product_variations_group);
|
||||
$product_variations_group[$last_product_variations_group]['group'] = 'ended';
|
||||
|
||||
// Product Variations Attribute Group
|
||||
|
||||
// Define your product variations field group somewhere in your plugin/theme
|
||||
$product_variations_table_html = file_get_contents(FORMIPAY_PATH . 'admin/templates/product-variations.php');
|
||||
|
||||
$product_variations_group = [
|
||||
'variation_table' => [
|
||||
'type' => 'html',
|
||||
'label' => __( 'Variations', 'formipay' ),
|
||||
'html' => $product_variations_table_html
|
||||
],
|
||||
];
|
||||
|
||||
$variation_all_fields = array_merge($product_attributes_group, $product_variations_group);
|
||||
|
||||
$variation_all_fields = apply_filters( 'formipay/product-settings/tab:variations', $variation_all_fields );
|
||||
$variation_all_fields = apply_filters( 'formipay/product-settings/tab:variations', $product_variations_group );
|
||||
|
||||
$fields['formipay_product_settings']['variation'] = array(
|
||||
'name' => __('Variations', 'formipay'),
|
||||
@@ -959,6 +1144,25 @@ class Product {
|
||||
wp_send_json_error();
|
||||
}
|
||||
|
||||
public function get_product_attributes() {
|
||||
$post_id = intval($_POST['post_id'] ?? 0);
|
||||
|
||||
// Check permissions
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
wp_send_json_error(['message' => 'Unauthorized']);
|
||||
}
|
||||
|
||||
// Get attributes from legacy meta key
|
||||
$data = get_post_meta($post_id, 'product_variation_attributes', true);
|
||||
$json = is_string($data) ? json_decode($data, true) : $data;
|
||||
|
||||
if (is_array($json)) {
|
||||
wp_send_json_success($json);
|
||||
}
|
||||
|
||||
wp_send_json_success([]);
|
||||
}
|
||||
|
||||
public function save_product_depracated($post_id, $post) {
|
||||
// Verify nonce and permissions here if you have a nonce field (recommended)
|
||||
|
||||
|
||||
@@ -653,6 +653,7 @@ class Render {
|
||||
wp_enqueue_script( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.js', [], FORMIPAY_VERSION, true );
|
||||
wp_enqueue_script( 'formipay-popup', FORMIPAY_URL . 'public/assets/js/popup-action.js', ['jquery', 'choices'], FORMIPAY_VERSION, true);
|
||||
wp_enqueue_script( 'formipay-form', FORMIPAY_URL . 'public/assets/js/form-action.js', ['jquery', 'choices'], FORMIPAY_VERSION, true);
|
||||
wp_enqueue_script( 'formipay-checkout-shipping', FORMIPAY_URL . 'public/assets/js/checkout-shipping.js', ['jquery', 'formipay-form'], FORMIPAY_VERSION, true);
|
||||
// Localize data for all forms
|
||||
$form_data = [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
@@ -661,6 +662,16 @@ class Render {
|
||||
'forms' => $this->get_form_data()
|
||||
];
|
||||
wp_localize_script('formipay-form', 'formipay_form', $form_data);
|
||||
|
||||
// Localize shipping labels for checkout
|
||||
$shipping_data = [
|
||||
'labels' => [
|
||||
'country' => __('Shipping Country', 'formipay'),
|
||||
'selectCountry' => __('Select your country', 'formipay'),
|
||||
'shippingMethod' => __('Shipping Method', 'formipay'),
|
||||
]
|
||||
];
|
||||
wp_localize_script('formipay-checkout-shipping', 'formipay_shipping', $shipping_data);
|
||||
|
||||
}
|
||||
|
||||
@@ -766,6 +777,11 @@ class Render {
|
||||
$allowed_currency_pack = $this->resolve_allowed_currencies($post_id);
|
||||
$currency_code = $allowed_currency_pack['default_code'];
|
||||
$currency_cfg = $this->resolve_currency_config($currency_code);
|
||||
|
||||
// Get form shipping settings
|
||||
$form_settings = get_post_meta($post_id, 'formipay_form_settings', true);
|
||||
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
|
||||
|
||||
$form_data[$post_id] = [
|
||||
'form_id' => $post_id,
|
||||
'currency' => formipay_post_currency($post_id),
|
||||
@@ -789,13 +805,14 @@ class Render {
|
||||
'button_text' => formipay_get_post_meta($post_id, 'button_text'),
|
||||
'button_processing_text' => formipay_get_post_meta($post_id, 'button_processing_text'),
|
||||
'isPopup' => formipay_isPopup($post_id),
|
||||
'trigger_selector' => formipay_get_post_meta($post_id, 'popup_click_selector') ?
|
||||
formipay_get_post_meta($post_id, 'popup_trigger_selector') :
|
||||
'trigger_selector' => formipay_get_post_meta($post_id, 'popup_click_selector') ?
|
||||
formipay_get_post_meta($post_id, 'popup_trigger_selector') :
|
||||
'.formipay-open-popup-button',
|
||||
'modal_selector' => '#formipay-popup-' . $post_id,
|
||||
'static_products' => array_filter(array_map('absint', explode(',', (string) formipay_get_post_meta($post_id, 'static_products')))),
|
||||
'static_items' => json_decode((string) formipay_get_post_meta($post_id, 'static_items'), true) ?: [],
|
||||
'currency_code' => (function($c){ $p = explode(':::', (string)$c); return $p[0] ?? 'IDR'; })(formipay_post_currency($post_id)),
|
||||
'shipping_enabled' => $shipping_enabled, // Form-level shipping setting
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Formipay;
|
||||
use Formipay\Traits\SingletonTrait;
|
||||
use Formipay\Admin\FieldConfigBridge;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
@@ -11,16 +12,39 @@ class Settings {
|
||||
/**
|
||||
* Initializes the plugin by setting filters and administration functions.
|
||||
*/
|
||||
|
||||
|
||||
protected function __construct() {
|
||||
|
||||
add_filter( 'wpcfto_options_page_setup', [$this, 'theme_option'] );
|
||||
// Register our submenu page
|
||||
add_action( 'admin_menu', [$this, 'add_settings_page'] );
|
||||
|
||||
add_action( 'admin_enqueue_scripts', [$this, 'enqueue'] );
|
||||
add_action( 'admin_footer', [$this, 'render_react_settings_template'] );
|
||||
|
||||
// AJAX handler for saving settings from React
|
||||
add_action( 'wp_ajax_formipay_save_settings', [$this, 'ajax_save_settings'] );
|
||||
|
||||
}
|
||||
|
||||
public function theme_option($setups){
|
||||
/**
|
||||
* Add settings submenu page
|
||||
*/
|
||||
public function add_settings_page() {
|
||||
add_submenu_page(
|
||||
'formipay', // Parent slug
|
||||
__('Formipay Settings', 'formipay'), // Page title
|
||||
__('Settings', 'formipay'), // Menu title
|
||||
'manage_options', // Capability
|
||||
'formipay-settings', // Menu slug
|
||||
[$this, 'render_settings_page'] // Callback function
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings field configuration for React
|
||||
* Direct method without WPCFTO dependency
|
||||
*/
|
||||
public function get_settings_fields() {
|
||||
$gateways = apply_filters( 'formipay/form-config/tab:payments/gateways', [] );
|
||||
|
||||
$payment_checkboxes = [];
|
||||
@@ -253,7 +277,7 @@ class Settings {
|
||||
'value' => __( 'Input your {{media}} to get new access link.', 'formipay' ),
|
||||
'submenu' => __( 'Thank-You Page', 'formipay' ),
|
||||
'hints' => array(
|
||||
'media' => __( 'Contact Media', 'formipay' )
|
||||
'media' => __( 'Contact Media', 'formipay' )
|
||||
),
|
||||
'description' => __( 'Use {{media}} shortcode to define what media of contact the buyer can receive the access link.', 'formipay' )
|
||||
),
|
||||
@@ -268,59 +292,115 @@ class Settings {
|
||||
|
||||
$pages_fields = apply_filters( 'formipay/global-settings/tab:pages', $pages_fields );
|
||||
|
||||
$global = array(
|
||||
'General' => array(
|
||||
$tabs_config = [
|
||||
'General' => [
|
||||
'name' => __( 'General', 'formipay' ),
|
||||
'fields' => $general_fields
|
||||
),
|
||||
'Pages' => array(
|
||||
],
|
||||
'Pages' => [
|
||||
'name' => __( 'Pages', 'formipay' ),
|
||||
'fields' => $pages_fields
|
||||
)
|
||||
);
|
||||
]
|
||||
];
|
||||
|
||||
$global = apply_filters( 'formipay/global-settings', $global );
|
||||
// Allow other modules to add/modify tabs
|
||||
$tabs_config = apply_filters( 'formipay/global-settings', $tabs_config );
|
||||
|
||||
foreach($global as $key => $value){
|
||||
$fields[$key] = $value;
|
||||
}
|
||||
return $tabs_config;
|
||||
}
|
||||
|
||||
$setups[] = array(
|
||||
'option_name' => 'formipay_settings',
|
||||
'title' => __('Formipay', 'formipay'),
|
||||
'sub_title' => __('Settings', 'formipay'),
|
||||
'logo' => FORMIPAY_URL . 'admin/assets/img/formipay-logo-circle-white.png',
|
||||
'page' => array(
|
||||
'parent_slug' => 'formipay',
|
||||
'page_title' => __('Formipay Settings', 'formipay'),
|
||||
'menu_title' => __('Settings', 'formipay'),
|
||||
'menu_slug' => 'formipay-settings',
|
||||
'position' => 40,
|
||||
),
|
||||
|
||||
'fields' => $fields
|
||||
);
|
||||
|
||||
return $setups;
|
||||
/**
|
||||
* Render settings page (empty container for React)
|
||||
*/
|
||||
public function render_settings_page() {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<div id="formipay-settings-page-container"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function enqueue() {
|
||||
|
||||
|
||||
global $current_screen;
|
||||
|
||||
if ( $current_screen->id === 'formipay_page_formipay-settings' ) {
|
||||
wp_enqueue_style('admin-setting-style', FORMIPAY_URL . 'admin/assets/css/global-setting.css', [], FORMIPAY_VERSION, 'all' );
|
||||
wp_enqueue_script('admin-setting-script', FORMIPAY_URL . 'admin/assets/js/admin-setting.js', ['jquery'], FORMIPAY_VERSION, true);
|
||||
// Enqueue React admin assets for FieldRenderer
|
||||
wp_enqueue_script( 'formipay-admin', FORMIPAY_URL . 'build/admin.js', ['react', 'react-dom'], FORMIPAY_VERSION, true );
|
||||
wp_enqueue_style( 'formipay-admin', FORMIPAY_URL . 'build/admin.css', [], FORMIPAY_VERSION, 'all' );
|
||||
}
|
||||
}
|
||||
|
||||
wp_localize_script( 'admin-setting-script', 'formipay_admin_setting', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'site_url' => site_url(),
|
||||
'nonce' => wp_create_nonce('formipay-admin-nonce'),
|
||||
'multicurrency' => formipay_is_multi_currency_active(),
|
||||
'all_currencies' => formipay_currency_as_options(),
|
||||
'global_selected_currencies' => formipay_global_currency_options(),
|
||||
'default_currency' => formipay_default_currency()
|
||||
] );
|
||||
/**
|
||||
* Render React settings template in admin footer
|
||||
* This outputs the config JSON and mount point for the React FieldRenderer
|
||||
*/
|
||||
public function render_react_settings_template() {
|
||||
global $current_screen;
|
||||
|
||||
// Only render on settings page
|
||||
if ( $current_screen->id !== 'formipay_page_formipay-settings' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the configuration for settings
|
||||
$config = FieldConfigBridge::get_config_for_settings('formipay_settings');
|
||||
$config_json = wp_json_encode($config);
|
||||
|
||||
?>
|
||||
<div id="formipay-settings-react" data-formipay-settings-config="<?php echo esc_attr($config_json); ?>"></div>
|
||||
<script>
|
||||
// Move the React mount point to our page container
|
||||
jQuery(document).ready(function($) {
|
||||
$('#formipay-settings-react').appendTo('#formipay-settings-page-container');
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for saving settings from React
|
||||
*/
|
||||
public function ajax_save_settings() {
|
||||
// Verify nonce
|
||||
check_ajax_referer( 'formipay-field-config', 'nonce', true );
|
||||
|
||||
// Check permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error([
|
||||
'message' => __( 'You do not have permission to save settings.', 'formipay' )
|
||||
]);
|
||||
}
|
||||
|
||||
// Get settings data
|
||||
$settings = isset($_POST['settings']) ? json_decode(sanitize_text_field(wp_unslash($_POST['settings'])), true) : [];
|
||||
|
||||
if (empty($settings)) {
|
||||
wp_send_json_error([
|
||||
'message' => __( 'No settings data received.', 'formipay' )
|
||||
]);
|
||||
}
|
||||
|
||||
// Remove nonce from settings before saving
|
||||
unset($settings['nonce']);
|
||||
|
||||
// Get existing settings
|
||||
$existing_settings = get_option('formipay_settings', []);
|
||||
|
||||
// Merge with existing settings to preserve values not in current form
|
||||
$updated_settings = array_merge($existing_settings, $settings);
|
||||
|
||||
// Update option
|
||||
$result = update_option('formipay_settings', $updated_settings);
|
||||
|
||||
if ($result) {
|
||||
wp_send_json_success([
|
||||
'message' => __( 'Settings saved successfully.', 'formipay' )
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => __( 'Failed to save settings.', 'formipay' )
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ class FlatRate extends Shipping {
|
||||
|
||||
parent::__construct();
|
||||
|
||||
add_filter( 'formipay/product-config/tab:shipping/method', [$this, 'add_shipping_method'], 15 );
|
||||
add_filter( 'formipay/product-config/tab:shipping', [$this, 'add_shipping_settings'], 15 );
|
||||
// Register flat rate as a form-level shipping method
|
||||
add_filter( 'formipay/form-settings/tab:shipping/method', [$this, 'add_shipping_method'], 15 );
|
||||
add_filter( 'formipay/form-settings/tab:shipping', [$this, 'add_shipping_settings'], 15 );
|
||||
|
||||
// Add to order details
|
||||
add_filter( 'formipay/order/order-details', [$this, 'add_shipping_to_order_details'], 99, 3 );
|
||||
@@ -33,113 +34,133 @@ class FlatRate extends Shipping {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add flat rate settings to form shipping configuration
|
||||
* These fields are shown when "Flat Rate" is selected as the shipping method
|
||||
*/
|
||||
public function add_shipping_settings($fields) {
|
||||
|
||||
// Get global currencies configuration
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Basic flat rate fields (type and label)
|
||||
$flat_rate_fields = array(
|
||||
$this->shipping_method.'_group' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Flat Rate Setup', 'formipay' ),
|
||||
'description' => __( 'Configure flat rate shipping cost for this form', 'formipay' ),
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'flat_rate'
|
||||
)
|
||||
'key' => 'shipping_enabled',
|
||||
'value' => 'flat_rate'
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
'group' => 'started'
|
||||
),
|
||||
$this->shipping_method.'_type' => array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Type', 'formipay' ),
|
||||
'options' => array(
|
||||
'fixed' => __( 'Fixed', 'formipay' ),
|
||||
'percentage' => __( 'Percentage', 'formipay' )
|
||||
'fixed' => __( 'Fixed Amount', 'formipay' ),
|
||||
'percentage' => __( 'Percentage of Order Total', 'formipay' )
|
||||
),
|
||||
'value' => 'fixed',
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'flat_rate'
|
||||
)
|
||||
'key' => 'shipping_enabled',
|
||||
'value' => 'flat_rate'
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
),
|
||||
$this->shipping_method.'_amount' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Amount', 'formipay' ),
|
||||
'value' => '10',
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'flat_rate'
|
||||
)
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
),
|
||||
$this->shipping_method.'_label' => array(
|
||||
'type' => 'text',
|
||||
'label' => __( 'Label', 'formipay' ),
|
||||
'description' => __( 'This will be shown in Order Review and Order Details', 'formipay' ),
|
||||
'value' => __( 'Shipping Fee', 'formipay' ),
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'flat_rate'
|
||||
)
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
'group' => 'ended'
|
||||
),
|
||||
);
|
||||
|
||||
// Add per-currency amount fields
|
||||
foreach ($global_currencies as $currency) {
|
||||
// Get the currency code (first part of triple) - this is used for meta key suffix
|
||||
$currency_code = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
|
||||
$step = ($currency['decimal_digits'] ?? 2) > 0 ? pow(10, -($currency['decimal_digits'] ?? 2)) : 1;
|
||||
$is_last = ($currency === end($global_currencies));
|
||||
|
||||
$flat_rate_fields[$this->shipping_method.'_amount_'.$currency_code] = array(
|
||||
'type' => 'number',
|
||||
'label' => sprintf(__( 'Amount (%s)', 'formipay' ), $currency_code),
|
||||
'description' => $is_last ? __( 'Shipping cost for this form (not per-product)', 'formipay' ) : '',
|
||||
'step' => $step,
|
||||
'min' => 0,
|
||||
'placeholder' => $is_last ? __( 'Enter Amount...', 'formipay' ) : __( 'Auto', 'formipay' ),
|
||||
'dependency' => array(
|
||||
'key' => 'shipping_enabled',
|
||||
'value' => 'flat_rate'
|
||||
),
|
||||
'group' => $is_last ? 'ended' : null,
|
||||
);
|
||||
}
|
||||
|
||||
// Merge fields into the main fields array
|
||||
foreach($flat_rate_fields as $key => $value){
|
||||
$fields[$key] = $value;
|
||||
}
|
||||
|
||||
|
||||
return $fields;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add shipping cost to order details
|
||||
*
|
||||
* @param array $details Order details array
|
||||
* @param int $form_id Product/form ID
|
||||
* @param array $order_data Order data from submission
|
||||
* @return array Updated order details
|
||||
*/
|
||||
public function add_shipping_to_order_details( $details, $form_id, $order_data ) {
|
||||
|
||||
if( formipay_get_post_meta($form_id, 'product_type') == 'physical' && formipay_get_post_meta($form_id, 'shipping_method')){
|
||||
if ( formipay_get_post_meta($form_id, 'product_type') == 'physical' && formipay_get_post_meta($form_id, 'shipping_method') == 'flat_rate' ) {
|
||||
|
||||
$amount = floatval( formipay_price_format( formipay_get_post_meta( $form_id, 'flat_rate_amount' ) ) );
|
||||
$flat_rate_type = formipay_get_post_meta($form_id, 'flat_rate_type');
|
||||
$flat_rate_label = formipay_get_post_meta($form_id, 'flat_rate_label');
|
||||
|
||||
if( formipay_get_post_meta($form_id, 'flat_rate_type') == 'percentage' ) {
|
||||
$price = floatval( formipay_get_post_meta($form_id, 'product_price') );
|
||||
$calculate = $price * $amount / 100;
|
||||
// Get the selected currency from request (same way Order class does it)
|
||||
$currency = isset($_REQUEST['currency']) ? sanitize_text_field( wp_unslash($_REQUEST['currency']) ) : (string) formipay_default_currency('code');
|
||||
|
||||
// Get flat rate amount - check for currency-specific first, then fallback to base
|
||||
$flat_rate_amount = formipay_get_post_meta($form_id, 'flat_rate_amount_' . $currency);
|
||||
if (empty($flat_rate_amount)) {
|
||||
$flat_rate_amount = formipay_get_post_meta($form_id, 'flat_rate_amount');
|
||||
}
|
||||
|
||||
$amount = floatval( formipay_price_format($flat_rate_amount) );
|
||||
|
||||
// For percentage-based, calculate from actual product price paid
|
||||
if ( $flat_rate_type == 'percentage' ) {
|
||||
// Find the actual product price from order details (already currency-aware)
|
||||
$product_price = 0;
|
||||
foreach ($details as $item) {
|
||||
if (isset($item['context']) && $item['context'] == 'product') {
|
||||
// Use the first product's amount (already in selected currency)
|
||||
$product_price = floatval($item['amount']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no product found in details, fallback to lookup by currency
|
||||
if ($product_price == 0) {
|
||||
$regular_key = 'setting_product_price_regular_' . $currency;
|
||||
$sale_key = 'setting_product_price_sale_' . $currency;
|
||||
$regular_price = formipay_get_post_meta($form_id, $regular_key);
|
||||
$sale_price = formipay_get_post_meta($form_id, $sale_key);
|
||||
$product_price = ($sale_price !== '' && $sale_price !== null) ? floatval($sale_price) : floatval($regular_price);
|
||||
}
|
||||
|
||||
$calculate = $product_price * $amount / 100;
|
||||
$amount = floatval($calculate);
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'item' => formipay_get_post_meta($form_id, 'flat_rate_label'),
|
||||
'item' => $flat_rate_label,
|
||||
'amount' => $amount,
|
||||
'subtotal' => $amount
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
return $details;
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,15 @@ use Formipay\Traits\SingletonTrait;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||
|
||||
/**
|
||||
* Abstract Shipping Class - Core Carrier Extension System
|
||||
*
|
||||
* This class provides the hook system for carrier API extensions.
|
||||
* Carriers like Rajaongkir, Biteship, etc. can extend Formipay shipping
|
||||
* by registering themselves via the provided hooks.
|
||||
*
|
||||
* @phase 4 - Carrier API Extension System
|
||||
*/
|
||||
abstract class Shipping {
|
||||
|
||||
use SingletonTrait;
|
||||
@@ -14,8 +23,42 @@ abstract class Shipping {
|
||||
|
||||
protected function __construct() {
|
||||
|
||||
add_filter( 'formipay/global-settings', [$this, 'add_setting_shipping_menu'], 15 );
|
||||
add_filter( 'formipay/product-config', [$this, 'add_form_shipping_menu'], 75 );
|
||||
// Phase 1-3: Core shipping functionality
|
||||
add_filter( 'formipay/global-settings', [$this, 'add_setting_shipping_menu'], 15 );
|
||||
add_filter( 'formipay/form-config', [$this, 'add_form_shipping_config'], 75 );
|
||||
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_global_shipping_settings'], 15 );
|
||||
|
||||
// Phase 4: Carrier Extension Hooks
|
||||
// Carrier registration - allows carriers to register themselves
|
||||
add_filter( 'formipay/shipping/carriers', '__return_empty_array', 5 );
|
||||
|
||||
// Carrier API keys - inject into global shipping settings
|
||||
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_carrier_api_settings'], 20 );
|
||||
|
||||
// Checkout address fields - inject carrier-specific address fields
|
||||
add_filter( 'formipay/checkout/shipping-address-fields', [$this, 'get_carrier_address_fields'], 10, 3 );
|
||||
|
||||
// Live rate fetching - allow carriers to provide real-time rates
|
||||
add_filter( 'formipay/shipping/live-rates', '__return_empty_array', 10, 4 );
|
||||
|
||||
// AJAX handler for testing carrier connection
|
||||
add_action( 'wp_ajax_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
|
||||
add_action( 'wp_ajax_nopriv_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
|
||||
|
||||
// Phase 5: Checkout Integration
|
||||
// AJAX endpoint for getting available shipping methods for checkout
|
||||
add_action( 'wp_ajax_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
|
||||
add_action( 'wp_ajax_nopriv_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
|
||||
|
||||
// AJAX endpoint for getting supported countries
|
||||
add_action( 'wp_ajax_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
|
||||
add_action( 'wp_ajax_nopriv_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
|
||||
|
||||
// Hook to add shipping cost to cart calculation
|
||||
add_filter( 'formipay/checkout/cart/calculation', [$this, 'add_shipping_to_cart'], 10, 3 );
|
||||
|
||||
// Hook to add shipping data to order submission
|
||||
add_filter( 'formipay/order/process-data', [$this, 'add_shipping_to_order_data'], 10, 2 );
|
||||
|
||||
}
|
||||
|
||||
@@ -36,19 +79,212 @@ abstract class Shipping {
|
||||
|
||||
}
|
||||
|
||||
public function add_form_shipping_menu($fields) {
|
||||
/**
|
||||
* Add global shipping settings fields
|
||||
* This implements Phase 3 of the shipping module: Global Shipping Settings
|
||||
*/
|
||||
public function add_global_shipping_settings($fields) {
|
||||
|
||||
$shipping_methods = apply_filters( 'formipay/product-settings/tab:shipping/method', [
|
||||
// Load countries from JSON file
|
||||
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
|
||||
$countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
|
||||
|
||||
$country_options = [];
|
||||
if (is_array($countries)) {
|
||||
foreach ($countries as $country) {
|
||||
$code = $country['code'] ?? '';
|
||||
$name = $country['name'] ?? '';
|
||||
if ($code && $name) {
|
||||
$country_options[$code] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store Origin Section
|
||||
$fields['shipping_origin_group'] = array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Store Origin', 'formipay' ),
|
||||
'description' => __( 'Your business location for shipping calculations', 'formipay' ),
|
||||
'group' => 'started'
|
||||
);
|
||||
|
||||
$fields['shipping_origin_country'] = array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Origin Country', 'formipay' ),
|
||||
'options' => $country_options,
|
||||
'searchable' => true,
|
||||
'description' => __( 'Select the country where your products ship from', 'formipay' ),
|
||||
);
|
||||
|
||||
$fields['shipping_weight_unit'] = array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Weight Unit', 'formipay' ),
|
||||
'options' => array(
|
||||
'kg' => __( 'Kilograms (kg)', 'formipay' ),
|
||||
'g' => __( 'Grams (g)', 'formipay' ),
|
||||
'lb' => __( 'Pounds (lb)', 'formipay' ),
|
||||
'oz' => __( 'Ounces (oz)', 'formipay' ),
|
||||
),
|
||||
'value' => 'kg',
|
||||
);
|
||||
|
||||
$fields['shipping_dimension_unit'] = array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Dimension Unit', 'formipay' ),
|
||||
'options' => array(
|
||||
'cm' => __( 'Centimeters (cm)', 'formipay' ),
|
||||
'in' => __( 'Inches (in)', 'formipay' ),
|
||||
'm' => __( 'Meters (m)', 'formipay' ),
|
||||
),
|
||||
'value' => 'cm',
|
||||
'group' => 'ended'
|
||||
);
|
||||
|
||||
// Shipping Calculation Method
|
||||
$fields['shipping_calculation_group'] = array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Shipping Calculation', 'formipay' ),
|
||||
'description' => __( 'How shipping costs are calculated for orders', 'formipay' ),
|
||||
'group' => 'started'
|
||||
);
|
||||
|
||||
$fields['shipping_calculation_method'] = array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Calculation Method', 'formipay' ),
|
||||
'options' => array(
|
||||
'per_order' => __( 'Per Order (single shipping fee for entire order)', 'formipay' ),
|
||||
'per_item' => __( 'Per Item (shipping fee multiplied by quantity)', 'formipay' ),
|
||||
),
|
||||
'value' => 'per_order',
|
||||
'group' => 'ended'
|
||||
);
|
||||
|
||||
// Supported Destinations Section
|
||||
$fields['shipping_destinations_group'] = array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Supported Destinations', 'formipay' ),
|
||||
'description' => __( 'Configure which countries you ship to and their shipping rates', 'formipay' ),
|
||||
'group' => 'started'
|
||||
);
|
||||
|
||||
// Get enabled currencies for flat rate table
|
||||
$formipay_settings = get_option('formipay_settings', []);
|
||||
$enabled_currencies = [];
|
||||
|
||||
if (!empty($formipay_settings['multicurrencies']) && is_array($formipay_settings['multicurrencies'])) {
|
||||
foreach ($formipay_settings['multicurrencies'] as $currency) {
|
||||
if (isset($currency['currency'])) {
|
||||
$parts = explode(':::', $currency['currency']);
|
||||
$enabled_currencies[] = [
|
||||
'code' => $parts[0] ?? '',
|
||||
'title' => $parts[1] ?? '',
|
||||
'symbol' => $parts[2] ?? $parts[0] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default currency if multicurrency is not enabled
|
||||
if (empty($enabled_currencies)) {
|
||||
$default_currency = $formipay_settings['default_currency'] ?? 'IDR:::Indonesian rupiah:::Rp';
|
||||
$parts = explode(':::', $default_currency);
|
||||
$enabled_currencies[] = [
|
||||
'code' => $parts[0] ?? 'IDR',
|
||||
'title' => $parts[1] ?? 'Indonesian rupiah',
|
||||
'symbol' => $parts[2] ?? 'Rp',
|
||||
];
|
||||
}
|
||||
|
||||
// Build currency amount fields for the repeater
|
||||
$currency_fields = [];
|
||||
foreach ($enabled_currencies as $curr) {
|
||||
$code = $curr['code'];
|
||||
$symbol = $curr['symbol'];
|
||||
$currency_fields['flat_rate_' . $code] = array(
|
||||
'type' => 'number',
|
||||
'label' => sprintf(__( 'Flat Rate (%s)', 'formipay' ), $code),
|
||||
'step' => 0.01,
|
||||
'min' => 0,
|
||||
'placeholder' => '0.00',
|
||||
);
|
||||
}
|
||||
|
||||
// Build free shipping threshold field (use primary currency)
|
||||
$primary_currency = $enabled_currencies[0] ?? [];
|
||||
$primary_symbol = $primary_currency['symbol'] ?? '';
|
||||
|
||||
$fields['shipping_destinations'] = array(
|
||||
'type' => 'repeater',
|
||||
'label' => __( 'Destinations', 'formipay' ),
|
||||
'description' => __( 'Add countries you ship to and configure their shipping options', 'formipay' ),
|
||||
'fields' => array_merge(
|
||||
[
|
||||
'country' => array(
|
||||
'type' => 'select',
|
||||
'label' => __('Country', 'formipay'),
|
||||
'options' => $country_options,
|
||||
'required' => true,
|
||||
'searchable' => true,
|
||||
'is_group_title' => true
|
||||
),
|
||||
'rate_source' => array(
|
||||
'type' => 'select',
|
||||
'label' => __('Rate Source', 'formipay'),
|
||||
'options' => array(
|
||||
'flat_rate' => __( 'Flat Rate', 'formipay' ),
|
||||
// 'api' => __( 'Carrier API', 'formipay' ), // Phase 4
|
||||
),
|
||||
'value' => 'flat_rate',
|
||||
),
|
||||
],
|
||||
$currency_fields,
|
||||
[
|
||||
'free_shipping_threshold' => array(
|
||||
'type' => 'number',
|
||||
'label' => sprintf(__( 'Free Shipping Threshold (%s)', 'formipay' ), $primary_symbol),
|
||||
'description' => __( 'Order amount above which shipping is free. Leave empty to disable.', 'formipay' ),
|
||||
'step' => 0.01,
|
||||
'min' => 0,
|
||||
'placeholder' => 'Empty = disabled',
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Note: Carrier API settings will be added in Phase 4
|
||||
$fields['carrier_api_note'] = array(
|
||||
'type' => 'notification_message',
|
||||
'description' => __( '
|
||||
<h3>Carrier API Integration</h3>
|
||||
<p>Live carrier rates (Rajaongkir, Biteship, etc.) will be available in Phase 4 of the shipping module.</p>
|
||||
<p>Currently, only Flat Rate and Free Shipping methods are available at the product level.</p>
|
||||
', 'formipay' ),
|
||||
);
|
||||
|
||||
return $fields;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add shipping configuration to form settings
|
||||
* This replaces product-level shipping with form-level shipping
|
||||
*/
|
||||
public function add_form_shipping_config($fields) {
|
||||
|
||||
$shipping_methods = apply_filters( 'formipay/form-settings/tab:shipping/method', [
|
||||
'no_shipping' => [
|
||||
'method' => __( 'No Shipping Required', 'formipay' )
|
||||
],
|
||||
'flat_rate' => [
|
||||
'method' => __( 'Flat Rate', 'formipay' )
|
||||
],
|
||||
'free_shipping' => [
|
||||
'method' => __( 'Free Shipping', 'formipay' )
|
||||
]
|
||||
] );
|
||||
|
||||
$shipping_options = [];
|
||||
$shipping_fields = [];
|
||||
|
||||
foreach($shipping_methods as $id => $shipping){
|
||||
// $id = $shipping['id'];
|
||||
$label = $shipping['method'];
|
||||
if(isset($shipping['courier'])){
|
||||
$label .= ' - '.$shipping['courier'];
|
||||
@@ -59,102 +295,440 @@ abstract class Shipping {
|
||||
$shipping_options[$id] = $label;
|
||||
}
|
||||
|
||||
$shipping_fields = [
|
||||
'shipping_notice' => array(
|
||||
'type' => 'notification_message',
|
||||
'image' => FORMIPAY_URL . 'admin/assets/img/logistics.png',
|
||||
'description' => __( '
|
||||
<h1>No Shipping Method Available</h1>
|
||||
<p>Shipping methods only for physical product type. If you insist to use shipping method, change your product type first</p>
|
||||
', 'formipay' ),
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'digital',
|
||||
'section' => 'general'
|
||||
),
|
||||
),
|
||||
'shipping_method' => array(
|
||||
'type' => 'radio',
|
||||
'label' => esc_html__('Shipping Methods', 'formipay'),
|
||||
'options' => $shipping_options,
|
||||
'dependency' => array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
)
|
||||
// Main shipping configuration group
|
||||
$shipping_config_group = [
|
||||
'shipping_enable_group' => [
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Shipping Configuration', 'formipay' ),
|
||||
'description' => __( 'Configure shipping options for this form. Shipping will be calculated based on form settings, not per-product.', 'formipay' ),
|
||||
'group' => 'started'
|
||||
],
|
||||
'shipping_enabled' => [
|
||||
'type' => 'radio',
|
||||
'label' => __( 'Shipping Method', 'formipay' ),
|
||||
'options' => $shipping_options,
|
||||
'value' => 'no_shipping',
|
||||
'description' => __( 'Select how shipping should be handled for orders from this form', 'formipay' ),
|
||||
]
|
||||
];
|
||||
|
||||
$free_shipping_fields = array(
|
||||
'free_shipping_group' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Free Shipping Setup', 'formipay' ),
|
||||
'description' => __( 'Will not add any shipping fee to the order', 'formipay' ),
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'free_shipping'
|
||||
)
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
'group' => 'started'
|
||||
),
|
||||
'free_shipping_label' => array(
|
||||
'type' => 'text',
|
||||
'label' => __( 'Label', 'formipay' ),
|
||||
'value' => __( 'Free Shipping', 'formipay' ),
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'free_shipping'
|
||||
)
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
),
|
||||
'free_shipping_add_to_order_review' => array(
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Show in Order Review', 'formipay' ),
|
||||
'dependency' => array(
|
||||
array(
|
||||
'key' => 'product_type',
|
||||
'value' => 'physical',
|
||||
'section' => 'general'
|
||||
),
|
||||
array(
|
||||
'key' => 'shipping_method',
|
||||
'value' => 'free_shipping'
|
||||
)
|
||||
),
|
||||
'dependencies' => '&&',
|
||||
'group' => 'ended'
|
||||
),
|
||||
);
|
||||
$shipping_config_group = apply_filters( 'formipay/form-settings/tab:shipping/group:config', $shipping_config_group );
|
||||
$last_config_key = array_key_last($shipping_config_group);
|
||||
$shipping_config_group[$last_config_key]['group'] = 'ended';
|
||||
|
||||
foreach($free_shipping_fields as $key => $value) {
|
||||
$shipping_fields[$key] = $value;
|
||||
// Apply carrier-specific settings (Flat Rate, etc.)
|
||||
$carrier_settings = apply_filters( 'formipay/form-settings/tab:shipping', [] );
|
||||
|
||||
$all_shipping_fields = array_merge($shipping_config_group, $carrier_settings);
|
||||
|
||||
$fields['formipay_form_settings']['shipping'] = [
|
||||
'name' => __( 'Shipping', 'formipay' ),
|
||||
'fields' => $all_shipping_fields
|
||||
];
|
||||
|
||||
return $fields;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* =============================================
|
||||
* PHASE 4: CARRIER EXTENSION HOOKS
|
||||
* =============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add carrier API settings to global shipping settings
|
||||
* Carriers can hook into `formipay/global-settings/tab:shipping/carriers` to add their API key fields
|
||||
*
|
||||
* @param array $fields Existing shipping settings fields
|
||||
* @return array Updated fields with carrier API settings
|
||||
*/
|
||||
public function add_carrier_api_settings($fields) {
|
||||
|
||||
// Get all registered carriers
|
||||
$carriers = apply_filters('formipay/shipping/carriers', []);
|
||||
|
||||
if (empty($carriers)) {
|
||||
// No carriers registered, add info message
|
||||
$fields['carrier_api_info'] = array(
|
||||
'type' => 'notification_message',
|
||||
'description' => __( '
|
||||
<h3>Carrier API Integration</h3>
|
||||
<p>To enable live shipping rates, install a carrier extension plugin.</p>
|
||||
<p>Extensions register themselves via the <code>formipay/shipping/carriers</code> filter.</p>
|
||||
', 'formipay' ),
|
||||
);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$shipping_fields = apply_filters( 'formipay/product-settings/tab:shipping', $shipping_fields );
|
||||
// Add carrier API settings section
|
||||
$fields['carrier_api_group'] = array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'Carrier API Keys', 'formipay' ),
|
||||
'description' => __( 'Configure API credentials for live shipping rate calculation', 'formipay' ),
|
||||
'group' => 'started'
|
||||
);
|
||||
|
||||
if(!empty($shipping_fields)){
|
||||
$fields['formipay_product_settings']['shipping'] = array(
|
||||
'name' => __( 'Shipping', 'formipay' ),
|
||||
'fields' => $shipping_fields
|
||||
);
|
||||
// Allow carriers to inject their API key fields
|
||||
$carrier_fields = apply_filters('formipay/global-settings/tab:shipping/carriers', []);
|
||||
|
||||
foreach ($carrier_fields as $key => $field) {
|
||||
$fields[$key] = $field;
|
||||
}
|
||||
|
||||
// Add test connection buttons for each carrier
|
||||
foreach ($carriers as $carrier_id => $carrier) {
|
||||
if (isset($carrier['test_connection']) && $carrier['test_connection']) {
|
||||
$fields['test_connection_' . $carrier_id] = array(
|
||||
'type' => 'html',
|
||||
'label' => __( 'Test Connection', 'formipay' ),
|
||||
'html' => sprintf(
|
||||
'<button type="button" class="button formipay-test-connection" data-carrier="%s">%s</button>
|
||||
<span class="formipay-connection-result" style="margin-left: 10px;"></span>',
|
||||
esc_attr($carrier_id),
|
||||
esc_html__('Test Connection', 'formipay')
|
||||
),
|
||||
'group' => ($carrier_id === array_key_last($carriers)) ? 'ended' : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get carrier-specific address fields for checkout
|
||||
* Carriers can hook into `formipay/checkout/address-fields/{carrier_id}` to provide their fields
|
||||
*
|
||||
* @param array $fields Current address fields
|
||||
* @param string $carrier_id Carrier identifier (e.g., 'rajaongkir', 'biteship')
|
||||
* @param string $country_code Destination country code
|
||||
* @return array Address fields for this carrier
|
||||
*/
|
||||
public function get_carrier_address_fields($fields, $carrier_id, $country_code) {
|
||||
|
||||
// Get carrier-specific address fields
|
||||
$carrier_fields = apply_filters('formipay/checkout/address-fields/' . $carrier_id, [], $country_code);
|
||||
|
||||
return array_merge($fields, $carrier_fields);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for testing carrier connection
|
||||
* Carriers can hook into `formipay/test_carrier_connection/{carrier_id}` to handle the test
|
||||
*/
|
||||
public function ajax_test_carrier_connection() {
|
||||
|
||||
check_ajax_referer('formipay-admin', 'nonce', true);
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('Unauthorized', 'formipay')]);
|
||||
}
|
||||
|
||||
$carrier_id = isset($_POST['carrier']) ? sanitize_text_field(wp_unslash($_POST['carrier'])) : '';
|
||||
|
||||
if (empty($carrier_id)) {
|
||||
wp_send_json_error(['message' => __('Missing carrier ID', 'formipay')]);
|
||||
}
|
||||
|
||||
// Allow carriers to handle their own connection test
|
||||
$result = apply_filters('formipay/test_carrier_connection/' . $carrier_id, [
|
||||
'success' => false,
|
||||
'message' => __('Carrier does not implement connection test', 'formipay'),
|
||||
], $_POST);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
} else {
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Get registered carriers
|
||||
* Returns all carriers that have registered via the filter
|
||||
*
|
||||
* @return array Registered carriers
|
||||
*/
|
||||
public static function get_registered_carriers() {
|
||||
|
||||
return apply_filters('formipay/shipping/carriers', []);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Get carrier by ID
|
||||
*
|
||||
* @param string $carrier_id Carrier identifier
|
||||
* @return array|null Carrier data or null if not found
|
||||
*/
|
||||
public static function get_carrier($carrier_id) {
|
||||
|
||||
$carriers = self::get_registered_carriers();
|
||||
return $carriers[$carrier_id] ?? null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Check if carrier supports a country
|
||||
*
|
||||
* @param string $carrier_id Carrier identifier
|
||||
* @param string $country_code Country code to check
|
||||
* @return bool True if carrier supports the country
|
||||
*/
|
||||
public static function carrier_supports_country($carrier_id, $country_code) {
|
||||
|
||||
$carrier = self::get_carrier($carrier_id);
|
||||
if (!$carrier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$supported_countries = $carrier['countries'] ?? [];
|
||||
return in_array($country_code, $supported_countries, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: Fetch live rates from carrier
|
||||
*
|
||||
* @param string $carrier_id Carrier identifier
|
||||
* @param array $params Rate request parameters (origin, destination, weight, dimensions)
|
||||
* @return array Available rates with costs
|
||||
*/
|
||||
public static function fetch_live_rates($carrier_id, $params) {
|
||||
|
||||
$default_params = [
|
||||
'origin_country' => '',
|
||||
'origin_city' => '',
|
||||
'origin_postcode' => '',
|
||||
'destination_country' => '',
|
||||
'destination_city' => '',
|
||||
'destination_postcode' => '',
|
||||
'weight' => 0,
|
||||
'weight_unit' => 'kg',
|
||||
'length' => 0,
|
||||
'width' => 0,
|
||||
'height' => 0,
|
||||
'dimension_unit' => 'cm',
|
||||
];
|
||||
|
||||
$params = wp_parse_args($params, $default_params);
|
||||
|
||||
return apply_filters('formipay/shipping/live-rates', [], $carrier_id, $params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* =============================================
|
||||
* PHASE 5: CHECKOUT INTEGRATION
|
||||
* =============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* AJAX: Get available shipping methods for a form
|
||||
* Now reads from form-level shipping settings instead of global settings
|
||||
*/
|
||||
public function ajax_get_shipping_methods() {
|
||||
|
||||
check_ajax_referer('formipay-public', 'nonce', true);
|
||||
|
||||
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
|
||||
$country_code = isset($_POST['country']) ? sanitize_text_field(wp_unslash($_POST['country'])) : '';
|
||||
$currency = isset($_POST['currency']) ? sanitize_text_field(wp_unslash($_POST['currency'])) : '';
|
||||
|
||||
if (!$form_id) {
|
||||
wp_send_json_error(['message' => __('Invalid request', 'formipay')]);
|
||||
}
|
||||
|
||||
// Get form shipping settings
|
||||
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
|
||||
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
|
||||
|
||||
$available_methods = [];
|
||||
|
||||
if ($shipping_enabled === 'flat_rate') {
|
||||
// Get flat rate from form settings
|
||||
$currency_code = $currency ?: 'IDR';
|
||||
$rate_key = 'flat_rate_amount_' . $currency_code;
|
||||
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
|
||||
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
|
||||
|
||||
// For percentage, we'll calculate on frontend based on cart total
|
||||
// For now, store the type so frontend knows how to handle it
|
||||
$available_methods[] = [
|
||||
'id' => 'flat_rate',
|
||||
'name' => __('Standard Shipping', 'formipay'),
|
||||
'description' => $flat_rate_type === 'percentage'
|
||||
? sprintf(__('%s%% of order total', 'formipay'), $flat_rate)
|
||||
: __('Delivery in 3-5 business days', 'formipay'),
|
||||
'cost' => $flat_rate,
|
||||
'currency' => $currency_code,
|
||||
'type' => $flat_rate_type,
|
||||
];
|
||||
|
||||
} elseif ($shipping_enabled === 'free_shipping') {
|
||||
// Free shipping from form settings
|
||||
$free_label = $form_settings['free_shipping_label'] ?? __('Free Shipping', 'formipay');
|
||||
$available_methods[] = [
|
||||
'id' => 'free_shipping',
|
||||
'name' => $free_label,
|
||||
'description' => __('No shipping cost', 'formipay'),
|
||||
'cost' => 0,
|
||||
'currency' => $currency_code ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// If country-specific shipping is needed in future, add check here
|
||||
// For now, form-level shipping applies to all countries
|
||||
|
||||
if (empty($available_methods)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Shipping is not available for this form', 'formipay'),
|
||||
'methods' => []
|
||||
]);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'methods' => $available_methods,
|
||||
'default_method' => $available_methods[0]['id'],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add shipping cost to cart calculation
|
||||
* Hooked into formipay/checkout/cart/calculation
|
||||
*/
|
||||
public function add_shipping_to_cart($cart, $form_id, $selected_currency) {
|
||||
|
||||
// Check if shipping is enabled for this form
|
||||
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
|
||||
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
|
||||
|
||||
if ($shipping_enabled === 'no_shipping') {
|
||||
return $cart;
|
||||
}
|
||||
|
||||
// Get selected shipping method from POST data or session
|
||||
$shipping_method = isset($_POST['shipping_method']) ? sanitize_text_field(wp_unslash($_POST['shipping_method'])) : '';
|
||||
$shipping_country = isset($_POST['shipping_country']) ? sanitize_text_field(wp_unslash($_POST['shipping_country'])) : '';
|
||||
|
||||
if (empty($shipping_method) || empty($shipping_country)) {
|
||||
return $cart;
|
||||
}
|
||||
|
||||
// Parse shipping method ID to get cost
|
||||
// Format: flat_rate, free_shipping, or {carrier}_{service}
|
||||
if ($shipping_method === 'free_shipping') {
|
||||
// Free shipping
|
||||
$cart['shipping'] = [
|
||||
'name' => __('Free Shipping', 'formipay'),
|
||||
'cost' => 0,
|
||||
];
|
||||
} elseif ($shipping_method === 'flat_rate') {
|
||||
// Flat rate - get cost from form settings
|
||||
$currency_code = $selected_currency ?: 'IDR';
|
||||
$rate_key = 'flat_rate_amount_' . $currency_code;
|
||||
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
|
||||
|
||||
// Check if percentage
|
||||
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
|
||||
if ($flat_rate_type === 'percentage') {
|
||||
$subtotal = floatval($cart['subtotal'] ?? 0);
|
||||
$flat_rate = ($subtotal * $flat_rate) / 100;
|
||||
}
|
||||
|
||||
$cart['shipping'] = [
|
||||
'name' => __('Standard Shipping', 'formipay'),
|
||||
'cost' => $flat_rate,
|
||||
];
|
||||
|
||||
// Recalculate totals
|
||||
$cart['subtotal'] = floatval($cart['subtotal'] ?? 0);
|
||||
$cart['tax'] = floatval($cart['tax'] ?? 0);
|
||||
$cart['discount'] = floatval($cart['discount'] ?? 0);
|
||||
|
||||
$cart['grand'] = $cart['subtotal'] + $cart['tax'] + $cart['shipping']['cost'] - $cart['discount'];
|
||||
}
|
||||
|
||||
return $cart;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add shipping data to order submission
|
||||
* Hooked into formipay/order/process-data
|
||||
*/
|
||||
public function add_shipping_to_order_data($form_data, $form_id) {
|
||||
|
||||
// Check if shipping is enabled for this form
|
||||
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
|
||||
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
|
||||
|
||||
if ($shipping_enabled === 'no_shipping') {
|
||||
return $form_data;
|
||||
}
|
||||
|
||||
// Add shipping info to form data
|
||||
if (isset($_POST['shipping_method'])) {
|
||||
$form_data['shipping_method'] = sanitize_text_field(wp_unslash($_POST['shipping_method']));
|
||||
}
|
||||
|
||||
if (isset($_POST['shipping_country'])) {
|
||||
$form_data['shipping_country'] = sanitize_text_field(wp_unslash($_POST['shipping_country']));
|
||||
}
|
||||
|
||||
return $form_data;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get supported shipping countries
|
||||
* With form-level shipping, returns all available countries
|
||||
*/
|
||||
public function ajax_get_supported_countries() {
|
||||
|
||||
// Verify nonce
|
||||
if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'formipay_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('Invalid security token', 'formipay')]);
|
||||
}
|
||||
|
||||
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
|
||||
|
||||
if (!$form_id) {
|
||||
wp_send_json_error(['message' => __('Invalid form ID', 'formipay')]);
|
||||
}
|
||||
|
||||
// Load countries from JSON file
|
||||
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
|
||||
$all_countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
|
||||
|
||||
// Build country list
|
||||
$countries = [];
|
||||
if (is_array($all_countries)) {
|
||||
foreach ($all_countries as $country) {
|
||||
$code = $country['code'] ?? '';
|
||||
$name = $country['name'] ?? '';
|
||||
if ($code && $name) {
|
||||
$countries[$code] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($countries)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('No countries available', 'formipay'),
|
||||
'countries' => []
|
||||
]);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'countries' => $countries,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/admin/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user