From f7a149a1c59fd85c38dcc24a66ecc9f5fa0309e6 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sat, 18 Apr 2026 11:17:53 +0700 Subject: [PATCH] feat: initialize React admin build pipeline (F2.1-F2.6) - Add package.json with @wordpress/scripts and React dependencies - Configure webpack for admin bundle output - Create src/admin directory structure (api, components, pages) - Implement API client with nonce handling (ajaxRequest, apiRequest) - Add API methods for orders, customers, products, forms, coupons, licenses - Create React App component with page routing - Add placeholder page components for all admin sections - Create ReactAdmin PHP class to manage asset enqueuing - Register ReactAdmin singleton in main plugin file - Bump version to 2.0.0 Build: Run 'npm install && npm run build' to generate assets --- formipay.php | 5 +- includes/Admin/ReactAdmin.php | 137 ++++++++++++++++++++++++++++++++++ package.json | 31 ++++++++ src/admin/api/client.js | 128 +++++++++++++++++++++++++++++++ src/admin/components/App.js | 39 ++++++++++ src/admin/index.js | 28 +++++++ src/admin/pages/Access.js | 14 ++++ src/admin/pages/Coupons.js | 14 ++++ src/admin/pages/Customers.js | 14 ++++ src/admin/pages/Forms.js | 14 ++++ src/admin/pages/Licenses.js | 14 ++++ src/admin/pages/Orders.js | 14 ++++ src/admin/pages/Products.js | 14 ++++ webpack.config.js | 14 ++++ 14 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 includes/Admin/ReactAdmin.php create mode 100644 package.json create mode 100644 src/admin/api/client.js create mode 100644 src/admin/components/App.js create mode 100644 src/admin/index.js create mode 100644 src/admin/pages/Access.js create mode 100644 src/admin/pages/Coupons.js create mode 100644 src/admin/pages/Customers.js create mode 100644 src/admin/pages/Forms.js create mode 100644 src/admin/pages/Licenses.js create mode 100644 src/admin/pages/Orders.js create mode 100644 src/admin/pages/Products.js create mode 100644 webpack.config.js diff --git a/formipay.php b/formipay.php index 1a1558adc..f7cd3d2f4 100644 --- a/formipay.php +++ b/formipay.php @@ -2,7 +2,7 @@ /** * Plugin Name: Formipay * Description: - - * Version: 1.0.0 + * Version: 2.0.0 * Plugin URI: https://formipay.com/ * Author: Formipay * Text Domain: formipay @@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) exit; define( 'FORMIPAY_NAME', 'Formipay' ); -define( 'FORMIPAY_VERSION', '1.0.0' ); +define( 'FORMIPAY_VERSION', '2.0.0' ); define( 'FORMIPAY_PATH', plugin_dir_path( __FILE__ ) ); define( 'FORMIPAY_URL', plugin_dir_url( __FILE__ ) ); define( 'FORMIPAY_MENU_SLUG', 'formipay' ); @@ -49,6 +49,7 @@ spl_autoload_register(function ($class) { }); \Formipay\Init::get_instance(); +\Formipay\Admin\ReactAdmin::get_instance(); register_activation_hook( __FILE__, 'formipay_activate' ); function formipay_activate() { diff --git a/includes/Admin/ReactAdmin.php b/includes/Admin/ReactAdmin.php new file mode 100644 index 000000000..0773ed0ee --- /dev/null +++ b/includes/Admin/ReactAdmin.php @@ -0,0 +1,137 @@ +id, 'formipay' ) === false ) { + return; + } + + // Enqueue React build assets + $build_dir = FORMIPAY_PATH . 'build'; + $build_url = FORMIPAY_URL . 'build'; + + if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) { + return; // Build not generated yet + } + + $assets_file = require $build_dir . '/admin.asset.php'; + $dependencies = $assets_file['dependencies'] ?? []; + $version = $assets_file['version'] ?? FORMIPAY_VERSION; + + wp_enqueue_style( + 'formipay-admin-style', + $build_url . '/style-admin.css', + [], + $version + ); + + wp_enqueue_script( + 'formipay-admin', + $build_url . '/admin.js', + $dependencies, + $version, + true + ); + + // Localize script with required data + $data = apply_filters( 'formipay/admin/data', [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'restUrl' => rest_url( 'formipay/v1' ), + 'nonce' => wp_create_nonce( 'formipay-admin' ), + ] ); + + wp_localize_script( 'formipay-admin', 'formipayAdmin', $data ); + + } + + public function localize_data( $data ) { + + $screen = get_current_screen(); + $page = ''; + + // Determine current page based on screen ID + if ( $screen->id === 'formipay_page_formipay-orders' ) { + $page = 'orders'; + } elseif ( $screen->id === 'formipay_page_formipay-customers' ) { + $page = 'customers'; + } elseif ( $screen->id === 'formipay_page_formipay-products' ) { + $page = 'products'; + } elseif ( $screen->id === 'toplevel_page_formipay' ) { + $page = 'forms'; + } elseif ( $screen->id === 'formipay_page_formipay-coupons' ) { + $page = 'coupons'; + } elseif ( $screen->id === 'formipay_page_formipay-access' ) { + $page = 'access'; + } elseif ( $screen->id === 'formipay_page_formipay-licenses' ) { + $page = 'licenses'; + } + + if ( $page ) { + $data[$page] = $this->get_page_data( $page ); + } + + return $data; + + } + + private function get_page_data( $page ) { + + $data = []; + + switch ( $page ) { + + case 'orders': + $data['statusOptions'] = formipay_order_status_list(); + break; + + case 'customers': + $data['columns'] = [ + 'id' => __( 'ID', 'formipay' ), + 'name' => __( 'Name', 'formipay' ), + 'email' => __( 'Email', 'formipay' ), + 'phone' => __( 'Phone', 'formipay' ), + 'total_order' => __( 'Total Orders', 'formipay' ), + ]; + break; + + case 'products': + $data['currencies'] = formipay_global_currency_options(); + break; + + } + + return $data; + + } + + /** + * Render React mount point + */ + public static function render_mount_point( $page ) { + + printf( + '
', + esc_attr( $page ) + ); + + } + +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..90904c975 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "formipay", + "version": "2.0.0", + "description": "WordPress payment form plugin with React admin interface", + "author": "Formipay", + "license": "GPL-2.0-or-later", + "scripts": { + "build": "wp-scripts build", + "start": "wp-scripts start", + "format": "wp-scripts format", + "lint:js": "wp-scripts lint-js", + "lint:js:fix": "wp-scripts lint-js --fix", + "lint:css": "wp-scripts lint-style", + "lint:css:fix": "wp-scripts lint-style --fix", + "packages-update": "wp-scripts packages-update" + }, + "devDependencies": { + "@wordpress/scripts": "^27.0.0" + }, + "dependencies": { + "@wordpress/api-fetch": "^6.0.0", + "@wordpress/components": "^25.0.0", + "@wordpress/data": "^9.0.0", + "@wordpress/date": "^4.0.0", + "@wordpress/element": "^5.0.0", + "@wordpress/i18n": "^4.0.0", + "@wordpress/icons": "^9.0.0", + "@wordpress/url": "^8.0.0", + "@tanstack/react-table": "^8.11.0" + } +} diff --git a/src/admin/api/client.js b/src/admin/api/client.js new file mode 100644 index 000000000..370c9577b --- /dev/null +++ b/src/admin/api/client.js @@ -0,0 +1,128 @@ +/** + * API client for Formipay admin interface + * Handles nonce-secured requests to WordPress admin-ajax.php + */ + +export const API_BASE = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'; +export const NONCE = window.formipayAdmin?.nonce || ''; +export const REST_BASE = window.formipayAdmin?.restUrl || '/wp-json/formipay/v1'; + +/** + * Generic AJAX request handler with nonce + */ +export async function ajaxRequest(action, data = {}) { + const formData = new FormData(); + + formData.append('action', action); + formData.append('_wpnonce', NONCE); + + Object.keys(data).forEach(key => { + if (typeof data[key] === 'object') { + formData.append(key, JSON.stringify(data[key])); + } else { + formData.append(key, data[key]); + } + }); + + try { + const response = await fetch(API_BASE, { + method: 'POST', + credentials: 'same-origin', + body: formData, + }); + + const result = await response.json(); + + if (!response.ok || result.success === false) { + throw new Error(result.message || 'Request failed'); + } + + return result; + } catch (error) { + console.error(`AJAX error [${action}]:`, error); + throw error; + } +} + +/** + * REST API request handler + */ +export async function apiRequest(endpoint, options = {}) { + const url = `${REST_BASE}${endpoint}`; + + const defaultOptions = { + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': NONCE, + }, + }; + + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.message || 'API request failed'); + } + + return result; + } catch (error) { + console.error(`API error [${endpoint}]:`, error); + throw error; + } +} + +/** + * Orders API + */ +export const ordersApi = { + list: (params = {}) => ajaxRequest('formipay_tabledata_orders', params), + get: (orderId) => ajaxRequest('formipay_get_order', { order_id: orderId }), + updateStatus: (orderId, status) => ajaxRequest('formipay_update_order_status', { + order_id: orderId, + status, + }), + delete: (orderIds) => ajaxRequest('formipay_bulk_delete_order', { ids: orderIds }), +}; + +/** + * Customers API + */ +export const customersApi = { + list: (params = {}) => ajaxRequest('formipay_tabledata_customers', params), + get: (customerId) => ajaxRequest('formipay_get_customer', { customer_id: customerId }), +}; + +/** + * Products API + */ +export const productsApi = { + list: (params = {}) => ajaxRequest('formipay_tabledata_products', params), + get: (productId) => ajaxRequest('formipay_get_product', { post_id: productId }), + getVariables: (productId) => ajaxRequest('get_product_variables', { post_id: productId }), +}; + +/** + * Forms API + */ +export const formsApi = { + list: (params = {}) => ajaxRequest('formipay_tabledata_forms', params), + get: (formId) => ajaxRequest('formipay_get_form', { post_id: formId }), +}; + +/** + * Coupons API + */ +export const couponsApi = { + list: (params = {}) => ajaxRequest('formipay_tabledata_coupons', params), + check: (code, formId) => ajaxRequest('formipay_check_coupon', { code, form_id: formId }), +}; + +/** + * License API + */ +export const licenseApi = { + verify: (key) => ajaxRequest('formipay_verify_license', { license_key: key }), + activate: (key) => ajaxRequest('formipay_activate_license', { license_key: key }), + deactivate: (key) => ajaxRequest('formipay_deactivate_license', { license_key: key }), +}; diff --git a/src/admin/components/App.js b/src/admin/components/App.js new file mode 100644 index 000000000..f5a2255c0 --- /dev/null +++ b/src/admin/components/App.js @@ -0,0 +1,39 @@ +/** + * App Component - Main admin application shell + */ + +import OrdersPage from '../pages/Orders'; +import CustomersPage from '../pages/Customers'; +import ProductsPage from '../pages/Products'; +import FormsPage from '../pages/Forms'; +import CouponsPage from '../pages/Coupons'; +import AccessPage from '../pages/Access'; +import LicensesPage from '../pages/Licenses'; + +const pageComponents = { + orders: OrdersPage, + customers: CustomersPage, + products: ProductsPage, + forms: FormsPage, + coupons: CouponsPage, + access: AccessPage, + licenses: LicensesPage, +}; + +export default function App({ page, initialData }) { + const PageComponent = pageComponents[page]; + + if (!PageComponent) { + return ( +
+

Unknown page: {page}

+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/admin/index.js b/src/admin/index.js new file mode 100644 index 000000000..17723fbdb --- /dev/null +++ b/src/admin/index.js @@ -0,0 +1,28 @@ +/** + * Formipay Admin - React Application Entry Point + */ + +import { render } from '@wordpress/element'; +import App from './components/App'; + +// Mount the React app to all available mount points +const mountApps = () => { + const mountPoints = document.querySelectorAll('[data-formipay-mount]'); + + mountPoints.forEach((mountPoint) => { + const page = mountPoint.dataset.formipayMount; + const initialData = window.formipayAdmin?.[page] || {}; + + render( + , + mountPoint + ); + }); +}; + +// Initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', mountApps); +} else { + mountApps(); +} diff --git a/src/admin/pages/Access.js b/src/admin/pages/Access.js new file mode 100644 index 000000000..668f38fdc --- /dev/null +++ b/src/admin/pages/Access.js @@ -0,0 +1,14 @@ +/** + * Access Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function AccessPage({ initialData }) { + return ( +
+

{ __('Access', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Coupons.js b/src/admin/pages/Coupons.js new file mode 100644 index 000000000..b0ff75a40 --- /dev/null +++ b/src/admin/pages/Coupons.js @@ -0,0 +1,14 @@ +/** + * Coupons Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function CouponsPage({ initialData }) { + return ( +
+

{ __('Coupons', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Customers.js b/src/admin/pages/Customers.js new file mode 100644 index 000000000..1b9e1598c --- /dev/null +++ b/src/admin/pages/Customers.js @@ -0,0 +1,14 @@ +/** + * Customers Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function CustomersPage({ initialData }) { + return ( +
+

{ __('Customers', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Forms.js b/src/admin/pages/Forms.js new file mode 100644 index 000000000..1928e7f72 --- /dev/null +++ b/src/admin/pages/Forms.js @@ -0,0 +1,14 @@ +/** + * Forms Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function FormsPage({ initialData }) { + return ( +
+

{ __('Forms', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Licenses.js b/src/admin/pages/Licenses.js new file mode 100644 index 000000000..fac1748c4 --- /dev/null +++ b/src/admin/pages/Licenses.js @@ -0,0 +1,14 @@ +/** + * Licenses Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function LicensesPage({ initialData }) { + return ( +
+

{ __('Licenses', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Orders.js b/src/admin/pages/Orders.js new file mode 100644 index 000000000..77b5eccfc --- /dev/null +++ b/src/admin/pages/Orders.js @@ -0,0 +1,14 @@ +/** + * Orders Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function OrdersPage({ initialData }) { + return ( +
+

{ __('Orders', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/src/admin/pages/Products.js b/src/admin/pages/Products.js new file mode 100644 index 000000000..08c06eb0a --- /dev/null +++ b/src/admin/pages/Products.js @@ -0,0 +1,14 @@ +/** + * Products Page - Placeholder + */ + +import { __ } from '@wordpress/i18n'; + +export default function ProductsPage({ initialData }) { + return ( +
+

{ __('Products', 'formipay') }

+

{ __('Page content coming soon...', 'formipay') }

+
+ ); +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..80d5c5041 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,14 @@ +const defaultConfig = require('@wordpress/scripts/config/webpack.config'); +const path = require('path'); + +module.exports = { + ...defaultConfig, + entry: { + 'admin': './src/admin/index.js', + }, + output: { + path: path.resolve(__dirname, 'build'), + filename: '[name].js', + chunkFilename: '[name].[contenthash].js', + }, +};