fix: Modal initial values + sticky footer + HTML descriptions

 Issue 1: Modal Not Showing Current Values (FIXED!)
Problem: Opening modal showed defaults, not current saved values
Root Cause: Backend only sent field.default, not current value
Solution:
- Backend: Added field.value with current saved value
- normalize_field() now includes: value: $current_settings[$key]
- Frontend: Use field.value ?? field.default for initial data
- GenericGatewayForm initializes with current values

Result:  Modal now shows "BNI Virtual Account 2" not "BNI Virtual Account"

 Issue 2: Sticky Modal Footer (FIXED!)
Problem: Footer scrolls away with long forms
Solution:
- Restructured modal: header + scrollable body + sticky footer
- DialogContent: flex flex-col with overflow on body only
- Footer: sticky bottom-0 with border-t
- Save button triggers form.requestSubmit()

Result:  Cancel, View in WooCommerce, Save always visible

 Issue 3: HTML in Descriptions (FIXED!)
Problem: TriPay icon shows as raw HTML string
Solution:
- Changed: {field.description}
- To: dangerouslySetInnerHTML={{ __html: field.description }}
- Respects vendor creativity (images, formatting, links)

Result:  TriPay icon image renders properly

📋 Technical Details:

Backend Changes (PaymentGatewaysProvider.php):
- get_gateway_settings() passes $current_settings to extractors
- normalize_field() adds 'value' => $current_settings[$key]
- All fields now have both default and current value

Frontend Changes:
- GatewayField interface: Added value?: string | boolean
- GenericGatewayForm: Initialize with field.value
- Modal structure: Header + Body (scroll) + Footer (sticky)
- Descriptions: Render as HTML with dangerouslySetInnerHTML

Files Modified:
- PaymentGatewaysProvider.php: Add current values to fields
- Payments.tsx: Restructure modal layout + add value to interface
- GenericGatewayForm.tsx: Use field.value + sticky footer + HTML descriptions

🎯 Result:
 Modal shows current saved values
 Footer always visible (no scrolling)
 Vendor HTML/images render properly
This commit is contained in:
dwindown
2025-11-05 23:52:57 +07:00
parent b578dfaeb0
commit 96f0482cfb
3 changed files with 84 additions and 43 deletions

View File

