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:
dwindown
2026-04-28 16:48:08 +07:00
parent 7a6765a579
commit 622c9f8eb7
206 changed files with 5788 additions and 1612 deletions

15
.gitignore vendored
View File

@@ -9,4 +9,17 @@ coverage
*.ntvs*
*.njsproj
*.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

View File

@@ -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.*

View File

@@ -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.*

View File

@@ -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.*

View File

@@ -24,6 +24,11 @@ class Access {
add_filter( 'formipay/access-config', [$this, 'source_config'] );
add_filter( 'formipay/access-config', [$this, 'details_config'] );
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
add_action( 'save_post', [$this, 'save_access_metabox_fields'], 10, 2 );
// Admin Page
add_action( 'wp_ajax_formipay_access_items_get_products', [$this, 'formipay_access_items_get_products'] );
add_action( 'wp_ajax_formipay-tabledata-access-items', [$this, 'formipay_tabledata_access_items'] );
@@ -90,82 +95,77 @@ class Access {
public function enqueue_admin() {
// Assets now handled by ReactAdmin class
return;
}
$screen = get_current_screen();
public function add_react_metabox() {
add_meta_box(
'formipay_access_settings',
__('Settings', 'formipay'),
[$this, 'render_react_metabox'],
'formipay-access',
'normal',
'high'
);
}
// Check that we are on the 'Checker' post editor screen
if ( $screen->post_type === 'formipay-access' && $screen->base === 'post' ) {
public function render_react_metabox($post) {
echo '<div data-formipay-field-renderer="access" data-post-id="' . esc_attr($post->ID) . '"></div>';
}
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(),
] );
public function render_react_metabox_template() {
global $post;
if (!$post || $post->post_type !== 'formipay-access') {
return;
}
if($current_screen->id == 'formipay_page_formipay-access-items') {
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
?>
<script type="text/javascript">
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
</script>
<?php
}
wp_enqueue_style( 'sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], 'all');
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION, 'all' );
wp_enqueue_style( 'gridjs', FORMIPAY_URL . 'vendor/GridJS/gridjs.mermaid.min.css', [], '6.2.0', 'all' );
wp_enqueue_style( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.css', [], FORMIPAY_VERSION, 'all' );
wp_enqueue_style( 'formipay-admin-pages', FORMIPAY_URL . 'admin/assets/css/admin-pages.css', [], FORMIPAY_VERSION, 'all' );
wp_enqueue_style( 'page-access-items', FORMIPAY_URL . 'admin/assets/css/page-access-items.css', [], FORMIPAY_VERSION, 'all' );
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) {

View 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));
}
}

View File

@@ -13,6 +13,9 @@ class ReactAdmin {
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
// AJAX endpoint for field configuration
add_action( 'wp_ajax_formipay-get-field-config', [$this, 'ajax_get_field_config'] );
}
public function enqueue_assets() {
@@ -40,18 +43,16 @@ class ReactAdmin {
$dependencies = $assets_file['dependencies'] ?? [];
// Filter out icon build dependencies - they're bundled, not separate scripts
$original_count = count($dependencies);
$dependencies = array_values(array_filter($dependencies, function($dep) {
return strpos($dep, 'wp-icons/build/') === false;
}));
error_log('[Formipay] Filtered dependencies: ' . $original_count . ' -> ' . count($dependencies));
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
wp_enqueue_style(
'formipay-admin-style',
$build_url . '/admin.css',
[],
['wp-admin', 'colors', 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus'],
$version
);
@@ -72,10 +73,6 @@ class ReactAdmin {
'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);
}
@@ -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);
}
}

View File

