feat: add React FieldRenderer system for settings and metaboxes
Complete React-based field rendering system that replaces WPCFTO Vue.js layer while maintaining PHP field configuration compatibility. Components: - FieldRenderer: Main renderer with tabs support (metabox) and direct mode (settings) - FieldTypes: 15+ field types (Text, Number, Select, Radio, Date, etc.) - RepeaterField: Collapsible repeater with currency label parsing - DependencyEngine: Show/hide fields based on conditions - ValidationEngine: Client-side validation with error messages - SettingsRenderer: Settings page with AJAX save to wp_options Features: - Repeater rows collapsed by default with readable currency titles - Searchable dropdowns using Popover + Command pattern - Proper label resolution for pre-selected values - Hidden input sync for WordPress form submission Also includes: - FieldConfigBridge: Transform PHP configs to React format - Updated Settings.php for React-based settings page - Radio-group UI component - wp-admin-restore.css for admin panel isolation
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -9,4 +9,17 @@ coverage
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?node_modules/
|
*.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.*
|
|
||||||
@@ -24,6 +24,11 @@ class Access {
|
|||||||
add_filter( 'formipay/access-config', [$this, 'source_config'] );
|
add_filter( 'formipay/access-config', [$this, 'source_config'] );
|
||||||
add_filter( 'formipay/access-config', [$this, 'details_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
|
// Admin Page
|
||||||
add_action( 'wp_ajax_formipay_access_items_get_products', [$this, 'formipay_access_items_get_products'] );
|
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'] );
|
add_action( 'wp_ajax_formipay-tabledata-access-items', [$this, 'formipay_tabledata_access_items'] );
|
||||||
@@ -90,82 +95,77 @@ class Access {
|
|||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
// Assets now handled by ReactAdmin class
|
// Assets now handled by ReactAdmin class
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_react_metabox() {
|
||||||
|
add_meta_box(
|
||||||
|
'formipay_access_settings',
|
||||||
|
__('Settings', 'formipay'),
|
||||||
|
[$this, 'render_react_metabox'],
|
||||||
|
'formipay-access',
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_react_metabox($post) {
|
||||||
|
echo '<div data-formipay-field-renderer="access" data-post-id="' . esc_attr($post->ID) . '"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render_react_metabox_template() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
if (!$post || $post->post_type !== 'formipay-access') {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$screen = get_current_screen();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
wp_localize_script( 'sweetalert2', 'formipay_admin', [
|
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
|
||||||
'site_url' => site_url(),
|
|
||||||
] );
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if($current_screen->id == 'formipay_page_formipay-access-items') {
|
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
|
||||||
|
?>
|
||||||
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], 'all');
|
<script type="text/javascript">
|
||||||
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION, 'all' );
|
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
|
||||||
wp_enqueue_style( 'gridjs', FORMIPAY_URL . 'vendor/GridJS/gridjs.mermaid.min.css', [], '6.2.0', 'all' );
|
</script>
|
||||||
wp_enqueue_style( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.css', [], FORMIPAY_VERSION, 'all' );
|
<?php
|
||||||
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' );
|
|
||||||
|
|
||||||
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) {
|
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,6 +13,9 @@ class ReactAdmin {
|
|||||||
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
|
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
|
||||||
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
|
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() {
|
public function enqueue_assets() {
|
||||||
@@ -40,18 +43,16 @@ class ReactAdmin {
|
|||||||
$dependencies = $assets_file['dependencies'] ?? [];
|
$dependencies = $assets_file['dependencies'] ?? [];
|
||||||
|
|
||||||
// Filter out icon build dependencies - they're bundled, not separate scripts
|
// Filter out icon build dependencies - they're bundled, not separate scripts
|
||||||
$original_count = count($dependencies);
|
|
||||||
$dependencies = array_values(array_filter($dependencies, function($dep) {
|
$dependencies = array_values(array_filter($dependencies, function($dep) {
|
||||||
return strpos($dep, 'wp-icons/build/') === false;
|
return strpos($dep, 'wp-icons/build/') === false;
|
||||||
}));
|
}));
|
||||||
error_log('[Formipay] Filtered dependencies: ' . $original_count . ' -> ' . count($dependencies));
|
|
||||||
|
|
||||||
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
|
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
|
||||||
|
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'formipay-admin-style',
|
'formipay-admin-style',
|
||||||
$build_url . '/admin.css',
|
$build_url . '/admin.css',
|
||||||
[],
|
['wp-admin', 'colors', 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus'],
|
||||||
$version
|
$version
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -72,10 +73,6 @@ class ReactAdmin {
|
|||||||
'siteUrl' => site_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);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -161,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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ class Coupon {
|
|||||||
// React Metabox
|
// React Metabox
|
||||||
add_action( 'add_meta_boxes', [$this, 'add_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.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
|
// Order
|
||||||
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
||||||
@@ -112,13 +116,6 @@ class Coupon {
|
|||||||
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
|
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_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_enqueue_script('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||||
|
|
||||||
// Localize admin data
|
|
||||||
wp_localize_script('formipay-admin', 'formipayAdmin', [
|
|
||||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
||||||
'siteUrl' => site_url(),
|
|
||||||
'nonce' => wp_create_nonce('formipay-admin'),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +151,7 @@ class Coupon {
|
|||||||
$rules_group_1 = array(
|
$rules_group_1 = array(
|
||||||
'rules_general_group' => array(
|
'rules_general_group' => array(
|
||||||
'type' => 'group_title',
|
'type' => 'group_title',
|
||||||
|
'label' => __( 'General', 'formipay' ),
|
||||||
'group' => 'started'
|
'group' => 'started'
|
||||||
),
|
),
|
||||||
'active' => array(
|
'active' => array(
|
||||||
@@ -169,6 +167,7 @@ class Coupon {
|
|||||||
'fixed' => __( 'Fixed', 'formipay' ),
|
'fixed' => __( 'Fixed', 'formipay' ),
|
||||||
'percentage' => __( 'Percentage', 'formipay' )
|
'percentage' => __( 'Percentage', 'formipay' )
|
||||||
),
|
),
|
||||||
|
'value' => 'fixed'
|
||||||
),
|
),
|
||||||
'amount_percentage' => array(
|
'amount_percentage' => array(
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
@@ -209,8 +208,7 @@ class Coupon {
|
|||||||
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||||
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
||||||
$decimal_digits = intval($currency['decimal_digits']);
|
$decimal_digits = intval($currency['decimal_digits']);
|
||||||
$step = $decimal_digits * 10;
|
$step = $decimal_digits > 0 ? pow(10, -$decimal_digits) : 1;
|
||||||
$step = $step > 0 ? 1 / $step : 1;
|
|
||||||
|
|
||||||
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
|
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
@@ -238,7 +236,11 @@ class Coupon {
|
|||||||
),
|
),
|
||||||
'step' => $step,
|
'step' => $step,
|
||||||
'min' => 0,
|
'min' => 0,
|
||||||
'placeholder' => __( 'Enter Max Amount...', 'formipay' )
|
'placeholder' => __( 'Enter Max Amount...', 'formipay' ),
|
||||||
|
'dependency' => array(
|
||||||
|
'key' => 'type',
|
||||||
|
'value' => 'percentage'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -324,7 +326,8 @@ class Coupon {
|
|||||||
'use_limit' => array(
|
'use_limit' => array(
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'label' => __( 'Usage Limit', 'formipay' ),
|
'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(
|
'date_limit' => array(
|
||||||
'type' => 'date',
|
'type' => 'date',
|
||||||
@@ -343,11 +346,11 @@ class Coupon {
|
|||||||
'label' => __( 'Products', 'formipay' ),
|
'label' => __( 'Products', 'formipay' ),
|
||||||
'description' => __( 'Only selected product(s) can use the coupon. Leave empty to apply to all 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',
|
'type' => 'autocomplete',
|
||||||
'post_type' => array('formipay-product'),
|
'object_type' => 'user',
|
||||||
'label' => __( 'Customers', 'formipay' ),
|
'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' )
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -588,6 +591,14 @@ class Coupon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$date_limit = formipay_get_post_meta($coupon->ID, 'date_limit');
|
$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');
|
$type = formipay_get_post_meta($coupon->ID, 'type');
|
||||||
$amount_meta_key = "amount_$type";
|
$amount_meta_key = "amount_$type";
|
||||||
@@ -622,7 +633,7 @@ class Coupon {
|
|||||||
'case_sensitive' => formipay_get_post_meta($coupon->ID, 'case_sensitive'),
|
'case_sensitive' => formipay_get_post_meta($coupon->ID, 'case_sensitive'),
|
||||||
'usage_count' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
|
'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)),
|
'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',
|
'active' => $is_active ? 'on' : 'off',
|
||||||
'post_status' => $is_active ? 'active' : 'inactive',
|
'post_status' => $is_active ? 'active' : 'inactive',
|
||||||
'status' => $is_active ? 'active' : 'inactive'
|
'status' => $is_active ? 'active' : 'inactive'
|
||||||
@@ -885,7 +896,7 @@ class Coupon {
|
|||||||
'max_amounts' => [],
|
'max_amounts' => [],
|
||||||
'forms' => formipay_get_post_meta($post_id, 'forms') ?: [],
|
'forms' => formipay_get_post_meta($post_id, 'forms') ?: [],
|
||||||
'products' => formipay_get_post_meta($post_id, 'products') ?: [],
|
'products' => formipay_get_post_meta($post_id, 'products') ?: [],
|
||||||
'customers' => formipay_get_post_meta($post_id, 'customers') ?: [],
|
'users' => formipay_get_post_meta($post_id, 'users') ?: [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get fixed amounts for each currency
|
// Get fixed amounts for each currency
|
||||||
@@ -987,7 +998,7 @@ class Coupon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save relation fields
|
// Save relation fields
|
||||||
$relation_fields = ['forms', 'products', 'customers'];
|
$relation_fields = ['forms', 'products', 'users'];
|
||||||
foreach ($relation_fields as $field) {
|
foreach ($relation_fields as $field) {
|
||||||
if ( isset($_REQUEST[$field]) ) {
|
if ( isset($_REQUEST[$field]) ) {
|
||||||
$values = is_array($_REQUEST[$field]) ? array_map('intval', $_REQUEST[$field]) : [];
|
$values = is_array($_REQUEST[$field]) ? array_map('intval', $_REQUEST[$field]) : [];
|
||||||
@@ -1006,7 +1017,7 @@ class Coupon {
|
|||||||
*/
|
*/
|
||||||
public function add_react_metabox() {
|
public function add_react_metabox() {
|
||||||
add_meta_box(
|
add_meta_box(
|
||||||
'formipay_coupon_reactor_metabox',
|
'formipay_coupon_settings',
|
||||||
__( 'Coupon Settings', 'formipay' ),
|
__( 'Coupon Settings', 'formipay' ),
|
||||||
[$this, 'render_react_metabox'],
|
[$this, 'render_react_metabox'],
|
||||||
'formipay-coupon',
|
'formipay-coupon',
|
||||||
@@ -1021,9 +1032,9 @@ class Coupon {
|
|||||||
public function render_react_metabox($post) {
|
public function render_react_metabox($post) {
|
||||||
?>
|
?>
|
||||||
<div
|
<div
|
||||||
data-formipay-metabox="coupon"
|
data-formipay-field-renderer="coupon"
|
||||||
data-post-id="<?php echo esc_attr($post->ID); ?>"
|
data-post-id="<?php echo esc_attr($post->ID); ?>"
|
||||||
class="formipay-react-metabox-container"
|
class="formipay-field-renderer-container"
|
||||||
>
|
>
|
||||||
<div class="formipay-loading">
|
<div class="formipay-loading">
|
||||||
<div class="formipay-spinner"></div>
|
<div class="formipay-spinner"></div>
|
||||||
@@ -1042,9 +1053,39 @@ class Coupon {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get global currencies
|
// 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();
|
$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">
|
<script type="text/javascript">
|
||||||
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
|
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
|
||||||
@@ -1062,6 +1103,7 @@ class Coupon {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Autocomplete search for relation fields
|
* Autocomplete search for relation fields
|
||||||
|
* Supports CPTs (post_type) and WP Users (object_type=user)
|
||||||
*/
|
*/
|
||||||
public function formipay_autocomplete_search() {
|
public function formipay_autocomplete_search() {
|
||||||
|
|
||||||
@@ -1072,9 +1114,56 @@ class Coupon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : '';
|
$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'])) : '';
|
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
|
||||||
$include = isset($_REQUEST['include']) ? array_map('intval', (array) $_REQUEST['include']) : [];
|
$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)) {
|
if (empty($post_type)) {
|
||||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||||
}
|
}
|
||||||
@@ -1125,4 +1214,78 @@ class Coupon {
|
|||||||
wp_send_json_success($results);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Formipay;
|
namespace Formipay;
|
||||||
use Formipay\Traits\SingletonTrait;
|
use Formipay\Traits\SingletonTrait;
|
||||||
|
use Formipay\Admin\FieldConfigBridge;
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
@@ -14,13 +15,36 @@ class Settings {
|
|||||||
|
|
||||||
protected function __construct() {
|
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_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', [] );
|
$gateways = apply_filters( 'formipay/form-config/tab:payments/gateways', [] );
|
||||||
|
|
||||||
$payment_checkboxes = [];
|
$payment_checkboxes = [];
|
||||||
@@ -268,40 +292,32 @@ class Settings {
|
|||||||
|
|
||||||
$pages_fields = apply_filters( 'formipay/global-settings/tab:pages', $pages_fields );
|
$pages_fields = apply_filters( 'formipay/global-settings/tab:pages', $pages_fields );
|
||||||
|
|
||||||
$global = array(
|
$tabs_config = [
|
||||||
'General' => array(
|
'General' => [
|
||||||
'name' => __( 'General', 'formipay' ),
|
'name' => __( 'General', 'formipay' ),
|
||||||
'fields' => $general_fields
|
'fields' => $general_fields
|
||||||
),
|
],
|
||||||
'Pages' => array(
|
'Pages' => [
|
||||||
'name' => __( 'Pages', 'formipay' ),
|
'name' => __( 'Pages', 'formipay' ),
|
||||||
'fields' => $pages_fields
|
'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){
|
return $tabs_config;
|
||||||
$fields[$key] = $value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$setups[] = array(
|
/**
|
||||||
'option_name' => 'formipay_settings',
|
* Render settings page (empty container for React)
|
||||||
'title' => __('Formipay', 'formipay'),
|
*/
|
||||||
'sub_title' => __('Settings', 'formipay'),
|
public function render_settings_page() {
|
||||||
'logo' => FORMIPAY_URL . 'admin/assets/img/formipay-logo-circle-white.png',
|
?>
|
||||||
'page' => array(
|
<div class="wrap">
|
||||||
'parent_slug' => 'formipay',
|
<div id="formipay-settings-page-container"></div>
|
||||||
'page_title' => __('Formipay Settings', 'formipay'),
|
</div>
|
||||||
'menu_title' => __('Settings', 'formipay'),
|
<?php
|
||||||
'menu_slug' => 'formipay-settings',
|
|
||||||
'position' => 40,
|
|
||||||
),
|
|
||||||
|
|
||||||
'fields' => $fields
|
|
||||||
);
|
|
||||||
|
|
||||||
return $setups;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue() {
|
public function enqueue() {
|
||||||
@@ -309,17 +325,81 @@ class Settings {
|
|||||||
global $current_screen;
|
global $current_screen;
|
||||||
|
|
||||||
if ( $current_screen->id === 'formipay_page_formipay-settings' ) {
|
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' );
|
// Enqueue React admin assets for FieldRenderer
|
||||||
wp_enqueue_script('admin-setting-script', FORMIPAY_URL . 'admin/assets/js/admin-setting.js', ['jquery'], FORMIPAY_VERSION, true);
|
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'),
|
* Render React settings template in admin footer
|
||||||
'site_url' => site_url(),
|
* This outputs the config JSON and mount point for the React FieldRenderer
|
||||||
'nonce' => wp_create_nonce('formipay-admin-nonce'),
|
*/
|
||||||
'multicurrency' => formipay_is_multi_currency_active(),
|
public function render_react_settings_template() {
|
||||||
'all_currencies' => formipay_currency_as_options(),
|
global $current_screen;
|
||||||
'global_selected_currencies' => formipay_global_currency_options(),
|
|
||||||
'default_currency' => formipay_default_currency()
|
// 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' )
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.
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