submission ??= new Submission();
}
private function entry(): Entry_Integration {
return $this->entry ??= new Entry_Integration();
}
/* ------------------------------------------------------------------ init */
public function init() {
parent::init();
add_filter('gform_validation', Guard::wrap([$this->submission(), 'validate']));
add_filter('gform_validation_message', Guard::wrap([$this->submission(), 'validation_message']), 10, 2);
add_filter('gform_confirmation', Guard::wrap([$this->submission(), 'confirmation']), 10, 4);
add_filter('gform_entry_meta', Guard::wrap([$this->entry(), 'register_entry_meta']), 10, 2);
add_filter('gform_custom_merge_tags', Guard::wrap([$this->entry(), 'register_merge_tags']), 10, 4);
add_filter('gform_replace_merge_tags', Guard::wrap([$this->entry(), 'replace_merge_tags']), 10, 7);
add_filter('gform_notification_events', Guard::wrap([new Notifier(), 'register_events']), 10, 2);
add_action('gform_entry_created', Guard::wrap([$this, 'on_entry_created']), 10, 2);
add_action('esp_instanda_resubmit', Guard::wrap([Resubmit_Handler::class, 'run']));
// Data-subject erasure: wire customer transactions into WP's Erase Personal Data tool.
add_filter('wp_privacy_personal_data_erasers', Guard::wrap([$this, 'register_privacy_erasers']));
// Keep Gravity Forms' scripts out of Jetpack Boost's JS concat/defer, which
// otherwise break GF's AJAX init and the post-submit INSTANDA redirect.
Boost_Compat::register();
}
public function init_admin() {
parent::init_admin();
add_action('admin_notices', Guard::wrap([new Failure_Surface(), 'unresolved_notice']));
add_action('admin_bar_menu', Guard::wrap([new Failure_Surface(), 'admin_bar_badge']), 100);
add_action('admin_notices', static function (): void {
if (get_transient('espinstanda_secret_save_error')) {
echo '
ESP INSTANDA: The Test password could not be saved - no encryption key is available. Add SPG_INSTANDA_KEY to wp-config.php, or ensure the libsodium PHP extension is enabled.
';
}
});
add_action('admin_menu', [$this, 'register_admin_pages'], 20);
}
/**
* GFAddOn dispatches admin-ajax.php requests to init_ajax() (NOT init_admin), so the
* wp_ajax_* handlers MUST be registered here or they never hook on an AJAX request
* (admin-ajax then returns HTTP 400 for the unregistered action).
*/
public function init_ajax() {
parent::init_ajax();
add_action('wp_ajax_espinstanda_test_connection', Guard::wrap([$this, 'ajax_test_connection']));
add_action('wp_ajax_espinstanda_resubmit', Guard::wrap([$this, 'ajax_resubmit']));
}
public function get_menu_icon() {
return 'gform-icon--cog';
}
/* ---------------------------------------------------------- plugin settings */
public function plugin_settings_fields() {
$go_live = new Go_Live();
$readiness = $go_live->readiness();
return [
[
'title' => 'Operating mode',
'fields' => [
[
'name' => 'mode_banner',
'type' => 'html',
'html' => $this->mode_banner_html($readiness),
],
[
'name' => 'env',
'label' => 'Active environment',
'type' => 'radio',
'horizontal' => true,
'default_value' => 'test',
'choices' => [
['label' => 'TEST', 'value' => 'test'],
['label' => Config::operating_mode() === 'live' ? 'LIVE' : 'LIVE (locked - complete the go-live checklist)', 'value' => 'live'],
],
'tooltip' => 'While locked to Test, the Live environment cannot be selected. Maps to SPG_INSTANDA_ENV.',
],
$this->maybe_const_field('site_domain', 'Site domain (siteDomainName)', 'SPG_INSTANDA_SITEDOMAIN', 'consumer.instanda.us'),
],
],
[
'title' => 'Credentials (Basic auth)',
'fields' => [
Config::username_is_constant('TEST')
? ['name' => 'username_test_const', 'label' => 'Test username', 'type' => 'html', 'html' => 'Set via the Test username constant (SPG_INSTANDA_USERNAME_TEST or SPG_INSTANDA_USER_TEST) in wp-config.php. Detected']
: ['name' => 'username_test', 'label' => 'Test username', 'type' => 'text', 'default_value' => ''],
[
'name' => 'password_test',
'label' => 'Test password',
'type' => Config::field_is_constant('SPG_INSTANDA_PASSWORD_TEST') ? 'html' : 'espinstanda_secret',
'html' => 'Set via SPG_INSTANDA_PASSWORD_TEST in wp-config.php. Detected',
],
[
'name' => 'live_credentials',
'type' => 'html',
'html' => $this->live_credentials_html(),
],
[
'name' => 'test_connection',
'label' => 'Test connection',
'type' => 'espinstanda_test_connection',
],
],
],
[
'title' => 'Submission storage & retention',
'fields' => [
['name' => 'storage_enabled', 'label' => 'Store submissions for resubmit', 'type' => 'toggle', 'default_value' => true],
['name' => 'retention_days', 'label' => 'Retention (days)', 'type' => 'text', 'input_type' => 'number', 'default_value' => '7', 'class' => 'small-text',
'tooltip' => 'Only delivered records are purged after this window. Failed/pending are kept until resolved.'],
],
],
[
'title' => 'Failure alerts',
'fields' => [
['name' => 'alert_emails', 'label' => 'Validation-failure alert emails', 'type' => 'textarea', 'class' => 'medium',
'tooltip' => 'Comma-separated. Used for failures that happen before an entry exists (sent with wp_mail).'],
],
],
];
}
/** A text field that locks to a "managed in wp-config.php" note when its constant is set. */
private function maybe_const_field(string $name, string $label, string $constant, string $default = ''): array {
if (Config::field_is_constant($constant)) {
return [
'name' => $name . '_const',
'label' => $label,
'type' => 'html',
'html' => 'Set via ' . esc_html($constant) . ' in wp-config.php. Detected',
];
}
return ['name' => $name, 'label' => $label, 'type' => 'text', 'default_value' => $default];
}
private function mode_banner_html(array $readiness): string {
$locked = Config::operating_mode() === 'test';
$class = $locked ? 'notice-warning' : ($readiness['ready'] ? 'notice-success' : 'notice-warning');
$msg = $locked
? 'Test mode. This plugin is locked to the INSTANDA TEST environment. Live is disabled until the go-live checklist is complete.'
: 'Live enabled. Set the environment to LIVE to send real customer quotes.';
$link = 'View go-live checklist →';
return '
';
}
private function live_credentials_html(): string {
if (Config::live_credentials_are_constants()) {
return '
Live credentials detected via wp-config.php constants. Detected
';
}
return '
Live credentials are not configured. When ready for production, add a Live username constant (SPG_INSTANDA_USERNAME_LIVE or SPG_INSTANDA_USER_LIVE) and SPG_INSTANDA_PASSWORD_LIVE to wp-config.php. Live credentials are never stored in the database.
This submission may already have created a quote in INSTANDA. Resubmitting will create a second quote. Confirm there is no existing quote for this customer before resubmitting.
';
}
if ($tx->environment !== Config::environment()->value) {
echo '
This transaction was created in ' . esc_html(strtoupper($tx->environment)) . ' but the plugin is currently set to ' . esc_html(strtoupper(Config::environment()->value)) . '. Resubmit is blocked.
';
}
public function render_go_live_page() {
if (!\GFCommon::current_user_can_any('espinstanda_settings')) {
wp_die('Insufficient permissions.');
}
$go_live = new Go_Live();
$this->handle_go_live_post($go_live);
$readiness = $go_live->readiness();
echo '
Go-live checklist
';
echo '
Everything that must be true before this integration sends real customer quotes to INSTANDA Live. Blocking items must all pass before Live can be enabled. This is reversible.
';
$this->render_checklist('Blocking items', $go_live->blocking_items());
$this->render_checklist('Advisory items (recommended - do not block)', $go_live->advisory_items());
echo '
'
. 'Before you switch: the read-only probes (B2/B3) prove credentials authenticate - not that a Live StartQuote renders a priced quote - and they expire after 30 minutes, so re-run "Test connection" / "Test Live credentials" just before switching. After switching to Live, complete one watched real submit per role, confirm the redirect lands on a priced quote, then re-confirm pricing (B5) on Live.'
. '