feat: implement coexistence strategy for Grid.js and React admin

Implement dual-mode rendering allowing classic Grid.js and new React
versions to run side-by-side during migration.

- Add coexistence mode checks to all admin page methods
- Check query param ?react=1 or option 'formipay_use_react_admin'
- Include classic PHP pages when React not active
- Add admin notice showing current version with toggle button
- Add footer toggle link to switch between versions

This ensures zero feature loss - old Grid.js pages continue working
(~20 features per page) while React versions are developed.

Files:
- Form.php, Coupon.php, Access.php, Order.php
- Customer.php, Product.php, License.php
- ReactAdmin.php (added version_notice, footer_toggle)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 16:55:56 +07:00
parent ab69d03f78
commit bd9cdac02e
12 changed files with 570 additions and 275 deletions

View File

@@ -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' ] );

View File

@@ -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(
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
esc_attr( $page )
'<div id="formipay-admin-root" data-formipay-mount="%s">Loading %s...</div>',
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(
'<div class="notice notice-info inline">
<p>
<strong>Formipay Admin:</strong> Using %s version.
<a href="%s" class="button button-small" style="margin-left: 10px;">Switch to %s</a>
</p>
</div>',
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 | <a href="%s">%s</a>',
$text,
esc_url( $toggle_url ),
esc_html( $toggle_text )
);
}

View File

@@ -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' ] );

View File

@@ -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' ] );

View File

@@ -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' ] );

View File

@@ -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' ] );

View File

@@ -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(
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
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(
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
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' ] );

View File

@@ -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) {