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( + '
+

+ Formipay Admin: Using %s version. + Switch to %s +

+
', + 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 ( +
+

{ emptyMessage }

+
+ ); + } + + return ( +
+ + + {columns.map((column) => ( + + ))} + + + + {data.map((row, rowIndex) => ( + onRowClick(row) : undefined} + className={onRowClick ? 'is-clickable' : ''} + > + {columns.map((column) => ( + + ))} + + ))} + +
{column.label}
+ {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; +}