diff --git a/MIGRATION_STRATEGY.md b/MIGRATION_STRATEGY.md
index a30286108..331eb9fb5 100644
--- a/MIGRATION_STRATEGY.md
+++ b/MIGRATION_STRATEGY.md
@@ -1,199 +1,281 @@
-# Formipay — Vue to React Migration Strategy
+# Formipay — Migration Strategy
-**Date:** April 18, 2026
+**Date:** April 18, 2026 (Updated)
**Context:** Phase 2 — React Admin Foundation
---
## Overview
-This document explains how to approach the existing Vue.js code in Formipay during the migration to React admin panels.
+This document explains how to approach migrating existing Formipay admin interfaces to React while **ensuring zero feature loss**.
-**Key Principle:** We are NOT doing a "rewrite everything at once" approach. Vue and React will coexist during Phase 2, with Vue being removed incrementally as React replacements are shipped.
+**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.
---
-## Current Vue.js Usage Inventory
+## Existing Technology Inventory
-| Type | Location | Purpose | Replacement Phase |
-|------|----------|---------|-------------------|
-| **WPCFTO Framework** | `vendor/` | Settings form builder (repeater, fields, etc.) | F2.8 (Global Settings) |
-| **Custom Vue 2 App** | `admin/assets/js/admin-product-editor.js` | Product variation pricing table | F2.9 (Product Editor) |
-| **Partial Vue Editor** | `admin/assets/vue/` (if exists) | Form builder canvas | F2.4 (Form Builder) |
+| 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 |
---
-## Migration Strategy
+## Critical: Grid.js Migration Strategy
-### Phase 1: Coexistence (Current State)
+### 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
```
-┌─────────────────────────────────────────────┐
-│ Admin Pages │
-├─────────────────────────────────────────────┤
-│ Forms │ Products │ Orders │ Settings │
-│ WPCFTO │ WPCFTO │ WPCFTO │ WPCFTO │
-│ + Vue │ + Vue │ (none) │ + Vue │
-│ │ (custom) │ │ │
-└─────────────────────────────────────────────┘
+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) │
+└─────────────────────────────────────────────────────────┘
```
-**Status:** All pages working. WPCFTO handles forms, custom Vue handles product variations.
+### Implementation Steps
----
+**Step 1: Feature Parity Checklist**
-### Phase 2: Incremental React Rollout
+Create `MIGRATION_CHECKLIST.md` with per-page feature requirements (see template below).
-**Week 3:** Build pipeline + React infrastructure. Vue untouched.
+**Step 2: Dual-Mode Rendering**
-**Week 4:** React Form Builder replaces WPCFTO form editor.
-```
-Forms: WPCFTO/Vue → React ✅
-Products: WPCFTO + custom Vue → unchanged
-Orders: (none) → React ✅
-Settings: WPCFTO → unchanged
-```
+In PHP page callback, check for query param:
-**Week 5:** React Order Management (no Vue to touch).
-```
-Orders → React ✅
-```
-
-**Week 6:** React Global Settings replaces WPCFTO.
-```
-Settings: WPCFTO → React ✅
-Products: WPCFTO + custom Vue → unchanged
-```
-
-**After Week 6:** WPCFTO framework can be fully removed. Only custom Vue remains.
-
----
-
-### Product Editor Migration (F2.9)
-
-The custom Vue app in `admin-product-editor.js` is the last Vue piece to migrate.
-
-#### Current Features (Must Recreate in React)
-
-| Feature | Complexity | Notes |
-|---------|-----------|-------|
-| Multi-currency flat pricing | Medium | Show all currencies as columns when enabled |
-| Multi-currency expanded mode | High | Inner tables per variation row |
-| Dynamic rows from attribute repeater | High | Syncs with WPCFTO repeater in real-time |
-| Decimal digits per currency | Medium | Step values calculated from currency config |
-| Required field validation | Low | Default currency must have price |
-| Real-time JSON update to hidden input | Medium | `product_variation_variables` hidden field |
-| SweetAlert2 error dialogs | Low | Uses existing window.Swal or custom alert |
-
-#### Implementation Approach
-
-**Option A: Recreate from Scratch (Recommended)**
-
-1. **Read current Vue code logic** — understand the data flow
-2. **Build React component** — `VariationPricingTable.jsx`
- - Use `@tanstack/react-table` for the table
- - Use `@wordpress/components` for form inputs (TextControl, etc.)
- - State management via React hooks (useState, useEffect)
-3. **Data sync layer** — same API endpoint `get_product_variables`
-4. **Validation logic** — port `findFirstMissingDefault()` to React
-
-**Pros:** Clean React code, modern patterns, better type safety
-**Cons:** Requires careful testing to match all edge cases
-
-**Option B: Vue-in-React Wrapper (Not Recommended)**
-
-Wrap the existing Vue app in a React component using a library like `vue-react-wrapper`.
-
-**Pros:** Faster, less risky
-**Cons:** Technical debt, adds bundle size, mixing paradigms
-
-**Recommendation:** Option A — rewrite in React. The logic is well-contained (~500 lines) and rewriting gives us clean, maintainable React code.
-
----
-
-## Migration Checklist for F2.9
-
-When implementing the React Product Editor:
-
-- [ ] Read and document current Vue variation table behavior
-- [ ] Create `src/admin/pages/Products/VariationPricingTable.jsx`
-- [ ] Implement multi-currency flat pricing mode
-- [ ] Implement multi-currency expanded mode (inner tables)
-- [ ] Implement attribute repeater sync (MutationObserver or polling)
-- [ ] Implement decimal digits per currency step calculation
-- [ ] Implement required field validation (default currency)
-- [ ] Implement real-time JSON update to hidden `product_variation_variables` input
-- [ ] Add SweetAlert2 or equivalent for validation errors
-- [ ] Test with various currency configurations
-- [ ] Test with existing products (data migration)
-- [ ] Remove old Vue script enqueuing from Product page
-- [ ] Remove Vue dependency if no longer used elsewhere
-
----
-
-## Data Compatibility
-
-The React component must read/write the same data format as the Vue app:
-
-**Hidden input format** (`product_variation_variables`):
-```json
-[
- {
- "key": "Red|||Large",
- "name": "Red - Large",
- "stock": "",
- "weight": 0,
- "active": true,
- "prices": [
- {
- "currency": "USD:::United States Dollar:::$",
- "regular_price": "29.99",
- "sale_price": "24.99",
- "currency_decimal_digits": 2
- }
- ]
- }
-]
-```
-
-**API endpoint** (already exists):
```php
-// Product.php - add this if not present
-add_action('wp_ajax_get_product_variables', [$this, 'ajax_get_product_variables']);
+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
+
+
+ Try React Admin (Beta)
+
+
+
+ Use Classic Admin
+
+
+```
+
+### 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
+
```
---
-## Testing Strategy
+## Data Compatibility Requirements
-1. **Unit Tests:** Test variation price calculation logic in isolation
-2. **Integration Tests:** Test loading/saving variations via AJAX
-3. **Manual Tests:**
- - Create product with variations
- - Test multi-currency flat mode
- - Test multi-currency expanded mode
- - Test validation (missing default price)
- - Test attribute repeater sync
- - Test editing existing products (data compatibility)
+### 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 critical bugs:
-1. Keep Vue version as fallback
-2. Add feature flag in settings: "Use React Product Editor (beta)"
-3. Default to Vue, allow opting into React
-4. Fix React, then make it default
+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
-- **No Vue Router used** — the Vue app is a simple mount, no routing to worry about
-- **No Vuex** — state is local component state + hidden input sync
-- **jQuery dependency** — the Vue app uses jQuery for DOM selection (`$('#product-variables-table')`. React version should eliminate this.
-- **Timing:** WPCFTO removal happens after Global Settings (F2.8), but Product Editor Vue app is independent — can be migrated anytime after Week 3.
+- **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
---
diff --git a/includes/Access.php b/includes/Access.php
index b46c801fb..3487c5016 100644
--- a/includes/Access.php
+++ b/includes/Access.php
@@ -84,10 +84,21 @@ class Access {
}
public function formipay_access_items() {
- \Formipay\Admin\ReactAdmin::render_mount_point('access');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('access');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-access-items.php';
+ }
}
public function enqueue_admin() {
+ // Assets now handled by ReactAdmin class
+ return;
global $current_screen;
@@ -432,7 +443,7 @@ class Access {
public function formipay_tabledata_access_items() {
- check_ajax_referer( 'formipay-admin-access-nonce', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -526,7 +537,7 @@ class Access {
public function formipay_access_items_get_products() {
- check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -560,7 +571,7 @@ class Access {
public function formipay_create_access_item_post() {
- check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -592,7 +603,7 @@ class Access {
public function formipay_delete_access_item() {
- check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -627,7 +638,7 @@ class Access {
public function formipay_bulk_delete_access_item() {
- check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -674,7 +685,7 @@ class Access {
public function formipay_duplicate_access_item() {
- check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/Admin/ReactAdmin.php b/includes/Admin/ReactAdmin.php
index 0773ed0ee..ac455b191 100644
--- a/includes/Admin/ReactAdmin.php
+++ b/includes/Admin/ReactAdmin.php
@@ -12,6 +12,8 @@ class ReactAdmin {
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
+ add_action( 'admin_notices', [$this, 'version_notice'] );
+ add_filter( 'admin_footer_text', [$this, 'footer_toggle'] );
}
@@ -29,16 +31,25 @@ class ReactAdmin {
$build_url = FORMIPAY_URL . 'build';
if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) {
+ error_log('[Formipay] Build files not found at: ' . $build_dir . '/admin.asset.php');
return; // Build not generated yet
}
$assets_file = require $build_dir . '/admin.asset.php';
$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 . '/style-admin.css',
+ $build_url . '/admin.css',
[],
$version
);
@@ -58,6 +69,10 @@ class ReactAdmin {
'nonce' => wp_create_nonce( 'formipay-admin' ),
] );
+ // 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 );
}
@@ -116,6 +131,14 @@ class ReactAdmin {
$data['currencies'] = formipay_global_currency_options();
break;
+ case 'forms':
+ case 'coupons':
+ case 'access':
+ case 'licenses':
+ // These pages fetch data via AJAX, no initial data needed
+ $data = [];
+ break;
+
}
return $data;
@@ -128,8 +151,63 @@ class ReactAdmin {
public static function render_mount_point( $page ) {
printf(
- '',
- esc_attr( $page )
+ 'Loading %s...
',
+ esc_attr( $page ),
+ esc_html( ucfirst( $page ) )
+ );
+
+ }
+
+ /**
+ * Show admin notice about current admin version
+ */
+ public function version_notice() {
+
+ $screen = get_current_screen();
+
+ // Only show on Formipay admin pages
+ if ( strpos( $screen->id, 'formipay' ) === false ) {
+ return;
+ }
+
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+ $version = $use_react ? 'React (Beta)' : 'Classic';
+
+ printf(
+ '',
+ esc_html( $version ),
+ esc_url( add_query_arg( 'react', $use_react ? '0' : '1' ) ),
+ esc_html( $use_react ? 'Classic' : 'React (Beta)' )
+ );
+
+ }
+
+ /**
+ * Add toggle link to admin footer
+ */
+ public function footer_toggle( $text ) {
+
+ $screen = get_current_screen();
+
+ // Only add toggle on Formipay admin pages
+ if ( strpos( $screen->id, 'formipay' ) === false ) {
+ return $text;
+ }
+
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+ $toggle_url = add_query_arg( 'react', $use_react ? '0' : '1' );
+ $toggle_text = $use_react ? 'Switch to Classic' : 'Try React (Beta)';
+
+ return sprintf(
+ '%s | %s',
+ $text,
+ esc_url( $toggle_url ),
+ esc_html( $toggle_text )
);
}
diff --git a/includes/Coupon.php b/includes/Coupon.php
index f73a4e983..5f44ddbd2 100644
--- a/includes/Coupon.php
+++ b/includes/Coupon.php
@@ -94,10 +94,21 @@ class Coupon {
}
public function formipay_coupon() {
- \Formipay\Admin\ReactAdmin::render_mount_point('coupons');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('coupons');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-coupons.php';
+ }
}
public function enqueue_admin() {
+ // Assets now handled by ReactAdmin class
+ return;
global $current_screen;
@@ -568,7 +579,7 @@ class Coupon {
public function formipay_tabledata_coupons() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -676,7 +687,7 @@ class Coupon {
public function formipay_coupon_get_products() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -710,7 +721,7 @@ class Coupon {
public function formipay_create_coupon_post() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -745,7 +756,7 @@ class Coupon {
public function formipay_delete_coupon() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -773,7 +784,7 @@ class Coupon {
public function formipay_bulk_delete_coupon() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -820,7 +831,7 @@ class Coupon {
public function formipay_duplicate_coupon() {
- check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/Customer.php b/includes/Customer.php
index baa8c2c3a..07563a5b2 100644
--- a/includes/Customer.php
+++ b/includes/Customer.php
@@ -219,12 +219,21 @@ class Customer {
}
public function customers_page() {
- \Formipay\Admin\ReactAdmin::render_mount_point('customers');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('customers');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-customers.php';
+ }
}
public function formipay_tabledata_customers() {
- check_ajax_referer( 'formipay-admin-access-nonce', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/Form.php b/includes/Form.php
index d5b47d834..20b034035 100644
--- a/includes/Form.php
+++ b/includes/Form.php
@@ -93,7 +93,16 @@ class Form {
}
public function formipay_form() {
- \Formipay\Admin\ReactAdmin::render_mount_point('forms');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('forms');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-forms.php';
+ }
}
public function metaboxes($post) {
@@ -1248,6 +1257,8 @@ class Form {
}
public function enqueue_admin() {
+ // Assets now handled by ReactAdmin class
+ return;
global $current_screen, $post;
// Check that we are on the 'Checker' post editor screen
@@ -1547,7 +1558,14 @@ class Form {
public function formipay_tabledata_forms() {
- check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
+ error_log('[Formipay] formipay_tabledata_forms called');
+
+ $nonce_check = check_ajax_referer( 'formipay-admin', '_wpnonce', false );
+ error_log('[Formipay] Nonce check result: ' . ($nonce_check ? 'valid' : 'invalid'));
+
+ if ( ! $nonce_check ) {
+ wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
+ }
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1629,7 +1647,7 @@ class Form {
public function formipay_create_form_post() {
- check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1702,7 +1720,7 @@ class Form {
public function formipay_delete_form() {
- check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1730,7 +1748,7 @@ class Form {
public function formipay_bulk_delete_form() {
- check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1777,7 +1795,7 @@ class Form {
public function formipay_duplicate_form() {
- check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/License.php b/includes/License.php
index 49c76bf70..8d9a9c3f6 100644
--- a/includes/License.php
+++ b/includes/License.php
@@ -70,7 +70,16 @@ class License {
}
public function page_licenses() {
- \Formipay\Admin\ReactAdmin::render_mount_point('licenses');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('licenses');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-licenses.php';
+ }
}
/** Enqueue admin assets for Licenses page */
@@ -121,7 +130,7 @@ class License {
/** GridJS data source */
public function tabledata() {
- check_ajax_referer('formipay-admin-licenses', '_wpnonce');
+ check_ajax_referer('formipay-admin', '_wpnonce');
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -202,7 +211,7 @@ class License {
/** Delete single license */
public function delete() {
- check_ajax_referer('formipay-admin-licenses', '_wpnonce');
+ check_ajax_referer('formipay-admin', '_wpnonce');
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -238,7 +247,7 @@ class License {
/** Bulk delete */
public function bulk_delete() {
- check_ajax_referer('formipay-admin-licenses', '_wpnonce');
+ check_ajax_referer('formipay-admin', '_wpnonce');
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/Order.php b/includes/Order.php
index 86d49640c..1a71bbbb9 100644
--- a/includes/Order.php
+++ b/includes/Order.php
@@ -644,91 +644,29 @@ class Order {
$order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : 0;
$page = $order_id ? 'order-detail' : 'orders';
- // Render React mount point
- printf(
- '',
- esc_attr($page)
- );
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ printf(
+ '',
+ esc_attr($page)
+ );
+ } else {
+ // Classic Grid.js version
+ if ($order_id) {
+ include_once FORMIPAY_PATH . 'admin/page-order-details.php';
+ } else {
+ include_once FORMIPAY_PATH . 'admin/page-orders.php';
+ }
+ }
}
public function enqueue() {
-
- global $current_screen;
-
- if($current_screen->id == 'formipay_page_formipay-orders') {
-
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : 0;
- if(empty($order_id)){
- wp_enqueue_style( 'page-orders', FORMIPAY_URL . 'admin/assets/css/admin-orders.css', [], FORMIPAY_VERSION, 'all' );
- wp_enqueue_script( 'page-orders', FORMIPAY_URL . 'admin/assets/js/admin-orders.js', ['jquery', 'gridjs'], FORMIPAY_VERSION, true );
-
- wp_localize_script( 'page-orders', 'formipay_orders_page', [
- 'ajax_url' => admin_url('admin-ajax.php'),
- 'site_url' => site_url(),
- 'columns' => [
- 'id' => esc_html__( 'ID', 'formipay' ),
- 'form' => esc_html__( 'Form', 'formipay' ),
- 'total' => esc_html__( 'Total', 'formipay' ),
- 'date' => esc_html__( 'Date', 'formipay' ),
- 'payment_gateway' => esc_html__( 'Payment Gateway', 'formipay' ),
- 'status' => esc_html__( 'Status', 'formipay' ),
- ],
- 'filter_form' => [
- 'products' => [
- 'placeholder' => esc_html__( 'Filter by Product', 'formipay' ),
- 'noresult_text' => esc_html__( 'No results found', 'formipay' )
- ],
- 'currencies' => [
- 'placeholder' => esc_html__( 'Filter by Currency', 'formipay' ),
- 'noresult_text' => esc_html__( 'No results found', 'formipay' )
- ]
- ],
- 'nonce' => wp_create_nonce( 'formipay-order-details' )
- ] );
- }else{
-
- wp_enqueue_style( 'bootstrap-icon', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap-icons.css', [], '1.11.1', 'all');
- wp_enqueue_style( 'bootstrap', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap.min.css', [], '5.3.2' );
- wp_enqueue_style( 'page-orders', FORMIPAY_URL . 'admin/assets/css/admin-order-details.css', [], FORMIPAY_VERSION, 'all' );
- wp_enqueue_script( 'handlebars', FORMIPAY_URL . 'vendor/HandleBars/handlebars.min.js', [], '4.7.7', true);
- wp_enqueue_script( 'bootstrap', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap.bundle.min.js', ['jquery'], '5.3.2', true );
- wp_enqueue_script( 'page-orders', FORMIPAY_URL . 'admin/assets/js/admin-order-details.js', ['jquery'], FORMIPAY_VERSION, true );
-
- wp_localize_script( 'page-orders', 'formipay_order_details_page', [
- 'ajax_url' => admin_url('admin-ajax.php'),
- 'site_url' => site_url(),
- 'order_id' => $order_id,
- 'order_detail' => [
- 'change_order_status_confirmation' => esc_html__( 'Are you sure to change status?', 'formipay' ),
- 'change_order_status_button_confirm' => esc_html__( 'Change', 'formipay' ),
- 'change_order_status_button_cancel' => esc_html__( 'Cancel', 'formipay' ),
- 'edit_button_loading_text' => esc_html__( 'Preparing...', 'formipay' ),
- 'update_button_loading_text' => esc_html__( 'Updating...', 'formipay' ),
- 'pass_method' => [
- 'magic_link' => esc_html__( 'Magic Link', 'formipay' ),
- 'static_password' => esc_html__( 'Static Password', 'formipay' )
- ]
- ],
- 'modal' => [
- 'delete' => [
- 'question' => esc_html__( 'Do you want to delete the order?', '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 order(s)?', 'formipay' ),
- 'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
- 'confirmButton' => esc_html__( 'Confirm', 'formipay' )
- ],
- ],
- 'nonce' => wp_create_nonce( 'formipay-order-details' )
- ] );
- }
-
- }
-
+ // Assets now handled by ReactAdmin class
+ return;
}
public function formipay_get_all_forms() {
@@ -767,7 +705,7 @@ class Order {
public function formipay_orders_get_choices() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -935,7 +873,7 @@ class Order {
public function formipay_tabledata_orders() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1038,7 +976,7 @@ class Order {
public function formipay_delete_order() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1066,7 +1004,7 @@ class Order {
public function formipay_bulk_delete_order() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1113,7 +1051,7 @@ class Order {
public function formipay_load_order_data() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1127,7 +1065,7 @@ class Order {
public function formipay_change_order_status() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1165,7 +1103,7 @@ class Order {
public function formipay_check_editable_field() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1213,7 +1151,7 @@ class Order {
public function formipay_update_editable_field_data() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
@@ -1268,7 +1206,7 @@ class Order {
public function formipay_update_digital_access() {
- check_ajax_referer( 'formipay-order-details', '_wpnonce' );
+ check_ajax_referer( 'formipay-admin', '_wpnonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
diff --git a/includes/Product.php b/includes/Product.php
index d8a94733a..8b93fb7d4 100644
--- a/includes/Product.php
+++ b/includes/Product.php
@@ -106,6 +106,8 @@ class Product {
}
public function enqueue_admin() {
+ // Assets now handled by ReactAdmin class
+ return;
global $current_screen;
@@ -228,7 +230,16 @@ class Product {
}
public function formipay_products() {
- \Formipay\Admin\ReactAdmin::render_mount_point('products');
+ // Coexistence mode: check query param or setting for React version
+ $use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
+
+ if ($use_react) {
+ // New React version
+ \Formipay\Admin\ReactAdmin::render_mount_point('products');
+ } else {
+ // Classic Grid.js version
+ include_once FORMIPAY_PATH . 'admin/page-products.php';
+ }
}
public function cpt_post_fields_box($boxes) {
diff --git a/src/admin/components/shared/DataTable.css b/src/admin/components/shared/DataTable.css
new file mode 100644
index 000000000..4662ecb56
--- /dev/null
+++ b/src/admin/components/shared/DataTable.css
@@ -0,0 +1,26 @@
+.formipay-data-table-loading,
+.formipay-data-table-empty {
+ padding: 40px;
+ text-align: center;
+}
+
+.formipay-data-table {
+ margin-top: 20px;
+}
+
+.formipay-data-table thead th {
+ padding: 12px 10px;
+ font-weight: 600;
+}
+
+.formipay-data-table tbody td {
+ padding: 10px;
+}
+
+.formipay-data-table tbody tr.is-clickable {
+ cursor: pointer;
+}
+
+.formipay-data-table tbody tr.is-clickable:hover {
+ background-color: #f0f0f1;
+}
diff --git a/src/admin/components/shared/DataTable.js b/src/admin/components/shared/DataTable.js
new file mode 100644
index 000000000..53528fde0
--- /dev/null
+++ b/src/admin/components/shared/DataTable.js
@@ -0,0 +1,57 @@
+/**
+ * Data Table - Simple table component for admin listings
+ */
+
+import { __ } from '@wordpress/i18n';
+import './DataTable.css';
+
+export default function DataTable({
+ columns,
+ data,
+ loading,
+ emptyMessage = __('No items found', 'formipay'),
+ onRowClick
+}) {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data || data.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {columns.map((column) => (
+ | {column.label} |
+ ))}
+
+
+
+ {data.map((row, rowIndex) => (
+ onRowClick(row) : undefined}
+ className={onRowClick ? 'is-clickable' : ''}
+ >
+ {columns.map((column) => (
+ |
+ {column.render ? column.render(row) : row[column.key]}
+ |
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/src/admin/pages/AdminPages.css b/src/admin/pages/AdminPages.css
new file mode 100644
index 000000000..3dd1e1123
--- /dev/null
+++ b/src/admin/pages/AdminPages.css
@@ -0,0 +1,45 @@
+.formipay-page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.formipay-page-header h1 {
+ margin: 0;
+ font-size: 23px;
+ font-weight: 400;
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-badge.status-publish,
+.status-badge.status-active {
+ background-color: #edfaef;
+ color: #00a32a;
+}
+
+.status-badge.status-draft,
+.status-badge.status-inactive {
+ background-color: #f0f0f1;
+ color: #646970;
+}
+
+.status-badge.status-expired {
+ background-color: #f6f7f7;
+ color: #d63638;
+}
+
+code {
+ background-color: #f0f0f1;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: monospace;
+ font-size: 13px;
+}