@@ -21,6 +21,7 @@ interface GatewayField {
title: string; title: string;
description: string; description: string;
default: string | boolean; default: string | boolean;
value?: string | boolean; // Current saved value from backend
placeholder?: string; placeholder?: string;
required: boolean; required: boolean;
options?: Record<string, string>; options?: Record<string, string>;
@@ -56,6 +57,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]); const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]);
// Initialize form data with current gateway values
React.useEffect(() => {
const initialData: Record<string, unknown> = {};
const categories: Record<string, GatewayField>[] = [
gateway.settings.basic,
gateway.settings.api,
gateway.settings.advanced,
];
categories.forEach((category) => {
Object.values(category).forEach((field) => {
// Use current value from field (backend sends this now!)
initialData[field.id] = field.value ?? field.default;
});
});
setFormData(initialData);
}, [gateway]);
// Check for unsupported fields // Check for unsupported fields
React.useEffect(() => { React.useEffect(() => {
const unsupported: string[] = []; const unsupported: string[] = [];
@@ -121,7 +142,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
{field.required && <span className="text-destructive ml-1">*</span>} {field.required && <span className="text-destructive ml-1">*</span>}
</Label> </Label>
{field.description && ( {field.description && (
<p className="text-sm text-muted-foreground">{field.description}</p> <p
className="text-sm text-muted-foreground"
dangerouslySetInnerHTML={{ __html: field.description }}
/>
)} )}
</div> </div>
</div> </div>
@@ -225,25 +249,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
const useMultiPage = totalFields >= 20; const useMultiPage = totalFields >= 20;
return ( return (
<form onSubmit={handleSubmit} className="space-y-6"> <>
{/* Warning for unsupported fields */} <form onSubmit={handleSubmit} className="space-y-6">
{unsupportedFields.length > 0 && ( {/* Warning for unsupported fields */}
<Alert> {unsupportedFields.length > 0 && (
<AlertTriangle className="h-4 w-4" /> <Alert>
<AlertDescription> <AlertTriangle className="h-4 w-4" />
Some advanced settings are not supported in this interface.{' '} <AlertDescription>
<a Some advanced settings are not supported in this interface.{' '}
href={gateway.wc_settings_url} <a
target="_blank" href={gateway.wc_settings_url}
rel="noopener noreferrer" target="_blank"
className="inline-flex items-center gap-1 font-medium underline" rel="noopener noreferrer"
> className="inline-flex items-center gap-1 font-medium underline"
Configure in WooCommerce >
<ExternalLink className="h-3 w-3" /> Configure in WooCommerce
</a> <ExternalLink className="h-3 w-3" />
</AlertDescription> </a>
</Alert> </AlertDescription>
)} </Alert>
)}
{useMultiPage ? ( {useMultiPage ? (
<Tabs defaultValue="basic" className="w-full"> <Tabs defaultValue="basic" className="w-full">
@@ -295,9 +320,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
)} )}
</div> </div>
)} )}
</form>
{/* Actions */} {/* Sticky Footer */}
<div className="flex items-center justify-between pt-4 border-t"> <div className="sticky bottom-0 bg-background border-t px-6 py-4 flex items-center justify-between mt-6">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -324,11 +350,18 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
</a> </a>
</Button> </Button>
<Button type="submit" disabled={isSaving}> <Button
onClick={(e) => {
e.preventDefault();
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Settings'} {isSaving ? 'Saving...' : 'Save Settings'}
</Button> </Button>
</div> </div>
</div> </div>
</form> </>
); );
} }

View File