@@ -42,6 +42,10 @@ class Coupon {
// React Metabox
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
// Save coupon data via WordPress save_post hook (regular hook with post type check inside)
add_action( 'save_post', [$this, 'save_coupon_on_post_update'], 10, 2 );
// Order
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
@@ -112,13 +116,6 @@ class Coupon {
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
wp_enqueue_style('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
wp_enqueue_script('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
// 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_general_group' => array(
'type' => 'group_title',
'label' => __( 'General', 'formipay' ),
'group' => 'started'
),
'active' => array(
@@ -169,6 +167,7 @@ class Coupon {
'fixed' => __( 'Fixed', 'formipay' ),
'percentage' => __( 'Percentage', 'formipay' )
),
'value' => 'fixed'
),
'amount_percentage' => array(
'type' => 'number',
@@ -209,8 +208,7 @@ class Coupon {
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
$decimal_digits = intval($currency['decimal_digits']);
$step = $decimal_digits * 10;
$step = $step > 0 ? 1 / $step : 1;
$step = $decimal_digits > 0 ? pow(10, -$decimal_digits) : 1;
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
'type' => 'number',
@@ -238,7 +236,11 @@ class Coupon {
),
'step' => $step,
'min' => 0,
'placeholder' => __( 'Enter Max Amount...', 'formipay' )
'placeholder' => __( 'Enter Max Amount...', 'formipay' ),
'dependency' => array(
'key' => 'type',
'value' => 'percentage'
)
);
}
@@ -324,7 +326,8 @@ class Coupon {
'use_limit' => array(
'type' => 'number',
'label' => __( 'Usage Limit', 'formipay' ),
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' )
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' ),
'placeholder' => __( 'Set limit...', 'formipay' )
),
'date_limit' => array(
'type' => 'date',
@@ -343,11 +346,11 @@ class Coupon {
'label' => __( 'Products', 'formipay' ),
'description' => __( 'Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay' )
),
'customers' => array(
'users' => array(
'type' => 'autocomplete',
'post_type' => array('formipay-product'),
'object_type' => 'user',
'label' => __( 'Customers', 'formipay' ),
'description' => __( 'Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay' )
'description' => __( 'Only selected registered customer(s) can use this coupon. Leave empty to apply to all customers.', 'formipay' )
)
);
@@ -588,6 +591,14 @@ class Coupon {
}
$date_limit = formipay_get_post_meta($coupon->ID, 'date_limit');
$date_limit_display = 'none';
if ($date_limit && $date_limit !== '') {
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $date_limit)) {
$date_limit_display = $date_limit;
} elseif (is_numeric($date_limit)) {
$date_limit_display = formipay_date('Y-m-d', intval($date_limit) / 1000);
}
}
$type = formipay_get_post_meta($coupon->ID, 'type');
$amount_meta_key = "amount_$type";
@@ -622,7 +633,7 @@ class Coupon {
'case_sensitive' => formipay_get_post_meta($coupon->ID, 'case_sensitive'),
'usage_count' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
'usages' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
'date_limit' => false !== $date_limit ? formipay_date('Y-m-d', intval(formipay_get_post_meta($coupon->ID, 'date_limit')) / 1000) : 'none',
'date_limit' => $date_limit_display,
'active' => $is_active ? 'on' : 'off',
'post_status' => $is_active ? 'active' : 'inactive',
'status' => $is_active ? 'active' : 'inactive'
@@ -885,7 +896,7 @@ class Coupon {
'max_amounts' => [],
'forms' => formipay_get_post_meta($post_id, 'forms') ?: [],
'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
@@ -987,7 +998,7 @@ class Coupon {
}
// Save relation fields
$relation_fields = ['forms', 'products', 'customers'];
$relation_fields = ['forms', 'products', 'users'];
foreach ($relation_fields as $field) {
if ( isset($_REQUEST[$field]) ) {
$values = is_array($_REQUEST[$field]) ? array_map('intval', $_REQUEST[$field]) : [];
@@ -1006,7 +1017,7 @@ class Coupon {
*/
public function add_react_metabox() {
add_meta_box(
'formipay_coupon_reactor_metabox',
'formipay_coupon_settings',
__( 'Coupon Settings', 'formipay' ),
[$this, 'render_react_metabox'],
'formipay-coupon',
@@ -1021,9 +1032,9 @@ class Coupon {
public function render_react_metabox($post) {
?>
<div
data-formipay-metabox="coupon"
data-formipay-field-renderer="coupon"
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-spinner"></div>
@@ -1042,9 +1053,39 @@ class Coupon {
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();
// Fallback: if no multicurrencies configured, use default currency
if (empty($global_currencies)) {
$formipay_settings = get_option('formipay_settings');
$default_currency_raw = formipay_default_currency('raw');
if (!empty($default_currency_raw)) {
$global_currencies = [
[
'currency' => $default_currency_raw,
'decimal_digits' => isset($formipay_settings['default_currency_decimal_digits']) ? intval($formipay_settings['default_currency_decimal_digits']) : 2,
'decimal_symbol' => isset($formipay_settings['default_currency_decimal_symbol']) ? $formipay_settings['default_currency_decimal_symbol'] : '.',
'thousand_separator' => isset($formipay_settings['default_currency_thousand_separator']) ? $formipay_settings['default_currency_thousand_separator'] : ',',
]
];
}
}
?>
<script type="text/javascript">
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
@@ -1062,6 +1103,7 @@ class Coupon {
/**
* Autocomplete search for relation fields
* Supports CPTs (post_type) and WP Users (object_type=user)
*/
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'])) : '';
$object_type = isset($_REQUEST['object_type']) ? sanitize_text_field(wp_unslash($_REQUEST['object_type'])) : '';
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
$include = isset($_REQUEST['include']) ? array_map('intval', (array) $_REQUEST['include']) : [];
// Handle WP Users
if ($object_type === 'user') {
// Resolve labels for specific IDs (pre-selected items)
if (!empty($include)) {
$users = get_users(['include' => $include, 'fields' => ['ID', 'display_name', 'user_email']]);
$results = [];
if (!empty($users)) {
foreach ($users as $user) {
$results[] = [
'value' => $user->ID,
'label' => $user->display_name . ' (' . $user->user_email . ')',
];
}
}
wp_send_json_success($results);
return;
}
// Search by keyword
if (strlen($search) < 2) {
wp_send_json_error( [ 'message' => 'Invalid request' ] );
}
$users = get_users([
'search' => '*' . $search . '*',
'search_columns' => ['display_name', 'user_email', 'user_login'],
'number' => 20,
'fields' => ['ID', 'display_name', 'user_email'],
]);
$results = [];
if (!empty($users)) {
foreach ($users as $user) {
$results[] = [
'value' => $user->ID,
'label' => $user->display_name . ' (' . $user->user_email . ')',
];
}
}
wp_send_json_success($results);
return;
}
// Handle CPTs (posts)
if (empty($post_type)) {
wp_send_json_error( [ 'message' => 'Invalid request' ] );
}
@@ -1125,4 +1214,78 @@ class Coupon {
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;
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Formipay;
use Formipay\Traits\SingletonTrait;
use Formipay\Admin\FieldConfigBridge;
if ( ! defined( 'ABSPATH' ) ) exit;
@@ -14,13 +15,36 @@ class Settings {
protected function __construct() {
add_filter( 'wpcfto_options_page_setup', [$this, 'theme_option'] );
// Register our submenu page
add_action( 'admin_menu', [$this, 'add_settings_page'] );
add_action( 'admin_enqueue_scripts', [$this, 'enqueue'] );
add_action( 'admin_footer', [$this, 'render_react_settings_template'] );
// AJAX handler for saving settings from React
add_action( 'wp_ajax_formipay_save_settings', [$this, 'ajax_save_settings'] );
}
public function theme_option($setups){
/**
* Add settings submenu page
*/
public function add_settings_page() {
add_submenu_page(
'formipay', // Parent slug
__('Formipay Settings', 'formipay'), // Page title
__('Settings', 'formipay'), // Menu title
'manage_options', // Capability
'formipay-settings', // Menu slug
[$this, 'render_settings_page'] // Callback function
);
}
/**
* Get settings field configuration for React
* Direct method without WPCFTO dependency
*/
public function get_settings_fields() {
$gateways = apply_filters( 'formipay/form-config/tab:payments/gateways', [] );
$payment_checkboxes = [];
@@ -268,40 +292,32 @@ class Settings {
$pages_fields = apply_filters( 'formipay/global-settings/tab:pages', $pages_fields );
$global = array(
'General' => array(
$tabs_config = [
'General' => [
'name' => __( 'General', 'formipay' ),
'fields' => $general_fields
),
'Pages' => array(
],
'Pages' => [
'name' => __( 'Pages', 'formipay' ),
'fields' => $pages_fields
)
);
]
];
$global = apply_filters( 'formipay/global-settings', $global );
// Allow other modules to add/modify tabs
$tabs_config = apply_filters( 'formipay/global-settings', $tabs_config );
foreach($global as $key => $value){
$fields[$key] = $value;
}
return $tabs_config;
}
$setups[] = array(
'option_name' => 'formipay_settings',
'title' => __('Formipay', 'formipay'),
'sub_title' => __('Settings', 'formipay'),
'logo' => FORMIPAY_URL . 'admin/assets/img/formipay-logo-circle-white.png',
'page' => array(
'parent_slug' => 'formipay',
'page_title' => __('Formipay Settings', 'formipay'),
'menu_title' => __('Settings', 'formipay'),
'menu_slug' => 'formipay-settings',
'position' => 40,
),
'fields' => $fields
);
return $setups;
/**
* Render settings page (empty container for React)
*/
public function render_settings_page() {
?>
<div class="wrap">
<div id="formipay-settings-page-container"></div>
</div>
<?php
}
public function enqueue() {
@@ -309,18 +325,82 @@ class Settings {
global $current_screen;
if ( $current_screen->id === 'formipay_page_formipay-settings' ) {
wp_enqueue_style('admin-setting-style', FORMIPAY_URL . 'admin/assets/css/global-setting.css', [], FORMIPAY_VERSION, 'all' );
wp_enqueue_script('admin-setting-script', FORMIPAY_URL . 'admin/assets/js/admin-setting.js', ['jquery'], FORMIPAY_VERSION, true);
// Enqueue React admin assets for FieldRenderer
wp_enqueue_script( 'formipay-admin', FORMIPAY_URL . 'build/admin.js', ['react', 'react-dom'], FORMIPAY_VERSION, true );
wp_enqueue_style( 'formipay-admin', FORMIPAY_URL . 'build/admin.css', [], FORMIPAY_VERSION, 'all' );
}
}
wp_localize_script( 'admin-setting-script', 'formipay_admin_setting', [
'ajax_url' => admin_url('admin-ajax.php'),
'site_url' => site_url(),
'nonce' => wp_create_nonce('formipay-admin-nonce'),
'multicurrency' => formipay_is_multi_currency_active(),
'all_currencies' => formipay_currency_as_options(),
'global_selected_currencies' => formipay_global_currency_options(),
'default_currency' => formipay_default_currency()
] );
/**
* Render React settings template in admin footer
* This outputs the config JSON and mount point for the React FieldRenderer
*/
public function render_react_settings_template() {
global $current_screen;
// Only render on settings page
if ( $current_screen->id !== 'formipay_page_formipay-settings' ) {
return;
}
// Get the configuration for settings
$config = FieldConfigBridge::get_config_for_settings('formipay_settings');
$config_json = wp_json_encode($config);
?>
<div id="formipay-settings-react" data-formipay-settings-config="<?php echo esc_attr($config_json); ?>"></div>
<script>
// Move the React mount point to our page container
jQuery(document).ready(function($) {
$('#formipay-settings-react').appendTo('#formipay-settings-page-container');
});
</script>
<?php
}
/**
* AJAX handler for saving settings from React
*/
public function ajax_save_settings() {
// Verify nonce
check_ajax_referer( 'formipay-field-config', 'nonce', true );
// Check permissions
if (!current_user_can('manage_options')) {
wp_send_json_error([
'message' => __( 'You do not have permission to save settings.', 'formipay' )
]);
}
// Get settings data
$settings = isset($_POST['settings']) ? json_decode(sanitize_text_field(wp_unslash($_POST['settings'])), true) : [];
if (empty($settings)) {
wp_send_json_error([
'message' => __( 'No settings data received.', 'formipay' )
]);
}
// Remove nonce from settings before saving
unset($settings['nonce']);
// Get existing settings
$existing_settings = get_option('formipay_settings', []);
// Merge with existing settings to preserve values not in current form
$updated_settings = array_merge($existing_settings, $settings);
// Update option
$result = update_option('formipay_settings', $updated_settings);
if ($result) {
wp_send_json_success([
'message' => __( 'Settings saved successfully.', 'formipay' )
]);
} else {
wp_send_json_error([
'message' => __( 'Failed to save settings.', 'formipay' )
]);
}
}

Some files were not shown because too many files have changed in this diff Show More