diff --git a/.gitignore b/.gitignore index 4ba8de367..01f932c40 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ todo.md artifacts *.results.json trace.json -license.json \ No newline at end of file +license.json +.fleet diff --git a/.wp-env.override.json b/.wp-env.override.json index 530d913bd..2bbde377f 100644 --- a/.wp-env.override.json +++ b/.wp-env.override.json @@ -6,22 +6,23 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_DISPLAY": true, - "FS_METHOD": "direct" + "FS_METHOD": "direct", + "WP_DEFAULT_THEME": "twentytwentythree" }, "env": { - "development": { - "themes": [ - "./one-theme" - ] - }, "tests": { + "config": { + "WP_DEBUG": false, + "WP_DEBUG_DISPLAY": false + }, "plugins": [ "." ], "mappings": { - "wp-content/mu-plugins": "./node_modules/@wordpress/e2e-tests/mu-plugins", - "wp-content/plugins/gutenberg-test-plugins": "./node_modules/@wordpress/e2e-tests/plugins" - } + "wp-content/mu-plugins": "./node_modules/@wordpress/e2e-tests/mu-plugins", + "wp-content/plugins/gutenberg-test-plugins": "./node_modules/@wordpress/e2e-tests/plugins", + "wp-content/themes/gutenberg-test-themes/twentytwentythree": "https://downloads.wordpress.org/theme/twentytwentythree.1.0.zip" + } } } -} \ No newline at end of file +} diff --git a/assets/images/guide/welcome-ai.png b/assets/images/guide/welcome-ai.png new file mode 100644 index 000000000..379e2f29b Binary files /dev/null and b/assets/images/guide/welcome-ai.png differ diff --git a/blocks.json b/blocks.json index e740e559d..4d6889618 100644 --- a/blocks.json +++ b/blocks.json @@ -99,6 +99,12 @@ "form-file": { "block": "blocks/blocks/form/file/block.json" }, + "form-hidden-field": { + "block": "blocks/blocks/form/hidden-field/block.json" + }, + "form-stripe-field": { + "block": "blocks/blocks/form/stripe-field/block.json" + }, "google-map": { "block": "blocks/blocks/google-map/block.json", "assets": { @@ -272,5 +278,8 @@ "product-upsells": { "isPro": true, "block": "pro/woocommerce/upsells/block.json" + }, + "content-generator": { + "block": "blocks/blocks/content-generator/block.json" } } diff --git a/inc/class-main.php b/inc/class-main.php index 02cc5ab8a..f2518a4eb 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -74,6 +74,7 @@ public function autoload_classes() { '\ThemeIsle\GutenbergBlocks\Integration\Form_Providers', '\ThemeIsle\GutenbergBlocks\Integration\Form_Email', '\ThemeIsle\GutenbergBlocks\Server\Form_Server', + '\ThemeIsle\GutenbergBlocks\Server\Prompt_Server', ); $classnames = apply_filters( 'otter_blocks_autoloader', $classnames ); diff --git a/inc/class-registration.php b/inc/class-registration.php index 5451e5924..7e9796229 100644 --- a/inc/class-registration.php +++ b/inc/class-registration.php @@ -498,16 +498,17 @@ function() { 'root' => esc_url_raw( rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'messages' => array( - 'submission' => __( 'Form submission from', 'otter-blocks' ), - 'captcha-not-loaded' => __( 'Captcha is not loaded. Please check your browser plugins to allow the Google reCaptcha.', 'otter-blocks' ), - 'check-captcha' => __( 'Please check the captcha.', 'otter-blocks' ), - 'invalid-email' => __( 'The email address is invalid!', 'otter-blocks' ), - 'already-registered' => __( 'The email was already registered!', 'otter-blocks' ), - 'try-again' => __( 'Error. Something is wrong with the server! Try again later.', 'otter-blocks' ), - 'privacy' => __( 'I have read and agreed the privacy statement.', 'otter-blocks' ), - 'too-many-files' => __( 'Too many files loaded. Maximum is: ', 'otter-blocks' ), - 'big-file' => __( 'File size is to big. The limit is: ', 'otter-blocks' ), - 'invalid-file' => __( 'Invalid files type. The submitted files could not be processed.', 'otter-blocks' ), + 'submission' => __( 'Form submission from', 'otter-blocks' ), + 'captcha-not-loaded' => __( 'Captcha is not loaded. Please check your browser plugins to allow the Google reCaptcha.', 'otter-blocks' ), + 'check-captcha' => __( 'Please check the captcha.', 'otter-blocks' ), + 'invalid-email' => __( 'The email address is invalid!', 'otter-blocks' ), + 'already-registered' => __( 'The email was already registered!', 'otter-blocks' ), + 'try-again' => __( 'Error. Something is wrong with the server! Try again later.', 'otter-blocks' ), + 'privacy' => __( 'I have read and agreed the privacy statement.', 'otter-blocks' ), + 'too-many-files' => __( 'Too many files loaded. Maximum is: ', 'otter-blocks' ), + 'big-file' => __( 'File size is to big. The limit is: ', 'otter-blocks' ), + 'invalid-file' => __( 'Invalid files type. The submitted files could not be processed.', 'otter-blocks' ), + 'confirmingSubmission' => __( 'Confirming submission', 'otter-blocks' ), ), ) ); diff --git a/inc/integrations/api/form-request-data.php b/inc/integrations/api/form-request-data.php index 503533eaa..8c8039488 100644 --- a/inc/integrations/api/form-request-data.php +++ b/inc/integrations/api/form-request-data.php @@ -7,8 +7,6 @@ namespace ThemeIsle\GutenbergBlocks\Integration; -use ArrayAccess; - /** * Class Form_Data_Request * @@ -43,7 +41,7 @@ class Form_Data_Request { /** * Form fields options. * - * @var array + * @var array * @since 2.2.3 */ protected $form_fields_options = array(); @@ -104,6 +102,22 @@ class Form_Data_Request { */ protected $warning_codes = array(); + /** + * Saving mode of the form data. + * + * @var string $saving_mode Saving mode. + * @since 2.4 + */ + protected $saving_mode = 'permanent'; + + /** + * The form metadata generated through the request. Use prefix 'frontend_' to make the value visible in the frontend. + * + * @var array + * @since 2.4 + */ + public $metadata = array(); + /** * Constructor. * @@ -113,7 +127,7 @@ class Form_Data_Request { */ public function __construct( $request = null ) { - if ( ! is_a( $request, 'WP_REST_Request' ) ) { + if ( ! isset( $request ) || ! is_a( $request, 'WP_REST_Request' ) ) { return; } @@ -132,8 +146,21 @@ public function __construct( $request = null ) { } } - $this->form_options = new Form_Settings_Data( array() ); + + if ( ! empty( $request->get_header( 'O-Form-Save-Mode' ) ) ) { + $this->set_saving_mode( $request->get_header( 'O-Form-Save-Mode' ) ); + } + } + + /** + * Set the form data. + * + * @param mixed $form_request_data The form data. + * @return void + */ + private function set_data_from_request( $form_request_data ) { + $this->request_data = $form_request_data; } /** @@ -150,34 +177,34 @@ public function set_form_options( $form_options ) { /** * Get the value of the field from the request. * - * @param string $field_name The name of the field. + * @param string $key The name of the field. * @return mixed * @since 2.0.3 */ - public function get( $field_name ) { - return $this->is_set( $field_name ) ? $this->request_data[ $field_name ] : null; + public function get_root_data( $key ) { + return $this->is_root_data_set( $key ) ? $this->request_data[ $key ] : null; } /** - * Get the field value. + * Get the item from the payload. * - * @param string $field_name The name of the field. + * @param string $key The name of the item. * @return mixed|null * @since 2.0.3 */ - public function get_payload_field( $field_name ) { - return $this->payload_has_field( $field_name ) ? $this->request_data['payload'][ $field_name ] : null; + public function get_data_from_payload( $key ) { + return $this->payload_has( $key ) ? $this->request_data['payload'][ $key ] : null; } /** - * Check if the payload has the field. + * Check if the payload has the data item set. * - * @param string $field_name The name of the field. + * @param string $key The name of the item. * @return bool * @since 2.0.3 */ - public function payload_has_field( $field_name ) { - return $this->has_payload() && isset( $this->request_data['payload'][ $field_name ] ); + public function payload_has( $key ) { + return $this->has_payload() && isset( $this->request_data['payload'][ $key ] ); } /** @@ -202,27 +229,29 @@ public function change_provider( $provider ) { } /** - * Check if the value of the field is set. + * Check if the root data of the request is set. + * The root data is the top level structure of the request. * - * @param string $field_name The name of the field. + * @param string $key The name of the field. * @return boolean * @since 2.0.0 */ - public function is_set( $field_name ) { + public function is_root_data_set( $key ) { // TODO: we can do a more refined verification like checking for empty strings or arrays. - return isset( $this->request_data[ $field_name ] ); + return isset( $this->request_data[ $key ] ); } /** - * Check if the given fields are set. + * Check if the root data of the request has the given fields. + * The root data is the top level structure of the request. * - * @param array $fields_name The name of the fields. + * @param array $keys The name of the fields. * @return boolean * @since 2.0.0 */ - public function are_fields_set( $fields_name ) { - foreach ( $fields_name as $field_name ) { - if ( ! isset( $this->request_data[ $field_name ] ) ) { + public function are_root_data_set( $keys ) { + foreach ( $keys as $key ) { + if ( ! $this->is_root_data_set( $key ) ) { return false; } } @@ -230,61 +259,21 @@ public function are_fields_set( $fields_name ) { } /** - * Check if the given fields are set. + * Check if the given data fields are set in the payload. * - * @param array $fields_name The name of the fields. + * @param array $keys The name of the fields. * @return boolean * @since 2.0.3 */ - public function are_payload_fields_set( $fields_name ) { - foreach ( $fields_name as $field_name ) { - if ( ! isset( $this->request_data['payload'][ $field_name ] ) || '' === $this->request_data['payload'][ $field_name ] ) { + public function are_payload_data_set( $keys ) { + foreach ( $keys as $key ) { + if ( ! isset( $this->request_data['payload'][ $key ] ) || '' === $this->request_data['payload'][ $key ] ) { return false; } } return true; } - /** - * Check if the payload has the given fields. - * - * @param array $fields_name The name of the fields. - * @return bool - * @since 2.0.3 - */ - public function payload_has_fields( $fields_name ) { - foreach ( $fields_name as $field_name ) { - if ( ! $this->payload_has_field( $field_name ) ) { - return false; - } - } - return true; - } - - /** - * Check if the field has one of the given values. - * - * @param string $field_name The name of the field. - * @param array $values The desired values of the field. - * @return boolean - * @since 2.0.0 - */ - public function field_has( $field_name, $values ) { - return in_array( $this->get( $field_name ), $values, true ); - } - - /** - * Check if a field has the given values. - * - * @param string $field_name The field name. - * @param array $values The values. - * @return bool - * @since 2.0.3 - */ - public function payload_field_has( $field_name, $values ) { - return in_array( $this->get_payload_field( $field_name ), $values, true ); - } - /** * Sanitize the request data. * @@ -325,8 +314,8 @@ public static function sanitize_array_map_deep( $array ) { * @return mixed Form input data. * @since 2.0.0 */ - public function get_form_inputs() { - return $this->get_payload_field( 'formInputsData' ); + public function get_fields() { + return $this->get_data_from_payload( 'formInputsData' ); } /** @@ -335,7 +324,7 @@ public function get_form_inputs() { * @return Form_Settings_Data|null * @since 2.0.0 */ - public function get_form_options() { + public function get_wp_options() { return $this->form_options; } @@ -382,12 +371,14 @@ public function can_keep_uploaded_files() { /** * Set if we should keep the uploaded files. * - * @param bool $keep_uploaded_files True if we should keep the uploaded files. + * @param bool|mixed $keep_uploaded_files True if we should keep the uploaded files. * @return void * @since 2.2.3 */ public function set_keep_uploaded_files( $keep_uploaded_files ) { - $this->keep_uploaded_files = $keep_uploaded_files; + if ( is_bool( $keep_uploaded_files ) ) { + $this->keep_uploaded_files = (bool) $keep_uploaded_files; + } } /** @@ -413,12 +404,14 @@ public function get_files_loaded_to_media_library() { /** * Set the files loaded to media library. * - * @param array $files_loaded_to_media_library The files loaded to media library. + * @param array|mixed $files_loaded_to_media_library The files loaded to media library. * @return void * @since 2.2.3 */ public function set_files_loaded_to_media_library( $files_loaded_to_media_library ) { - $this->files_loaded_to_media_library = $files_loaded_to_media_library; + if ( is_array( $files_loaded_to_media_library ) ) { + $this->files_loaded_to_media_library = $files_loaded_to_media_library; + } } /** @@ -441,16 +434,6 @@ public function get_error_code() { return $this->error_code; } - /** - * Get the error details. - * - * @return string - * @since 2.2.3 - */ - public function get_error_details() { - return $this->error_details; - } - /** * Set the error. * @@ -470,11 +453,11 @@ public function set_error( $error_code, $error_details = null ) { * @return mixed|string * @since 2.2.3 */ - public function get_email_from_form_input() { - $inputs = $this->get_form_inputs(); + public function get_first_email_from_input_fields() { + $inputs = $this->get_fields(); if ( is_array( $inputs ) ) { foreach ( $inputs as $input_field ) { - if ( 'email' == $input_field['type'] ) { + if ( ! empty( $input_field['type'] ) && 'email' == $input_field['type'] ) { return $input_field['value']; } } @@ -485,11 +468,11 @@ public function get_email_from_form_input() { /** * Add a field option. * - * @param Form_Field_Option_Data $field_option The field option. + * @param Form_Field_WP_Option_Data $field_option The field option. * @return void */ - public function add_field_option( $field_option ) { - if ( $field_option instanceof Form_Field_Option_Data ) { + public function add_field_wp_option( $field_option ) { + if ( $field_option instanceof Form_Field_WP_Option_Data ) { if ( empty( $this->form_fields_options ) ) { $this->form_fields_options = array(); } @@ -498,24 +481,12 @@ public function add_field_option( $field_option ) { } } - /** - * Remove a field option. - * - * @param string $field_option_name The field option name. - * @return void - */ - public function remove_field_option( $field_option_name ) { - if ( isset( $this->form_fields_options[ $field_option_name ] ) ) { - unset( $this->form_fields_options[ $field_option_name ] ); - } - } - /** * Get the field options. * - * @return array + * @return array */ - public function get_field_options() { + public function get_wp_fields_options() { return $this->form_fields_options; } @@ -523,7 +494,7 @@ public function get_field_options() { * Get the field option. * * @param string $field_option_name The field option name. - * @return Form_Field_Option_Data|null + * @return Form_Field_WP_Option_Data|null */ public function get_field_option( $field_option_name ) { if ( isset( $this->form_fields_options[ $field_option_name ] ) ) { @@ -595,7 +566,7 @@ public function has_warning_codes( $codes ) { * @return mixed|string|null */ public function get_form_option_id() { - return $this->get_payload_field( 'formOption' ); + return $this->get_data_from_payload( 'formOption' ); } /** @@ -606,4 +577,135 @@ public function get_form_option_id() { public function get_request() { return $this->request; } + + /** + * Get the saving mode. + * + * @return string|null + */ + public function get_saving_mode() { + return $this->saving_mode; + } + + /** + * Check if we are saving temporary data. + * + * @return bool + */ + public function is_temporary() { + return 'temporary' === $this->saving_mode; + } + + /** + * Check if we are saving duplicate data. + * + * @return bool + */ + public function is_duplicate() { + return 'duplicate' === $this->saving_mode; + } + + /** + * Set the saving mode. + * + * @param string $saving_mode The saving mode. + * @return void + */ + public function set_saving_mode( $saving_mode ) { + if ( empty( $saving_mode ) ) { + return; + } + + $this->saving_mode = $saving_mode; + } + + /** + * Mark as duplicate. + * + * @return void + */ + public function mark_as_duplicate() { + $this->set_saving_mode( 'duplicate' ); + } + + /** + * Mark as temporary data. + * + * @return void + */ + public function mark_as_temporary() { + $this->set_saving_mode( 'temporary' ); + } + + /** + * Dump the data. Can be used to reconstruct the object. + * + * @return array The data to dump. + */ + public function dump_data() { + return array( + 'form_data' => $this->request_data, + 'uploaded_files_path' => $this->uploaded_files_path, + 'files_loaded_to_media_library' => $this->files_loaded_to_media_library, + 'keep_uploaded_files' => $this->keep_uploaded_files, + 'metadata' => $this->metadata, + ); + } + + /** + * Create a new instance from dumped data. + * + * @param array $dumped_data The dumped data. + * @return Form_Data_Request + */ + public static function create_from_dump( $dumped_data ) { + + $form = new self( null ); + + if ( ! empty( $dumped_data['form_data'] ) ) { + $form->set_data_from_request( $dumped_data['form_data'] ); + } + + if ( ! empty( $dumped_data['uploaded_files_path'] ) ) { + $form->set_uploaded_files_path( $dumped_data['uploaded_files_path'] ); + } + + if ( ! empty( $dumped_data['files_loaded_to_media_library'] ) ) { + $form->set_files_loaded_to_media_library( $dumped_data['files_loaded_to_media_library'] ); + } + + if ( ! empty( $dumped_data['keep_uploaded_files'] ) ) { + $form->set_keep_uploaded_files( $dumped_data['keep_uploaded_files'] ); + } + + if ( ! empty( $dumped_data['metadata'] ) ) { + $form->metadata = $dumped_data['metadata']; + } + + return $form; + } + + /** + * Append the frontend metadata to the response. + * + * @param Form_Data_Response $response The response. + * @return void + */ + public function append_metadata( $response ) { + foreach ( $this->metadata as $key => $value ) { + if ( 0 === strpos( $key, 'frontend_' ) ) { + $response->add_response_field( $key, $value ); + } + } + } + + /** + * Check if field is present in the metadata. + * + * @param string $key The key. + * @return bool + */ + public function has_metadata( $key ) { + return isset( $this->metadata[ $key ] ); + } } diff --git a/inc/integrations/api/form-response-data.php b/inc/integrations/api/form-response-data.php index bd45925ab..0d93c372d 100644 --- a/inc/integrations/api/form-response-data.php +++ b/inc/integrations/api/form-response-data.php @@ -45,6 +45,7 @@ class Form_Data_Response { const ERROR_BOT_DETECTED = '110'; const ERROR_FILES_METADATA_FORMAT = '111'; const ERROR_FILE_MISSING_BINARY = '112'; + const ERROR_MISSING_DUMP_DATA = '113'; @@ -58,6 +59,12 @@ class Form_Data_Response { const ERROR_PROVIDER_INVALID_EMAIL = '207'; const ERROR_PROVIDER_DUPLICATED_EMAIL = '208'; const ERROR_PROVIDER_CREDENTIAL_ERROR = '209'; + const ERROR_WEBHOOK_COULD_NOT_TRIGGER = '210'; + const ERROR_RUNTIME_STRIPE_SESSION_VALIDATION = '300'; + const ERROR_STRIPE_CHECKOUT_SESSION_CREATION = '301'; + const ERROR_STRIPE_CHECKOUT_SESSION_NOT_FOUND = '302'; + const ERROR_STRIPE_PAYMENT_UNPAID = '303'; + const ERROR_STRIPE_METADATA_RECORD_NOT_FOUND = '304'; /** @@ -77,6 +84,13 @@ class Form_Data_Response { */ protected $is_credential_error = false; + /** + * The REST response. + * + * @var WP_REST_Response|WP_Error|WP_HTTP_Response|null + */ + public $rest_response = null; + /** * Constructor. * @@ -178,12 +192,25 @@ public function is_success() { * @since 2.0.3 */ public function build_response() { - // TODO: We can to addition operation when returning the response. + if ( isset( $this->rest_response ) ) { + $this->rest_response->set_data( $this->response ); + return $this->rest_response; + } + $this->process_error_code(); return rest_ensure_response( $this->response ); } + /** + * Create the REST response. + * + * @return void + */ + public function make_internal_response() { + $this->rest_response = $this->build_response(); + } + /** * Mark response as success. * @@ -358,6 +385,13 @@ public static function get_error_code_message( $error_code ) { self::ERROR_AUTORESPONDER_COULD_NOT_SEND => __( 'The email from Autoresponder could not be sent.', 'otter-blocks' ), self::ERROR_FILE_MISSING_BINARY => __( 'The file data is missing.', 'otter-blocks' ), self::ERROR_MALFORMED_REQUEST => __( 'The request is malformed.', 'otter-blocks' ), + self::ERROR_WEBHOOK_COULD_NOT_TRIGGER => __( 'The webhook could not be triggered.', 'otter-blocks' ), + self::ERROR_MISSING_DUMP_DATA => __( 'The form dump data is missing.', 'otter-blocks' ), + self::ERROR_STRIPE_CHECKOUT_SESSION_CREATION => __( 'The Stripe Checkout session could not be created.', 'otter-blocks' ), + self::ERROR_STRIPE_CHECKOUT_SESSION_NOT_FOUND => __( 'The Stripe Checkout session was not found.', 'otter-blocks' ), + self::ERROR_STRIPE_PAYMENT_UNPAID => __( 'The payment was not completed.', 'otter-blocks' ), + self::ERROR_STRIPE_METADATA_RECORD_NOT_FOUND => __( 'The metadata submission record was not found.', 'otter-blocks' ), + self::ERROR_RUNTIME_STRIPE_SESSION_VALIDATION => __( 'The payment has been processed. You will be contacted by the support team.', 'otter-blocks' ), ); if ( ! isset( $error_messages[ $error_code ] ) ) { diff --git a/inc/integrations/class-form-email.php b/inc/integrations/class-form-email.php index cad215e9e..c73c06776 100644 --- a/inc/integrations/class-form-email.php +++ b/inc/integrations/class-form-email.php @@ -118,7 +118,7 @@ public function build_head() { * @since 2.0.3 */ public function build_body( $form_data ) { - $email_form_content = $form_data->get_form_inputs(); + $email_form_content = $form_data->get_fields(); $content = ''; $attachment_links = ''; @@ -282,7 +282,7 @@ public function build_test_email( $form_data ) { esc_html__( 'Mail From: ', 'otter-blocks' ), sanitize_email( get_site_option( 'admin_email' ) ), esc_html( __( 'This a test email. If you receive this email, your SMTP set-up is working for sending emails via Form Block.', 'otter-blocks' ) ), - $form_data->get_payload_field( 'site' ) + $form_data->get_data_from_payload( 'site' ) ); } diff --git a/inc/integrations/class-form-field-option-data.php b/inc/integrations/class-form-field-wp-option-data.php similarity index 70% rename from inc/integrations/class-form-field-option-data.php rename to inc/integrations/class-form-field-wp-option-data.php index 552f6fe8a..1994d0a94 100644 --- a/inc/integrations/class-form-field-option-data.php +++ b/inc/integrations/class-form-field-wp-option-data.php @@ -13,7 +13,7 @@ * @package ThemeIsle\GutenbergBlocks\Integration * @since 2.2.3 */ -class Form_Field_Option_Data { +class Form_Field_WP_Option_Data { /** * The name of the field option. @@ -36,17 +36,22 @@ class Form_Field_Option_Data { */ protected $options = array(); + /** + * The stripe data of the field option. + * + * @var array + */ + protected $stripe_product_info = array(); + /** * Form_Field_Option_Data constructor. * * @param string $field_option_name The name of the field option. * @param string $field_option_type The type of the field option. - * @param array $options The options of the field option. */ - public function __construct( $field_option_name = '', $field_option_type = '', $options = array() ) { + public function __construct( $field_option_name = '', $field_option_type = '' ) { $this->field_option_name = $field_option_name; $this->field_option_type = $field_option_type; - $this->options = $options; } /** @@ -92,13 +97,17 @@ public function get_option( $option_name ) { } /** - * Set the option of the field option. + * Get the stripe data of the field option. * - * @param string $option_name The name of the option. - * @param mixed $option_value The value of the option. + * @return array The stripe data of the field option: + * [ + * 'product' => (string) The ID of the product, + * 'price' => (string) The price ID of the product, + * 'quantity' => (int) The quantity of the product to order + * ] */ - public function set_option( $option_name, $option_value ) { - $this->options[ $option_name ] = $option_value; + public function get_stripe_product_info() { + return $this->stripe_product_info; } /** @@ -128,6 +137,24 @@ public function set_name( $field_option_name ) { $this->field_option_name = $field_option_name; } + /** + * Set the stripe product data of the field option. + * + * @param array $stripe_product_info The stripe product data of the field option. + * @return void + */ + public function set_stripe_product_info( $stripe_product_info ) { + if ( ! is_array( $stripe_product_info ) ) { + return; + } + + if ( ! isset( $stripe_product_info['product'] ) || ! isset( $stripe_product_info['price'] ) ) { + return; + } + + $this->stripe_product_info = $stripe_product_info; + } + /** * Check if the field option has the option. * @@ -147,6 +174,8 @@ public function has_options() { return ! empty( $this->options ); } + + /** * Check if the field option has type. * @@ -164,4 +193,13 @@ public function has_type() { public function has_name() { return ! empty( $this->field_option_name ); } + + /** + * Check if the field option has stripe product data. + * + * @return bool + */ + public function has_stripe_product_info() { + return ! empty( $this->stripe_product_info ); + } } diff --git a/inc/integrations/class-form-providers.php b/inc/integrations/class-form-providers.php index 6e76c8306..c36a4eda1 100644 --- a/inc/integrations/class-form-providers.php +++ b/inc/integrations/class-form-providers.php @@ -92,7 +92,7 @@ public function register_providers( $new_providers ) { * @since 2.0.3 */ public function select_provider_from_form_options( $form_request ) { - $form_options = $form_request->get_form_options(); + $form_options = $form_request->get_wp_options(); if ( $form_options->has_provider() && $form_options->has_credentials() ) { return $this->get_provider_handlers( $form_options->get_provider() ); } diff --git a/inc/integrations/class-form-settings-data.php b/inc/integrations/class-form-settings-data.php index 21c27cb1e..edd1c62b3 100644 --- a/inc/integrations/class-form-settings-data.php +++ b/inc/integrations/class-form-settings-data.php @@ -121,6 +121,20 @@ class Form_Settings_Data { */ private $submissions_save_location = ''; + /** + * The webhook ID. + * + * @var string + */ + private $webhook_id = ''; + + /** + * The required fields. + * + * @var array + */ + private $required_fields = array(); + /** * The default constructor. * @@ -241,6 +255,12 @@ public static function get_form_setting_from_wordpress_options( $form_option ) { $integration->set_submissions_save_location( 'database-email' ); } $integration->set_meta( $form ); + if ( isset( $form['webhookId'] ) ) { + $integration->set_webhook_id( $form['webhookId'] ); + } + if ( isset( $form['requiredFields'] ) && is_array( $form['requiredFields'] ) ) { + $integration->set_required_fields( $form['requiredFields'] ); + } } } return $integration; @@ -644,6 +664,15 @@ public function get_autoresponder() { return $this->autoresponder; } + /** + * Get the webhook id. + * + * @return string + */ + public function get_webhook_id() { + return $this->webhook_id; + } + /** * Set the autoresponder. * @@ -675,4 +704,47 @@ public function set_submissions_save_location( $submissions_save_location ) { $this->submissions_save_location = $submissions_save_location; return $this; } + + /** + * Set the webhook ID. + * + * @param string $webhook_id The webhook ID. + * @return $this + */ + private function set_webhook_id( $webhook_id ) { + if ( ! empty( $webhook_id ) ) { + $this->webhook_id = $webhook_id; + } + return $this; + } + + /** + * Set the required fields. + * + * @param array $required_fields The required fields. + * @return $this + */ + public function set_required_fields( $required_fields ) { + + $this->required_fields = $required_fields; + return $this; + } + + /** + * Get the required fields. + * + * @return array + */ + public function get_required_fields() { + return $this->required_fields; + } + + /** + * Check if the form has required fields. + * + * @return bool + */ + public function has_required_fields() { + return ! empty( $this->required_fields ); + } } diff --git a/inc/integrations/providers/class-mailchimp.php b/inc/integrations/providers/class-mailchimp.php index 88bab3def..a03b3849e 100644 --- a/inc/integrations/providers/class-mailchimp.php +++ b/inc/integrations/providers/class-mailchimp.php @@ -141,7 +141,7 @@ public function make_subscribe_request( $email ) { */ public function subscribe( $form_data ) { - $email = $form_data->get_email_from_form_input(); + $email = $form_data->get_first_email_from_input_fields(); $response = $this->make_subscribe_request( $email ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); @@ -265,8 +265,8 @@ private function get_new_user_status_mailchimp( $list_id ) { * @since 2.0.3 */ public function get_information_from_provider( $request ) { - if ( $request->is_set( 'action' ) ) { - if ( $request->get( 'action' ) == 'listId' ) { + if ( $request->is_root_data_set( 'action' ) ) { + if ( $request->get_root_data( 'action' ) == 'listId' ) { return $this->get_lists(); } } diff --git a/inc/integrations/providers/class-sendinblue.php b/inc/integrations/providers/class-sendinblue.php index 267d4f0f1..38c5cca4a 100644 --- a/inc/integrations/providers/class-sendinblue.php +++ b/inc/integrations/providers/class-sendinblue.php @@ -133,7 +133,7 @@ public function subscribe( $form_data ) { return $form_data; } - $email = $form_data->get_email_from_form_input(); + $email = $form_data->get_first_email_from_input_fields(); $response = $this->make_subscribe_request( $email ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); @@ -220,8 +220,8 @@ public function set_list_id( $list_id ) { * @since 2.0.3 */ public function get_information_from_provider( $request ) { - if ( $request->is_set( 'action' ) ) { - if ( $request->get( 'action' ) == 'listId' ) { + if ( $request->is_root_data_set( 'action' ) ) { + if ( $request->get_root_data( 'action' ) == 'listId' ) { return $this->get_lists(); } } diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 8ee25a375..6284f49eb 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -28,6 +28,11 @@ public function init() { add_action( 'admin_menu', array( $this, 'register_menu_page' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect' ) ); add_action( 'admin_notices', array( $this, 'maybe_add_otter_banner' ), 30 ); + + $form_options = get_option( 'themeisle_blocks_form_emails' ); + if ( ! empty( $form_options ) ) { + add_action( 'wp_dashboard_setup', array( $this, 'form_submissions_widget' ) ); + } } /** @@ -271,6 +276,321 @@ private function the_otter_banner() { 'otter_form_record', + 'posts_per_page' => 5, + ); + + if ( 'all' !== $posts_filter ) { + $query_args['post_status'] = $posts_filter; + } + + $query = new \WP_Query( $query_args ); + + + $records_count = wp_count_posts( 'otter_form_record' ); + + $count = $records_count->read + $records_count->unread; + + if ( 'read' === $posts_filter ) { + $count = $records_count->read; + } elseif ( 'unread' === $posts_filter ) { + $count = $records_count->unread; + } + + if ( $query->have_posts() ) { + + while ( $query->have_posts() ) { + $query->the_post(); + + $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); + + $title = null; + $date = null; + + if ( isset( $meta['post_id']['value'] ) ) { + $date = get_the_date( 'F j, H:i', $meta['post_id']['value'] ); + } + + if ( isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ) { + // Find the first email field and use that as the title. + foreach ( $meta['inputs'] as $input ) { + if ( isset( $input['type'] ) && 'email' === $input['type'] && ! empty( $input['value'] ) ) { + $title = $input['value']; + break; + } + } + } + + + if ( ! $title ) { + + if ( isset( $meta['post_id']['value'] ) ) { + $title = __( 'Submission', 'otter-blocks' ) . ' #' . get_the_ID(); + } else { + $title = __( 'No title', 'otter-blocks' ); + } + } + + $entries[] = array( + 'title' => $title, + 'date' => $date, + ); + } + } + } + + ?> + + + + +
+ + +
+
+ Otter Logo +
+ +
+