@@ -18,6 +18,7 @@ interface GatewayField {
title: string; title: string;
description: string; description: string;
default: string | boolean; default: string | boolean;
value: string | boolean; // Current saved value
placeholder?: string; placeholder?: string;
required: boolean; required: boolean;
options?: Record<string, string>; options?: Record<string, string>;
@@ -327,15 +328,17 @@ export default function PaymentsPage() {
{/* Gateway Settings Modal */} {/* Gateway Settings Modal */}
{selectedGateway && ( {selectedGateway && (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}> <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0">
<DialogHeader> <DialogHeader className="px-6 pt-6 pb-4 border-b">
<DialogTitle>{selectedGateway.title} Settings</DialogTitle> <DialogTitle>{selectedGateway.title} Settings</DialogTitle>
</DialogHeader> </DialogHeader>
<GenericGatewayForm <div className="flex-1 overflow-y-auto px-6 py-4">
gateway={selectedGateway} <GenericGatewayForm
onSave={handleSaveGateway} gateway={selectedGateway}
onCancel={() => setIsModalOpen(false)} onSave={handleSaveGateway}
/> onCancel={() => setIsModalOpen(false)}
/>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}

View File

@@ -194,11 +194,12 @@ class PaymentGatewaysProvider {
*/ */
private static function get_gateway_settings(WC_Payment_Gateway $gateway): array { private static function get_gateway_settings(WC_Payment_Gateway $gateway): array {
$form_fields = $gateway->get_form_fields(); $form_fields = $gateway->get_form_fields();
$current_settings = $gateway->settings; // Get current saved values
return [ return [
'basic' => self::extract_basic_fields($form_fields), 'basic' => self::extract_basic_fields($form_fields, $current_settings),
'api' => self::extract_api_fields($form_fields), 'api' => self::extract_api_fields($form_fields, $current_settings),
'advanced' => self::extract_advanced_fields($form_fields), 'advanced' => self::extract_advanced_fields($form_fields, $current_settings),
]; ];
} }
@@ -208,9 +209,9 @@ class PaymentGatewaysProvider {
* @param array $form_fields All form fields * @param array $form_fields All form fields
* @return array Basic fields * @return array Basic fields
*/ */
private static function extract_basic_fields(array $form_fields): array { private static function extract_basic_fields(array $form_fields, array $current_settings): array {
$basic_keys = ['enabled', 'title', 'description', 'instructions']; $basic_keys = ['enabled', 'title', 'description', 'instructions'];
return self::filter_fields($form_fields, $basic_keys); return self::filter_fields($form_fields, $basic_keys, $current_settings);
} }
/** /**
@@ -219,14 +220,14 @@ class PaymentGatewaysProvider {
* @param array $form_fields All form fields * @param array $form_fields All form fields
* @return array API fields * @return array API fields
*/ */
private static function extract_api_fields(array $form_fields): array { private static function extract_api_fields(array $form_fields, array $current_settings): array {
$api_patterns = ['key', 'secret', 'token', 'api', 'client', 'merchant', 'account']; $api_patterns = ['key', 'secret', 'token', 'api', 'client', 'merchant', 'account'];
$api_fields = []; $api_fields = [];
foreach ($form_fields as $key => $field) { foreach ($form_fields as $key => $field) {
foreach ($api_patterns as $pattern) { foreach ($api_patterns as $pattern) {
if (stripos($key, $pattern) !== false) { if (stripos($key, $pattern) !== false) {
$api_fields[$key] = self::normalize_field($key, $field); $api_fields[$key] = self::normalize_field($key, $field, $current_settings);
break; break;
} }
} }
@@ -241,7 +242,7 @@ class PaymentGatewaysProvider {
* @param array $form_fields All form fields * @param array $form_fields All form fields
* @return array Advanced fields * @return array Advanced fields
*/ */
private static function extract_advanced_fields(array $form_fields): array { private static function extract_advanced_fields(array $form_fields, array $current_settings): array {
$basic_keys = ['enabled', 'title', 'description', 'instructions']; $basic_keys = ['enabled', 'title', 'description', 'instructions'];
$advanced_fields = []; $advanced_fields = [];
@@ -262,7 +263,7 @@ class PaymentGatewaysProvider {
} }
if (!$is_api) { if (!$is_api) {
$advanced_fields[$key] = self::normalize_field($key, $field); $advanced_fields[$key] = self::normalize_field($key, $field, $current_settings);
} }
} }
@@ -276,12 +277,12 @@ class PaymentGatewaysProvider {
* @param array $keys Keys to extract * @param array $keys Keys to extract
* @return array Filtered fields * @return array Filtered fields
*/ */
private static function filter_fields(array $form_fields, array $keys): array { private static function filter_fields(array $form_fields, array $keys, array $current_settings): array {
$filtered = []; $filtered = [];
foreach ($keys as $key) { foreach ($keys as $key) {
if (isset($form_fields[$key])) { if (isset($form_fields[$key])) {
$filtered[$key] = self::normalize_field($key, $form_fields[$key]); $filtered[$key] = self::normalize_field($key, $form_fields[$key], $current_settings);
} }
} }
@@ -295,13 +296,17 @@ class PaymentGatewaysProvider {
* @param array $field Field data * @param array $field Field data
* @return array Normalized field * @return array Normalized field
*/ */
private static function normalize_field(string $key, array $field): array { private static function normalize_field(string $key, array $field, array $current_settings): array {
// Use current value if available, otherwise use default
$current_value = $current_settings[$key] ?? $field['default'] ?? '';
return [ return [
'id' => $key, 'id' => $key,
'type' => $field['type'] ?? 'text', 'type' => $field['type'] ?? 'text',
'title' => $field['title'] ?? '', 'title' => $field['title'] ?? '',
'description' => $field['description'] ?? '', 'description' => $field['description'] ?? '',
'default' => $field['default'] ?? '', 'default' => $field['default'] ?? '',
'value' => $current_value, // Add current value!
'placeholder' => $field['placeholder'] ?? '', 'placeholder' => $field['placeholder'] ?? '',
'required' => !empty($field['required']), 'required' => !empty($field['required']),
'options' => $field['options'] ?? null, 'options' => $field['options'] ?? null,