+ +
+
+ + +
+
+
+ + + + : + + + + +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ array( 'type' => 'string', ), + 'webhookId' => array( + 'type' => 'string', + ), + 'requiredFields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), ), ), ), @@ -433,6 +450,18 @@ function ( $item ) { $item['options']['maxFilesNumber'] = sanitize_text_field( $item['options']['maxFilesNumber'] ); } + if ( isset( $item['stripe']['product'] ) ) { + $item['stripe']['product'] = sanitize_text_field( $item['stripe']['product'] ); + } + + if ( isset( $item['stripe']['price'] ) ) { + $item['stripe']['price'] = sanitize_text_field( $item['stripe']['price'] ); + } + + if ( isset( $item['stripe']['quantity'] ) && ! is_int( $item['stripe']['quantity'] ) ) { + $item['stripe']['quantity'] = sanitize_text_field( $item['stripe']['quantity'] ); + } + return $item; }, $array @@ -471,6 +500,22 @@ function ( $item ) { ), 'default' => array(), ), + 'stripe' => array( + 'type' => 'object', + 'properties' => array( + 'product' => array( + 'type' => 'string', + ), + 'price' => array( + 'type' => 'string', + ), + 'quantity' => array( + 'type' => 'number', + 'default' => 1, + ), + ), + + ), ), ), ), @@ -511,6 +556,141 @@ function ( $item ) { ), ) ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_webhooks_options', + array( + 'type' => 'array', + 'description' => __( 'Otter Registered Webhooks.', 'otter-blocks' ), + 'sanitize_callback' => function ( $array ) { + return array_map( + function ( $item ) { + if ( isset( $item['id'] ) ) { + $item['id'] = sanitize_text_field( $item['id'] ); + } + if ( isset( $item['url'] ) ) { + $item['url'] = esc_url_raw( $item['url'] ); + } + if ( isset( $item['name'] ) ) { + $item['name'] = sanitize_text_field( $item['name'] ); + } + if ( isset( $item['method'] ) ) { + $item['method'] = sanitize_text_field( $item['method'] ); + } + if ( isset( $item['headers'] ) && is_array( $item['headers'] ) ) { + foreach ( $item['headers'] as &$header ) { + $header['key'] = sanitize_text_field( $header['key'] ); + $header['value'] = sanitize_text_field( $header['value'] ); + } + } else { + $item['headers'] = array(); + } + return $item; + }, + $array + ); + }, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + ), + 'headers' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + ), + 'value' => array( + 'type' => 'string', + ), + ), + ), + ), + 'name' => array( + 'type' => 'string', + ), + 'method' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_open_ai_api_key', + array( + 'type' => 'string', + 'description' => __( 'The OpenAI API Key required for usage of Otter AI features.', 'otter-blocks' ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => '', + ) + ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_otter_ai_usage', + array( + 'type' => 'object', + 'description' => __( 'Usage of Otter AI features.', 'otter-blocks' ), + 'show_in_test' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'usage_count' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + ), + 'value' => array( + 'type' => 'string', + ), + ), + ), + 'default' => array(), + ), + 'prompts' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + ), + 'values' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'default' => array(), + ), + ), + ), + ), + 'default' => array(), + ) + ); } /** diff --git a/inc/render/class-form-multiple-choice.php b/inc/render/class-form-multiple-choice.php index 70a96299c..46809f755 100644 --- a/inc/render/class-form-multiple-choice.php +++ b/inc/render/class-form-multiple-choice.php @@ -32,11 +32,12 @@ public function render( $attributes ) { $options_array = explode( "\n", $options ); $is_required = isset( $attributes['isRequired'] ) ? boolval( $attributes['isRequired'] ) : false; $has_multiple_selection = isset( $attributes['multipleSelection'] ) ? boolval( $attributes['multipleSelection'] ) : false; + $mapped_name = isset( $attributes['mappedName'] ) ? $attributes['mappedName'] : $id; $output = '
'; if ( 'select' === $field_type ) { - $output .= $this->render_select_field( $label, $options_array, $id, $has_multiple_selection, $is_required ); + $output .= $this->render_select_field( $label, $options_array, $id, $mapped_name, $has_multiple_selection, $is_required ); } else { $output .= ''; @@ -50,7 +51,7 @@ public function render( $attributes ) { $field_value = implode( '_', explode( ' ', sanitize_title( $field_label ) ) ); $field_id = 'field-' . $field_value; - $output .= $this->render_field( $field_type, $field_label, $field_value, $id, $field_id, $is_required ); + $output .= $this->render_field( $field_type, $field_label, $field_value, $mapped_name, $field_id, $is_required ); } $output .= '
'; @@ -90,13 +91,14 @@ public function render_field( $type, $label, $value, $name, $id, $is_required = * @param string $label The label of the field. * @param array $options_array The options of the field. * @param string $id The id of the field. + * @param string $name The name of the field. * @param bool $is_multiple The multiple status of the field. * @param bool $is_required The required status of the field. * @return string */ - public function render_select_field( $label, $options_array, $id, $is_multiple, $is_required ) { + public function render_select_field( $label, $options_array, $id, $name, $is_multiple, $is_required ) { $output = ''; - $output .= ''; foreach ( $options_array as $field_label ) { @@ -114,7 +116,7 @@ public function render_select_field( $label, $options_array, $id, $is_multiple, /** * Render the required sign. - * + * * @param bool $is_required The required status of the field. * @return string */ diff --git a/inc/server/class-form-server.php b/inc/server/class-form-server.php index b2c940a5e..434765282 100644 --- a/inc/server/class-form-server.php +++ b/inc/server/class-form-server.php @@ -11,12 +11,13 @@ use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request; use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response; use ThemeIsle\GutenbergBlocks\Integration\Form_Email; -use ThemeIsle\GutenbergBlocks\Integration\Form_Field_Option_Data; +use ThemeIsle\GutenbergBlocks\Integration\Form_Field_WP_Option_Data; use ThemeIsle\GutenbergBlocks\Integration\Form_Providers; use ThemeIsle\GutenbergBlocks\Integration\Form_Settings_Data; use ThemeIsle\GutenbergBlocks\Integration\Form_Utils; use ThemeIsle\GutenbergBlocks\Integration\Mailchimp_Integration; use ThemeIsle\GutenbergBlocks\Integration\Sendinblue_Integration; +use ThemeIsle\GutenbergBlocks\Plugins\Stripe_API; use ThemeIsle\GutenbergBlocks\Pro; use WP_Error; use WP_HTTP_Response; @@ -146,6 +147,9 @@ public function init() { */ add_action( 'otter_form_after_submit', array( $this, 'after_submit' ) ); add_action( 'otter_form_after_submit', array( $this, 'send_error_email_to_admin' ), 999 ); + + add_action( 'otter_form_on_submission_confirmed', array( $this, 'apply_main_provider' ) ); + add_filter( 'otter_form_session_confirmation', array( $this, 'verify_confirmation_session' ) ); } /** @@ -176,6 +180,36 @@ public function register_routes() { ), ) ); + register_rest_route( + $namespace, + '/form/confirm', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'confirm_form_submission' ), + 'permission_callback' => function ( $request ) { + $session = $request->get_param( 'stripe_checkout' ); + + if ( apply_filters( 'otter_form_session_confirmation', $session ) ) { + return __return_true(); + } + return false; + }, + 'args' => array( + 'record_id' => array( + 'validate_callback' => function( $param, $request, $key ) { + return is_numeric( $param ); + }, + ), + 'session' => array( + 'validate_callback' => function( $param, $request, $key ) { + return is_string( $param ); + }, + ), + ), + ), + ) + ); register_rest_route( $namespace, '/form/editor', @@ -202,13 +236,13 @@ public function editor( $request ) { $data = new Form_Data_Request( $request ); $res = new Form_Data_Response(); - $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $data->get_payload_field( 'formOption' ) ); + $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $data->get_data_from_payload( 'formOption' ) ); $data->set_form_options( $form_options ); // Select the handler functions based on the provider. $provider = $form_options->get_provider(); - if ( $data->payload_has_field( 'provider' ) ) { - $provider = $data->get_payload_field( 'provider' ); + if ( $data->payload_has( 'provider' ) ) { + $provider = $data->get_data_from_payload( 'provider' ); } $provider_handlers = Form_Providers::$instance->get_provider_handlers( $provider, 'editor' ); @@ -216,9 +250,9 @@ public function editor( $request ) { return $res->set_code( $data->get_error_code() )->build_response(); } - if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $data->get( 'handler' ) ) ) { + if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $data->get_root_data( 'handler' ) ) ) { // Send the data to the provider. - return $provider_handlers[ $data->get( 'handler' ) ]( $data ); + return $provider_handlers[ $data->get_root_data( 'handler' ) ]( $data ); } else { $res->set_error( __( 'The email service provider was not registered!', 'otter-blocks' ) ); } @@ -239,14 +273,16 @@ public function frontend( $request ) { $res = new Form_Data_Response(); $form_data = new Form_Data_Request( $request ); + // Validate the form data. + $form_data = apply_filters( 'otter_form_validate_form', $form_data ); + + $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_data_from_payload( 'formOption' ) ); + $form_data->set_form_options( $form_options ); + $form_data = self::pull_fields_options_for_form( $form_data ); + try { - // Validate the form data. - $form_data = apply_filters( 'otter_form_data_validation', $form_data ); - $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_payload_field( 'formOption' ) ); - $form_data->set_form_options( $form_options ); - $form_data = $this->pull_fields_options_for_form( $form_data ); // Check bot validation. $form_data = apply_filters( 'otter_form_anti_spam_validation', $form_data ); @@ -268,16 +304,7 @@ public function frontend( $request ) { $res->add_response_field( 'redirectLink', $form_options->get_redirect_link() ); } - // Select the submit function based on the provider. - $provider_handlers = apply_filters( 'otter_select_form_provider', $form_data ); - - if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $form_data->get( 'handler' ) ) ) { - - // Send the data to the provider handler. - $form_data = $provider_handlers[ $form_data->get( 'handler' ) ]( $form_data ); - } else { - $res->set_code( Form_Data_Response::ERROR_PROVIDER_NOT_REGISTERED ); - } + $this->apply_main_provider( $form_data ); // Send the data to the provider. do_action( 'otter_form_after_submit', $form_data ); @@ -301,10 +328,58 @@ public function frontend( $request ) { $form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() ); $this->send_error_email( $form_data ); } finally { + $form_data->append_metadata( $res ); return $res->build_response(); } } + /** + * Confirm form submission. + * + * @param WP_REST_Request $request Form request. + * @return WP_Error|WP_HTTP_Response|WP_REST_Response + */ + public function confirm_form_submission( $request ) { + + $response = new Form_Data_Response(); + + try { + $response = apply_filters( 'otter_form_record_confirm', $response, $request ); + } catch ( Exception $e ) { + $response->set_code( Form_Data_Response::ERROR_RUNTIME_STRIPE_SESSION_VALIDATION ); + $response->add_reason( $e->getMessage() ); + } finally { + return $response->build_response(); + } + } + + /** + * Apply the main provider. + * + * @param Form_Data_Request|null $form_data Form data. + * @return void + */ + public function apply_main_provider( $form_data ) { + + if ( ! isset( $form_data ) ) { + return; + } + + if ( $form_data->has_error() || $form_data->is_temporary() ) { + return; + } + + $provider_handlers = apply_filters( 'otter_select_form_provider', $form_data ); + + if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $form_data->get_root_data( 'handler' ) ) ) { + + // Send the data to the provider handler. + $provider_handlers[ $form_data->get_root_data( 'handler' ) ]( $form_data ); + } else { + $form_data->set_error( Form_Data_Response::ERROR_PROVIDER_NOT_REGISTERED ); + } + } + /** * Send Email using SMTP. * @@ -323,7 +398,7 @@ public function send_default_email( $form_data ) { } try { - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_wp_options(); $can_send_email = substr( $form_options->get_submissions_save_location(), -strlen( 'email' ) ) === 'email'; @@ -340,8 +415,8 @@ public function send_default_email( $form_data ) { $to = sanitize_email( get_site_option( 'admin_email' ) ); // Check if we need to send it to another user email. - if ( $form_data->payload_has_field( 'formOption' ) ) { - $option_name = $form_data->get_payload_field( 'formOption' ); + if ( $form_data->payload_has( 'formOption' ) ) { + $option_name = $form_data->get_data_from_payload( 'formOption' ); $form_emails = get_option( 'themeisle_blocks_form_emails' ); foreach ( $form_emails as $form ) { @@ -467,10 +542,10 @@ public function change_provider_based_on_consent( $form_data ) { // If there is no consent, change the service to send only an email. if ( - 'submit-subscribe' === $form_data->get_form_options()->get_action() && + 'submit-subscribe' === $form_data->get_wp_options()->get_action() && ( - ! $form_data->payload_has_field( 'consent' ) || - ! $form_data->get_payload_field( 'consent' ) + ! $form_data->payload_has( 'consent' ) || + ! $form_data->get_data_from_payload( 'consent' ) ) ) { $form_data->change_provider( 'default' ); @@ -497,9 +572,9 @@ public function after_submit( $form_data ) { // Send also an email to the form editor/owner with the data alongside the subscription. if ( - 'submit-subscribe' === $form_data->get_form_options()->get_action() && - $form_data->get_form_options()->has_provider() && - 'default' !== $form_data->get_form_options()->get_provider() + 'submit-subscribe' === $form_data->get_wp_options()->get_action() && + $form_data->get_wp_options()->has_provider() && + 'default' !== $form_data->get_wp_options()->get_provider() ) { $this->send_default_email( $form_data ); } @@ -522,13 +597,13 @@ public function anti_spam_validation( $form_data ) { } if ( - $form_data->payload_has_field( 'antiSpamTime' ) && - is_numeric( $form_data->get_payload_field( 'antiSpamTime' ) ) && - $form_data->payload_has_field( 'antiSpamHoneyPot' ) + $form_data->payload_has( 'antiSpamTime' ) && + is_numeric( $form_data->get_data_from_payload( 'antiSpamTime' ) ) && + $form_data->payload_has( 'antiSpamHoneyPot' ) ) { if ( - $form_data->get_payload_field( 'antiSpamTime' ) >= self::ANTI_SPAM_TIMEOUT && - '' === $form_data->get_payload_field( 'antiSpamHoneyPot' ) + $form_data->get_data_from_payload( 'antiSpamTime' ) >= self::ANTI_SPAM_TIMEOUT && + '' === $form_data->get_data_from_payload( 'antiSpamHoneyPot' ) ) { return $form_data; } @@ -570,8 +645,8 @@ public static function send_test_email( $form_data ) { $email_body = Form_Email::instance()->build_test_email( $form_data ); // Sent the form date to the admin site as a default behaviour. $to = sanitize_email( get_site_option( 'admin_email' ) ); - if ( $form_data->payload_has_field( 'to' ) && '' !== $form_data->get_payload_field( 'to' ) ) { - $to = $form_data->get_payload_field( 'to' ); + if ( $form_data->payload_has( 'to' ) && '' !== $form_data->get_data_from_payload( 'to' ) ) { + $to = $form_data->get_data_from_payload( 'to' ); } $headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . get_bloginfo( 'name', 'display' ) . '<' . $to . '>' ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail @@ -598,7 +673,7 @@ public function get_integration_data( $form_data ) { try { $service = null; - switch ( $form_data->get_payload_field( 'provider' ) ) { + switch ( $form_data->get_data_from_payload( 'provider' ) ) { case 'mailchimp': $service = new Mailchimp_Integration(); break; @@ -610,9 +685,9 @@ public function get_integration_data( $form_data ) { } if ( isset( $service ) ) { - $valid_api_key = $service::validate_api_key( $form_data->get_payload_field( 'apiKey' ) ); + $valid_api_key = $service::validate_api_key( $form_data->get_data_from_payload( 'apiKey' ) ); if ( $valid_api_key['valid'] ) { - $service->set_api_key( $form_data->get_payload_field( 'apiKey' ) ); + $service->set_api_key( $form_data->get_data_from_payload( 'apiKey' ) ); $res->set_response( $service->get_information_from_provider( $form_data ) ); } else { $res->set_code( $valid_api_key['code'] ); @@ -635,7 +710,7 @@ public function get_integration_data( $form_data ) { */ public function test_subscription_service( $form_data ) { $res = new Form_Data_Response(); - $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_payload_field( 'formOption' ) ); + $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_data_from_payload( 'formOption' ) ); try { $service = null; @@ -689,12 +764,12 @@ public function subscribe_to_service( $form_data ) { return $form_data; } - if ( $form_data->has_error() ) { + if ( $form_data->has_error() || $form_data->is_temporary() ) { return $form_data; } // Get the first email from form. - $email = $form_data->get_email_from_form_input(); + $email = $form_data->get_first_email_from_input_fields(); if ( '' === $email ) { $form_data->set_error( Form_Data_Response::ERROR_MISSING_EMAIL, __( 'Marketing Integration is active, but there is no Email field in the form. Please check your Form block settings in the page.', 'otter-blocks' ) ); @@ -702,16 +777,16 @@ public function subscribe_to_service( $form_data ) { } if ( - 'submit-subscribe' === $form_data->get_form_options()->get_action() && - $form_data->payload_has_field( 'consent' ) && - ! $form_data->get_payload_field( 'consent' ) + 'submit-subscribe' === $form_data->get_wp_options()->get_action() && + $form_data->payload_has( 'consent' ) && + ! $form_data->get_data_from_payload( 'consent' ) ) { return $form_data; } try { // Get the api credentials from the Form block. - $wp_options_form = $form_data->get_form_options(); + $wp_options_form = $form_data->get_wp_options(); $error_code = $wp_options_form->check_data(); @@ -756,14 +831,14 @@ public function subscribe_to_service( $form_data ) { */ public function has_required_data( $form_data ) { - $main_fields_set = $form_data->are_fields_set( + $main_fields_set = $form_data->are_root_data_set( array( 'handler', 'payload', ) ); - $required_payload_fields = $form_data->are_payload_fields_set( + $required_payload_fields = $form_data->are_payload_data_set( array( 'nonceValue', 'postUrl', @@ -772,7 +847,7 @@ public function has_required_data( $form_data ) { ) ); - $is_nonce_valid = wp_verify_nonce( $form_data->get_payload_field( 'nonceValue' ), 'form-verification' ); + $is_nonce_valid = wp_verify_nonce( $form_data->get_data_from_payload( 'nonceValue' ), 'form-verification' ); return $main_fields_set && $required_payload_fields && $is_nonce_valid; } @@ -823,24 +898,24 @@ public function check_form_captcha( $form_data ) { return $form_data; } - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_wp_options(); if ( $form_options->form_has_captcha() && ( - ! $form_data->payload_has_field( 'token' ) || - '' === $form_data->get_payload_field( 'token' ) + ! $form_data->payload_has( 'token' ) || + '' === $form_data->get_data_from_payload( 'token' ) ) ) { $form_data->set_error( Form_Data_Response::ERROR_MISSING_CAPTCHA ); } - if ( $form_data->payload_has_field( 'token' ) ) { + if ( $form_data->payload_has( 'token' ) ) { $secret = get_option( 'themeisle_google_captcha_api_secret_key' ); $resp = wp_remote_post( apply_filters( 'otter_blocks_recaptcha_verify_url', 'https://www.google.com/recaptcha/api/siteverify' ), array( - 'body' => 'secret=' . $secret . '&response=' . $form_data->get_payload_field( 'token' ), + 'body' => 'secret=' . $secret . '&response=' . $form_data->get_data_from_payload( 'token' ), 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', ], @@ -888,9 +963,9 @@ public function build_email_error_content( $content ) { * @since 2.0.3 */ public function get_email_from_form_input( Form_Data_Request $data ) { - $inputs = $data->get_payload_field( 'formInputsData' ); + $inputs = $data->get_data_from_payload( 'formInputsData' ); if ( is_array( $inputs ) ) { - foreach ( $data->get_payload_field( 'formInputsData' ) as $input_field ) { + foreach ( $inputs as $input_field ) { if ( isset( $input_field['type'] ) && 'email' == $input_field['type'] ) { return $input_field['value']; } @@ -916,7 +991,7 @@ public function check_form_files( $form_data ) { return $form_data; } - $inputs = $form_data->get_form_inputs(); + $inputs = $form_data->get_fields(); foreach ( $inputs as $input ) { if ( Form_Utils::is_file_field( $input ) && ! Form_Utils::is_file_field_valid( $input ) ) { @@ -934,7 +1009,7 @@ public function check_form_files( $form_data ) { * @param Form_Data_Request $form_data The form data. * @since 2.2.3 */ - public function pull_fields_options_for_form( $form_data ) { + public static function pull_fields_options_for_form( $form_data ) { if ( ! ( $form_data instanceof Form_Data_Request ) || $form_data->has_error() ) { return $form_data; } @@ -945,22 +1020,55 @@ public function pull_fields_options_for_form( $form_data ) { return $form_data; } - foreach ( $form_data->get_form_inputs() as $input ) { + foreach ( $form_data->get_fields() as $input ) { if ( isset( $input['metadata']['fieldOptionName'] ) ) { $field_name = $input['metadata']['fieldOptionName']; foreach ( $global_fields_options as $field ) { if ( isset( $field['fieldOptionName'] ) && $field['fieldOptionName'] === $field_name ) { - $new_field = new Form_Field_Option_Data( $field_name, $field['fieldOptionType'], $field['options'] ); - $form_data->add_field_option( $new_field ); + $new_field = new Form_Field_WP_Option_Data( $field_name, $field['fieldOptionType'] ); + if ( isset( $field['options'] ) ) { + $new_field->set_options( $field['options'] ); + } + if ( isset( $field['stripe'] ) ) { + $new_field->set_stripe_product_info( $field['stripe'] ); + } + $form_data->add_field_wp_option( $new_field ); break; } } } } + $required_fields = $form_data->get_wp_options()->get_required_fields(); + + foreach ( $required_fields as $required_field ) { + foreach ( $global_fields_options as $field ) { + if ( isset( $field['fieldOptionName'] ) && $field['fieldOptionName'] === $required_field ) { + $new_field = new Form_Field_WP_Option_Data( $required_field, $field['fieldOptionType'] ); + if ( isset( $field['options'] ) ) { + $new_field->set_options( $field['options'] ); + } + if ( isset( $field['stripe'] ) ) { + $new_field->set_stripe_product_info( $field['stripe'] ); + } + $form_data->add_field_wp_option( $new_field ); + break; + } + } + } + return $form_data; } + /** + * Verify the given confirmation session. + * + * @param string $session The session id. + * @return bool + */ + public function verify_confirmation_session( $session ) { + return ! empty( $session ) && is_string( $session ); + } /** * The instance method for the static class. * Defines and returns the instance of the static class. diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php new file mode 100644 index 000000000..25198c8d9 --- /dev/null +++ b/inc/server/class-prompt-server.php @@ -0,0 +1,397 @@ +namespace . $this->version; + + register_rest_route( + $namespace, + '/prompt', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_prompts' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + ), + ) + ); + + register_rest_route( + $namespace, + '/generate', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'forward_prompt' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + ), + ) + ); + } + + /** + * Forward the prompt to OpenAI API. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response + */ + public function forward_prompt( $request ) { + $open_ai_endpoint = 'https://api.openai.com/v1/chat/completions'; + + // Get the body from request and decode it. + $body = $request->get_body(); + $body = json_decode( $body, true ); + + $api_key = get_option( 'themeisle_open_ai_api_key' ); + + // Extract the data from keys that start with 'otter_'. + $otter_data = array_filter( + $body, + function ( $key ) { + return 0 === strpos( $key, 'otter_' ); + }, + ARRAY_FILTER_USE_KEY + ); + + // Remove the values which keys start with 'otter_'. + $body = array_diff_key( $body, $otter_data ); + + $response = wp_remote_post( + $open_ai_endpoint, + array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $api_key, + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $body ), + 'timeout' => 2 * MINUTE_IN_SECONDS, + ) + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $body = wp_remote_retrieve_body( $response ); + $body = json_decode( $body, true ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new \WP_Error( 'rest_invalid_json', __( 'Could not parse the response from OpenAI. Try again.', 'otter-blocks' ), array( 'status' => 400 ) ); + } + + return new \WP_REST_Response( $body, wp_remote_retrieve_response_code( $response ) ); + } + + /** + * Get prompts. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response + */ + public function get_prompts( $request ) { + $response = array( + 'prompts' => array(), + 'code' => '0', + 'error' => '', + ); + + // Get the saved prompts. + $prompts = get_transient( $this->transient_prompts ); + + if ( false === $prompts ) { + /** + * If we don't have the prompts saved, we need to retrieve them from the server. Once retrieved, we save them in a transient and return them. + */ + $response = $this->retrieve_prompts_from_server(); + } + + if ( '0' === $response['code'] ) { + if ( $request->get_param( 'name' ) !== null ) { + $prompts = ! empty( $prompts ) ? $prompts : $response['prompts']; + + // Prompt can be filtered by name. By filtering by name, we can get only the prompt we need and save some bandwidth. + $single_prompt = array_values( + array_filter( + $prompts, + function ( $prompt ) use ( $request ) { + return $prompt['otter_name'] === $request->get_param( 'name' ); + } + ) + ); + + if ( empty( $single_prompt ) ) { + $response['prompts'] = $prompts; + $response['code'] = '1'; + $response['error'] = __( 'Something went wrong when preparing the data for this feature.', 'otter-blocks' ); + } else { + $response['prompts'] = $single_prompt; + } + } else { + $response['prompts'] = $prompts; + } + } + + + return rest_ensure_response( $response ); + } + + /** + * + * Retrieve prompts from server. + * + * @return array + */ + public function retrieve_prompts_from_server() { + + if ( false !== get_transient( $this->timeout_transient ) ) { + return array( + 'response' => array(), + 'code' => '3', + 'error' => __( 'Timeout is active. Please try again in', 'otter-blocks' ) . 5 . __( 'minutes.', 'otter-blocks' ), + ); + } + + $url = add_query_arg( + array( + 'site_url' => get_site_url(), + 'license_id' => apply_filters( 'product_otter_license_key', 'free' ), + 'cache' => gmdate( 'u' ), + 'isValid' => boolval( get_option( 'themeisle_open_ai_api_key', false ) ) ? 'true' : 'false', + ), + 'https://api.themeisle.com/templates-cloud/otter-prompts' + ); + + $response = ''; + + if ( function_exists( 'vip_safe_wp_remote_get' ) ) { + $response = vip_safe_wp_remote_get( esc_url_raw( $url ) ); + } else { + $response = wp_remote_get( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + } + + $response = wp_remote_retrieve_body( $response ); + $response = json_decode( $response, true ); + + if ( ! is_array( $response ) || 0 === count( $response ) || ! $this->check_prompt_structure( $response ) ) { + set_transient( $this->timeout_transient, '1', 5 * MINUTE_IN_SECONDS ); + return array( + 'response' => array(), + 'code' => '2', + 'error' => __( 'Invalid data from central server. Please try again in', 'otter-blocks' ) . 5 . __( 'minutes.', 'otter-blocks' ), + ); + } + + set_transient( $this->transient_prompts, $response, WEEK_IN_SECONDS ); + + return array( + 'prompts' => $response, + 'code' => '0', + 'error' => '', + ); + } + + /** + * Check if the prompt structure is valid. + * + * @param mixed $response Response from the server. + * @return bool + */ + public function check_prompt_structure( $response ) { + if ( ! isset( $response ) ) { + return false; + } + + if ( ! is_array( $response ) ) { + return false; + } + + if ( 0 === count( $response ) ) { + return false; + } + + return true; + } + + /** + * Record prompt usage. + * + * @param array $otter_metadata The metadata from the prompt usage request. + * @return void + * @phpstan-ignore-next-line + */ + private function record_prompt_usage( $otter_metadata ) { + if ( ! isset( $otter_metadata['otter_used_action'] ) || ! isset( $otter_metadata['otter_user_content'] ) ) { + return; + } + + $action = $otter_metadata['otter_used_action']; + $user_content = $otter_metadata['otter_user_content']; + + $usage = get_option( 'themeisle_otter_ai_usage' ); + + if ( ! is_array( $usage ) ) { + $usage = array( + 'usage_count' => array(), + 'prompts' => array(), + ); + } + + if ( ! is_array( $usage['usage_count'] ) ) { + $usage['usage_count'] = array(); + } + + if ( ! is_array( $usage['prompts'] ) ) { + $usage['prompts'] = array(); + } + + $is_missing = true; + + foreach ( $usage['usage_count'] as &$u ) { + if ( isset( $u['key'] ) && $u['key'] === $action ) { + $u['value']++; + $is_missing = false; + } + } + + unset( $u ); + + if ( $is_missing ) { + $usage['usage_count'][] = array( + 'key' => $action, + 'value' => 1, + ); + } + + $is_missing = true; + + foreach ( $usage['prompts'] as &$u ) { + if ( isset( $u['key'] ) && $u['key'] === $action ) { + $u['values'][] = $user_content; + $is_missing = false; + + // Keep only the last 10 prompts. + if ( count( $u['values'] ) > 10 ) { + array_shift( $u['values'] ); + } + } + } + + unset( $u ); + + if ( $is_missing ) { + $usage['prompts'][] = array( + 'key' => $action, + 'values' => array( $user_content ), + ); + } + + update_option( 'themeisle_otter_ai_usage', $usage ); + } + + + /** + * The instance method for the static class. + * Defines and returns the instance of the static class. + * + * @static + * @since 1.7.0 + * @access public + * @return Prompt_Server + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + self::$instance->init(); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @access public + * @since 1.7.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @access public + * @since 1.7.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); + } +} diff --git a/plugins/otter-pro/inc/class-main.php b/plugins/otter-pro/inc/class-main.php index 4b7fabf6c..406876b67 100644 --- a/plugins/otter-pro/inc/class-main.php +++ b/plugins/otter-pro/inc/class-main.php @@ -105,6 +105,8 @@ public function register_blocks( $blocks ) { 'product-upsells', 'review-comparison', 'form-file', + 'form-hidden-field', + 'form-stripe-field', ); $blocks = array_merge( $blocks, $pro_blocks ); @@ -136,6 +138,8 @@ public function register_dynamic_blocks( $dynamic_blocks ) { 'product-upsells' => '\ThemeIsle\OtterPro\Render\WooCommerce\Product_Upsells_Block', 'review-comparison' => '\ThemeIsle\OtterPro\Render\Review_Comparison_Block', 'form-file' => '\ThemeIsle\OtterPro\Render\Form_File_Block', + 'form-hidden-field' => '\ThemeIsle\OtterPro\Render\Form_Hidden_Block', + 'form-stripe-field' => '\ThemeIsle\OtterPro\Render\Form_Stripe_Block', ); $dynamic_blocks = array_merge( $dynamic_blocks, $blocks ); @@ -157,6 +161,7 @@ public function register_blocks_css( $blocks ) { '\ThemeIsle\OtterPro\CSS\Blocks\Business_Hours_Item_CSS', '\ThemeIsle\OtterPro\CSS\Blocks\Review_Comparison_CSS', '\ThemeIsle\OtterPro\CSS\Blocks\Form_File_CSS', + '\ThemeIsle\OtterPro\CSS\Blocks\Form_Stripe_Field_CSS', ); $blocks = array_merge( $blocks, $pro_blocks ); diff --git a/plugins/otter-pro/inc/css/blocks/class-form-stripe-field-css.php b/plugins/otter-pro/inc/css/blocks/class-form-stripe-field-css.php new file mode 100644 index 000000000..33731b283 --- /dev/null +++ b/plugins/otter-pro/inc/css/blocks/class-form-stripe-field-css.php @@ -0,0 +1,70 @@ +add_item( + array( + 'properties' => array( + array( + 'property' => '--label-color', + 'value' => 'labelColor', + ), + array( + 'property' => '--stripe-border-color', + 'value' => 'borderColor', + ), + array( + 'property' => '--stripe-border-radius', + 'value' => 'borderRadius', + 'format' => function( $value ) { + return CSS_Utility::render_box( $value ); + }, + ), + array( + 'property' => '--stripe-border-width', + 'value' => 'borderWidth', + 'format' => function( $value ) { + return CSS_Utility::render_box( $value ); + }, + ), + ), + ) + ); + + $style = $css->generate(); + + return $style; + } + +} diff --git a/plugins/otter-pro/inc/plugins/class-form-emails-storing.php b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php index 7efdf83fe..f69dfa984 100644 --- a/plugins/otter-pro/inc/plugins/class-form-emails-storing.php +++ b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php @@ -8,6 +8,9 @@ namespace ThemeIsle\OtterPro\Plugins; use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request; +use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response; +use ThemeIsle\GutenbergBlocks\Integration\Form_Settings_Data; +use ThemeIsle\GutenbergBlocks\Plugins\Stripe_API; use ThemeIsle\GutenbergBlocks\Server\Form_Server; use WP_Post; use WP_Query; @@ -44,6 +47,7 @@ public function init() { add_action( 'init', array( $this, 'create_form_records_type' ) ); add_action( 'admin_init', array( $this, 'set_form_records_cap' ), 10, 0 ); add_action( 'otter_form_after_submit', array( $this, 'store_form_record' ) ); + add_action( 'admin_head', array( $this, 'add_style' ) ); // Customize the wp_list_table. @@ -67,6 +71,13 @@ public function init() { add_action( 'add_meta_boxes', array( $this, 'add_form_record_meta_box' ) ); add_action( 'admin_menu', array( $this, 'handle_admin_menu' ) ); add_action( 'save_post', array( $this, 'form_record_save_meta_box' ), 10, 2 ); + + add_filter( 'otter_form_record_confirm', array( $this, 'confirm_submission' ), 10, 2 ); + + add_action( 'draft_to_unread', array( $this, 'apply_hooks_on_draft_transition' ), 10 ); + add_action( 'otter_form_update_record_meta_dump', array( $this, 'update_submission_dump_data' ), 10, 2 ); + add_action( 'otter_form_automatic_confirmation', array( $this, 'move_old_stripe_draft_sessions_to_unread' ) ); + add_action( 'wp', array( $this, 'schedule_automatic_confirmation' ) ); } /** @@ -175,20 +186,24 @@ public function store_form_record( $form_data ) { return $form_data; } - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_wp_options(); if ( ! isset( $form_options ) ) { return $form_data; } - if ( false === strpos( $form_options->get_submissions_save_location(), 'database' ) ) { + if ( $form_data->is_duplicate() ) { + return $form_data; + } + + if ( false === strpos( $form_options->get_submissions_save_location(), 'database' ) && ! $form_data->is_temporary() ) { return $form_data; } $post_id = wp_insert_post( array( 'post_type' => self::FORM_RECORD_TYPE, - 'post_status' => 'unread', + 'post_status' => $form_data->is_temporary() ? 'draft' : 'unread', ) ); @@ -206,20 +221,24 @@ public function store_form_record( $form_data ) { $meta = array( 'form' => array( - 'label' => 'Form', - 'value' => $form_data->get_payload_field( 'formId' ), + 'label' => __( 'Form', 'otter-blocks' ), + 'value' => $form_data->get_data_from_payload( 'formId' ), ), 'post_url' => array( - 'label' => 'Post URL', - 'value' => $form_data->get_payload_field( 'postUrl' ), + 'label' => __( 'Post URL', 'otter-blocks' ), + 'value' => $form_data->get_data_from_payload( 'postUrl' ), ), 'post_id' => array( - 'label' => 'Post ID', - 'value' => $form_data->get_payload_field( 'postId' ), + 'label' => __( 'Post ID', 'otter-blocks' ), + 'value' => $form_data->get_data_from_payload( 'postId' ), + ), + 'dump' => array( + 'label' => __( 'Dumped data', 'otter-blocks' ), + 'value' => $form_data->is_temporary() ? $form_data->dump_data() : array(), ), ); - $form_inputs = $form_data->get_form_inputs(); + $form_inputs = $form_data->get_fields(); $uploaded_files = $form_data->get_uploaded_files_path(); $media_files = $form_data->get_files_loaded_to_media_library(); @@ -273,6 +292,9 @@ public function store_form_record( $form_data ) { } add_post_meta( $post_id, self::FORM_RECORD_META_KEY, $meta ); + + $form_data->metadata['otter_form_record_id'] = $post_id; + return $form_data; } @@ -699,13 +721,24 @@ public function fields_meta_box_markup( $post ) { $meta = get_post_meta( $post->ID, self::FORM_RECORD_META_KEY, true ); $previous_field_option = ''; + if ( empty( $meta ) ) { return; } + + $inputs = array(); + foreach ( $meta['inputs'] as $id => $field ) { + if ( empty( $field ) || 'stripe-field' === $field['type'] ) { + continue; + } + + $inputs[ $id ] = $field; + } + ?> - $field ) { ?> + $field ) { ?>