diff --git a/blocks.json b/blocks.json index 4cab781da..4d6889618 100644 --- a/blocks.json +++ b/blocks.json @@ -102,6 +102,9 @@ "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": { diff --git a/inc/integrations/api/form-request-data.php b/inc/integrations/api/form-request-data.php index 2431cb99b..743807d08 100644 --- a/inc/integrations/api/form-request-data.php +++ b/inc/integrations/api/form-request-data.php @@ -43,7 +43,7 @@ class Form_Data_Request { /** * Form fields options. * - * @var array + * @var array * @since 2.2.3 */ protected $form_fields_options = array(); @@ -325,7 +325,7 @@ public function get_form_inputs() { * @return Form_Settings_Data|null * @since 2.0.0 */ - public function get_form_options() { + public function get_form_wp_options() { return $this->form_options; } @@ -475,11 +475,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(); } @@ -494,7 +494,7 @@ public function add_field_option( $field_option ) { * @param string $field_option_name The field option name. * @return void */ - public function remove_field_option( $field_option_name ) { + public function remove_field_wp_option( $field_option_name ) { if ( isset( $this->form_fields_options[ $field_option_name ] ) ) { unset( $this->form_fields_options[ $field_option_name ] ); } @@ -503,9 +503,9 @@ public function remove_field_option( $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; } @@ -513,7 +513,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 ] ) ) { diff --git a/inc/integrations/class-form-field-option-data.php b/inc/integrations/class-form-field-WPOption-data.php similarity index 75% rename from inc/integrations/class-form-field-option-data.php rename to inc/integrations/class-form-field-WPOption-data.php index 552f6fe8a..d9c22a6b0 100644 --- a/inc/integrations/class-form-field-option-data.php +++ b/inc/integrations/class-form-field-WPOption-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; } /** @@ -91,6 +96,15 @@ public function get_option( $option_name ) { return $this->options[ $option_name ]; } + /** + * Get the stripe data of the field option. + * + * @return array The stripe data of the field option. + */ + public function get_stripe_product_info() { + return $this->stripe_product_info; + } + /** * Set the option of the field option. * @@ -128,6 +142,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 +179,8 @@ public function has_options() { return ! empty( $this->options ); } + + /** * Check if the field option has type. * @@ -164,4 +198,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..1e37f4789 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_form_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 c8cee7e76..edd1c62b3 100644 --- a/inc/integrations/class-form-settings-data.php +++ b/inc/integrations/class-form-settings-data.php @@ -128,6 +128,13 @@ class Form_Settings_Data { */ private $webhook_id = ''; + /** + * The required fields. + * + * @var array + */ + private $required_fields = array(); + /** * The default constructor. * @@ -251,6 +258,9 @@ public static function get_form_setting_from_wordpress_options( $form_option ) { 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; @@ -707,4 +717,34 @@ private function set_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/plugins/class-options-settings.php b/inc/plugins/class-options-settings.php index 8b7a0dd25..26c4fe0eb 100644 --- a/inc/plugins/class-options-settings.php +++ b/inc/plugins/class-options-settings.php @@ -216,6 +216,18 @@ public function register_settings() { ) ); + register_setting( + 'themeisle_blocks_settings', + 'themeisle_stripe_public_api_key', + array( + 'type' => 'string', + 'description' => __( 'Stripe Public API key for the Stripe Field Block.', 'otter-blocks' ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => '', + ) + ); + register_setting( 'themeisle_blocks_settings', 'themeisle_google_captcha_api_site_key', @@ -322,6 +334,14 @@ function ( $item ) { $item['submissionsSaveLocation'] = sanitize_text_field( $item['submissionsSaveLocation'] ); } + if ( isset( $item['requiredFields'] ) ) { + if ( is_array( $item['requiredFields'] ) ) { + $item['requiredFields'] = array_map( 'sanitize_text_field', $item['requiredFields'] ); + } else { + $item['requiredFields'] = array(); + } + } + return $item; }, $array @@ -397,6 +417,12 @@ function ( $item ) { 'webhookId' => array( 'type' => 'string', ), + 'requiredFields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), ), ), ), @@ -436,6 +462,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 @@ -474,6 +512,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, + ), + ), + + ), ), ), ), diff --git a/inc/server/class-form-server.php b/inc/server/class-form-server.php index c586f4f51..8b753abc0 100644 --- a/inc/server/class-form-server.php +++ b/inc/server/class-form-server.php @@ -11,7 +11,7 @@ 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; @@ -238,7 +238,7 @@ public function frontend( $request ) { try { // Validate the form data. - $form_data = apply_filters( 'otter_form_data_validation', $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_payload_field( 'formOption' ) ); $form_data->set_form_options( $form_options ); @@ -317,7 +317,7 @@ public function send_default_email( $form_data ) { } try { - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_form_wp_options(); $can_send_email = substr( $form_options->get_submissions_save_location(), -strlen( 'email' ) ) === 'email'; @@ -461,7 +461,7 @@ 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_form_wp_options()->get_action() && ( ! $form_data->payload_has_field( 'consent' ) || ! $form_data->get_payload_field( 'consent' ) @@ -491,9 +491,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_form_wp_options()->get_action() && + $form_data->get_form_wp_options()->has_provider() && + 'default' !== $form_data->get_form_wp_options()->get_provider() ) { $this->send_default_email( $form_data ); } @@ -690,7 +690,7 @@ public function subscribe_to_service( $form_data ) { } if ( - 'submit-subscribe' === $form_data->get_form_options()->get_action() && + 'submit-subscribe' === $form_data->get_form_wp_options()->get_action() && $form_data->payload_has_field( 'consent' ) && ! $form_data->get_payload_field( 'consent' ) ) { @@ -699,7 +699,7 @@ public function subscribe_to_service( $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_form_wp_options(); $error_code = $wp_options_form->check_data(); @@ -811,7 +811,7 @@ public function check_form_captcha( $form_data ) { return $form_data; } - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_form_wp_options(); if ( $form_options->form_has_captcha() && @@ -938,14 +938,38 @@ public function pull_fields_options_for_form( $form_data ) { $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_form_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( $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; + } + } + } + return $form_data; } diff --git a/plugins/otter-pro/inc/class-main.php b/plugins/otter-pro/inc/class-main.php index 521ca8a16..93330b055 100644 --- a/plugins/otter-pro/inc/class-main.php +++ b/plugins/otter-pro/inc/class-main.php @@ -106,6 +106,7 @@ public function register_blocks( $blocks ) { 'review-comparison', 'form-file', 'form-hidden-field', + 'form-stripe-field', ); $blocks = array_merge( $blocks, $pro_blocks ); @@ -138,6 +139,7 @@ public function register_dynamic_blocks( $dynamic_blocks ) { '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 ); 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 efef2ce4e..29340324c 100644 --- a/plugins/otter-pro/inc/plugins/class-form-emails-storing.php +++ b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php @@ -175,7 +175,7 @@ public function store_form_record( $form_data ) { return $form_data; } - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_form_wp_options(); if ( ! isset( $form_options ) ) { return $form_data; diff --git a/plugins/otter-pro/inc/plugins/class-form-pro-features.php b/plugins/otter-pro/inc/plugins/class-form-pro-features.php index f18b49ad2..14435e519 100644 --- a/plugins/otter-pro/inc/plugins/class-form-pro-features.php +++ b/plugins/otter-pro/inc/plugins/class-form-pro-features.php @@ -9,6 +9,7 @@ use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request; use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response; +use ThemeIsle\GutenbergBlocks\Plugins\Stripe_API; use ThemeIsle\GutenbergBlocks\Server\Form_Server; use WP_Error; use WP_HTTP_Response; @@ -35,6 +36,7 @@ public function init() { add_action( 'otter_form_after_submit', array( $this, 'clean_files_from_uploads' ) ); add_action( 'otter_form_after_submit', array( $this, 'send_autoresponder' ), 99 ); add_action( 'otter_form_after_submit', array( $this, 'trigger_webhook' ) ); + add_action( 'otter_form_after_submit', array( $this, 'create_stripe_session' ) ); } } @@ -224,7 +226,7 @@ public function clean_files_from_uploads( $form_data ) { } try { - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_form_wp_options(); $can_delete = true; if ( isset( $form_options ) ) { @@ -318,7 +320,7 @@ public function send_autoresponder( $form_data ) { ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || $form_data->has_error() || - ! $form_data->get_form_options()->has_autoresponder() + ! $form_data->get_form_wp_options()->has_autoresponder() ) { return $form_data; } @@ -332,9 +334,9 @@ public function send_autoresponder( $form_data ) { try { $headers[] = 'Content-Type: text/html'; - $headers[] = 'From: ' . ( $form_data->get_form_options()->has_from_name() ? sanitize_text_field( $form_data->get_form_options()->get_from_name() ) : get_bloginfo( 'name', 'display' ) ); + $headers[] = 'From: ' . ( $form_data->get_form_wp_options()->has_from_name() ? sanitize_text_field( $form_data->get_form_wp_options()->get_from_name() ) : get_bloginfo( 'name', 'display' ) ); - $autoresponder = $form_data->get_form_options()->get_autoresponder(); + $autoresponder = $form_data->get_form_wp_options()->get_autoresponder(); $body = $this->replace_magic_tags( $autoresponder['body'], $form_data->get_form_inputs() ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail @@ -364,13 +366,13 @@ public function trigger_webhook( $form_data ) { ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || $form_data->has_error() || - empty( $form_data->get_form_options()->get_webhook_id() ) + empty( $form_data->get_form_wp_options()->get_webhook_id() ) ) { return $form_data; } try { - $form_webhook_id = $form_data->get_form_options()->get_webhook_id(); + $form_webhook_id = $form_data->get_form_wp_options()->get_webhook_id(); $webhooks = get_option( 'themeisle_webhooks_options', array() ); @@ -512,6 +514,108 @@ public function replace_magic_tags( $content, $form_inputs ) { return $content; } + /** + * Create a Stripe session. + * + * @param Form_Data_Request $form_data The form data. + */ + public function create_stripe_session( $form_data ) { + if ( ! isset( $form_data ) ) { + return $form_data; + } + + if ( + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() + ) { + return $form_data; + } + + $has_stripe = false; + + $fields_options = $form_data->get_wp_fields_options(); + + foreach ( $fields_options as $field ) { + if ( $field->has_type() && 'stripe' === $field->get_type() ) { + $has_stripe = true; + break; + } + } + + if ( ! $has_stripe ) { + return $form_data; + } + + $required_fields = $form_data->get_form_wp_options()->get_required_fields(); + + $products_to_process = array(); + + foreach ( $fields_options as $field ) { + if ( + $field->has_name() && + 'stripe' === $field->get_type() && + in_array( $field->get_name(), $required_fields, true ) && + $field->has_stripe_product_info() + ) { + $products_to_process[] = $field->get_stripe_product_info(); + } + } + + if ( empty( $products_to_process ) ) { + return $form_data; + } + + $payload = array( + 'mode' => 'payment', + ); + + $permalink = add_query_arg( + array( + 'stripe_session_id' => '{CHECKOUT_SESSION_ID}', + ), + $form_data->get_payload_field( 'postUrl' ) + ); + + $payload['success_url'] = $permalink; + $payload['cancel_url'] = $permalink; + + $customer_email = $form_data->get_email_from_form_input(); + if ( ! empty( $customer_email ) ) { + $payload['customer_email'] = $customer_email; + } + + // Prepare the line items for the Stripe session request. + $line_items = array(); + foreach ( $products_to_process as $product ) { + $line_items[] = array( + 'price' => $product['price'], + 'quantity' => 1, + ); + } + $payload['line_items'] = $line_items; + + + // Create the metadata array for the Stripe session request. + // TODO: Save also the record ID. + $raw_metadata = $this->prepare_webhook_payload( array(), $form_data, null ); + $metadata = array(); + foreach ( $raw_metadata as $key => $value ) { + $metadata[ mb_substr( $key, 0, 40 ) ] = mb_substr( wp_json_encode( $value ), 0, 500 ); + } + $payload['metadata'] = $metadata; + + $stripe = new Stripe_API(); + + $session = $stripe->create_request( + 'create_session', + $payload + ); + + + return $form_data; + } + /** * The instance method for the static class. diff --git a/plugins/otter-pro/inc/render/class-form-stripe-block.php b/plugins/otter-pro/inc/render/class-form-stripe-block.php new file mode 100644 index 000000000..b1c78d0eb --- /dev/null +++ b/plugins/otter-pro/inc/render/class-form-stripe-block.php @@ -0,0 +1,96 @@ +create_request( 'product', $attributes['product'] ); + + if ( is_wp_error( $product ) ) { + return sprintf( + '
%2$s
', + get_block_wrapper_attributes(), + __( 'An error occurred! Could not retrieve product information!', 'otter-blocks' ) . $this->format_error( $product ) + ); + } + + $details_markup = ''; + + if ( 0 < count( $product['images'] ) ) { + $details_markup .= '' . $product['description'] . ''; + } + + $price = $stripe->create_request( 'price', $attributes['price'] ); + + if ( is_wp_error( $price ) ) { + return sprintf( + '
%2$s
', + get_block_wrapper_attributes(), + __( 'An error occurred! Could not retrieve the price of the product!', 'otter-blocks' ) . $this->format_error( $price ) + ); + } + + $currency = Review_Block::get_currency( $price['currency'] ); + $amount = number_format( $price['unit_amount'] / 100, 2, '.', ' ' ); + + $details_markup .= '
'; + $details_markup .= '

' . $product['name'] . '

'; + $details_markup .= '
' . $currency . $amount . '
'; + $details_markup .= '
'; + + $html_attributes = 'id="' . $attributes['id'] . '" ' . + ( isset( $attributes['mappedName'] ) ? ( ' name="' . $attributes['mappedName'] . '"' ) : '' ) . + ( isset( $attributes['fieldOptionName'] ) ? ( ' data-field-option-name="' . $attributes['fieldOptionName'] . '"' ) : '' ); + + return sprintf( + '
%2$s
', + get_block_wrapper_attributes() . $html_attributes, + $details_markup + ); + } + + /** + * Format the error message. + * + * @param \WP_Error $error The error. + * @return string + */ + private function format_error( $error ) { + return defined( 'WP_DEBUG' ) && WP_DEBUG ? ( + '' . __( 'Error message: ', 'otter-blocks' ) . ' ' . $error->get_error_message() . '' + ) : ''; + } +} diff --git a/src/blocks/blocks/form/common.tsx b/src/blocks/blocks/form/common.tsx index a79c16e6d..dc2d1aade 100644 --- a/src/blocks/blocks/form/common.tsx +++ b/src/blocks/blocks/form/common.tsx @@ -65,6 +65,11 @@ export type FieldOption = { saveFiles?: string maxFilesNumber?: number } + stripe?: { + product: string, + price: string, + quantity: number, + } } export type FormInputCommonProps = { @@ -110,6 +115,10 @@ export const fieldTypesOptions = () => ([ label: __( 'Select', 'otter-blocks' ), value: 'select' }, + { + label: ( Boolean( window.otterPro?.isActive ) && ! Boolean( window.otterPro?.isExpired ) ) ? __( 'Stripe', 'otter-blocks' ) : __( 'Stripe (Pro)', 'otter-blocks' ), + value: 'stripe' + }, { label: __( 'Text', 'otter-blocks' ), value: 'text' @@ -137,6 +146,7 @@ export const switchFormFieldTo = ( type?: string, clientId ?:string, attributes? [ 'select' === type || 'checkbox' === type || 'radio' === type, 'form-multiple-choice' ], [ 'file' === type, 'form-file' ], [ 'hidden' === type, 'form-hidden-field' ], + [ 'stripe' === type, 'form-stripe-field' ], [ 'form-input' ] ]); diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index eed63c919..06114ad1f 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -78,7 +78,8 @@ const formOptionsMap = { bcc: 'bcc', autoresponder: 'autoresponder', submissionsSaveLocation: 'submissionsSaveLocation', - webhookId: 'webhookId' + webhookId: 'webhookId', + requiredFields: 'requiredFields' }; /** @@ -360,7 +361,8 @@ const Edit = ({ autoresponder: wpOptions?.autoresponder, autoresponderSubject: wpOptions?.autoresponderSubject, submissionsSaveLocation: wpOptions?.submissionsSaveLocation, - webhookId: wpOptions?.webhookId + webhookId: wpOptions?.webhookId, + requiredFields: wpOptions?.requiredFields }); }; @@ -418,6 +420,8 @@ const Edit = ({ let isMissing = true; let hasUpdated = false; + formOptions.requiredFields = extractRequiredFields(); + emails?.forEach( ({ form }, index ) => { if ( form !== attributes.optionName ) { return; @@ -481,6 +485,17 @@ const Edit = ({ } }; + const extractRequiredFields = () => { + + const stripeFields = findInnerBlocks( + children, + block => 'themeisle-blocks/form-stripe-field' === block.name, + block => 'themeisle-blocks/form' !== block?.name + ); + + return stripeFields?.map( block => block.attributes.fieldOptionName ) || []; + }; + /** * Save integration data. */ diff --git a/src/blocks/blocks/form/file/index.js b/src/blocks/blocks/form/file/index.js index 8ac8acf6c..8807192f3 100644 --- a/src/blocks/blocks/form/file/index.js +++ b/src/blocks/blocks/form/file/index.js @@ -80,6 +80,16 @@ if ( ! Boolean( window.themeisleGutenberg.hasPro ) ) { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-stripe-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-stripe-field', { + ...attrs + }); + } } ] } diff --git a/src/blocks/blocks/form/index.js b/src/blocks/blocks/form/index.js index 04afed768..b6f04d2eb 100644 --- a/src/blocks/blocks/form/index.js +++ b/src/blocks/blocks/form/index.js @@ -19,6 +19,7 @@ import './textarea/index.js'; import './multiple-choice/index.js'; import './file/index.js'; import './hidden-field/index.js'; +import './stripe-field/index.js'; const { name } = metadata; diff --git a/src/blocks/blocks/form/input/index.js b/src/blocks/blocks/form/input/index.js index 12edfd3a4..830d35a3b 100644 --- a/src/blocks/blocks/form/input/index.js +++ b/src/blocks/blocks/form/input/index.js @@ -117,6 +117,16 @@ registerBlockType( name, { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-stripe-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-stripe-field', { + ...attrs + }); + } } ] } diff --git a/src/blocks/blocks/form/multiple-choice/index.js b/src/blocks/blocks/form/multiple-choice/index.js index 03bc74868..4144d62aa 100644 --- a/src/blocks/blocks/form/multiple-choice/index.js +++ b/src/blocks/blocks/form/multiple-choice/index.js @@ -99,6 +99,16 @@ registerBlockType( name, { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-stripe-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-stripe-field', { + ...attrs + }); + } } ] } diff --git a/src/blocks/blocks/form/stripe-field/block.json b/src/blocks/blocks/form/stripe-field/block.json new file mode 100644 index 000000000..fe0108bbb --- /dev/null +++ b/src/blocks/blocks/form/stripe-field/block.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-stripe-field", + "title": "Stripe Field", + "category": "themeisle-blocks", + "description": "A field used for adding Stripe products to the form.", + "keywords": [ "product", "stripe", "field" ], + "textdomain": "otter-blocks", + "ancestor": [ "themeisle-blocks/form" ], + "attributes": { + "id": { + "type": "string" + }, + "fieldOptionName": { + "type": "string" + }, + "type": { + "type": "string" + }, + "label": { + "type": "string" + }, + "mappedName": { + "type": "string" + }, + "inputWidth": { + "type": "number" + }, + "product": { + "type": "string" + }, + "price": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/blocks/blocks/form/stripe-field/index.js b/src/blocks/blocks/form/stripe-field/index.js new file mode 100644 index 000000000..28376915e --- /dev/null +++ b/src/blocks/blocks/form/stripe-field/index.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { createBlock, registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../helpers/icons.js'; +import Inspector from './inspector'; +import { omit } from 'lodash'; + +const { name } = metadata; + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +if ( ! Boolean( window.themeisleGutenberg.hasPro ) ) { + + registerBlockType( name, { + ...metadata, + title: __( 'Stripe Field (PRO)', 'otter-blocks' ), + description: __( 'A field used for adding Stripe products to the form.', 'otter-blocks' ), + icon, + edit: ( props ) => { + return ( + + ); + }, + save: () => null, + transforms: { + to: [ + { + type: 'block', + blocks: [ 'themeisle-blocks/form-input' ], + transform: ( attributes ) => { + + return createBlock( 'themeisle-blocks/form-input', { + ...attributes + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-textarea' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-textarea', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-multiple-choice' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-multiple-choice', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-file' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-file', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-hidden-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-hidden-field', { + ...attrs + }); + } + } + ] + } + }); + +} diff --git a/src/blocks/blocks/form/stripe-field/inspector.js b/src/blocks/blocks/form/stripe-field/inspector.js new file mode 100644 index 000000000..182f245fd --- /dev/null +++ b/src/blocks/blocks/form/stripe-field/inspector.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls +} from '@wordpress/block-editor'; + +import { + Button, + ExternalLink, + PanelBody, + SelectControl, + TextControl +} from '@wordpress/components'; + +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ + +import { FormContext } from '../edit'; +import { Notice } from '../../../components'; +import { setUtm } from '../../../helpers/helper-functions'; +import { fieldTypesOptions, switchFormFieldTo } from '../common'; + + +/** + * + * @param {FormStripeFieldInspectorPros} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes, + clientId +}) => { + + const { + selectForm + } = useContext( FormContext ); + + return ( + + + + + { + if ( 'stripe' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + + { __( 'Get more options with Otter Pro. ', 'otter-blocks' ) } } + variant="upsell" + /> + + + + ); +}; + +export default Inspector; diff --git a/src/blocks/blocks/form/style.scss b/src/blocks/blocks/form/style.scss index fec3f8149..bb097a807 100644 --- a/src/blocks/blocks/form/style.scss +++ b/src/blocks/blocks/form/style.scss @@ -325,27 +325,27 @@ margin-left: 0px; margin-right: 0px; } - + &.is-style-o-c-three-quarters { flex-basis: calc( 75% - var( --inputs-gap ) ); max-width: 75%; } - + &.is-style-o-c-two-thirds { flex-basis: calc( 66.66666666666666% - var( --inputs-gap ) ); max-width: 66.66666666666666%; } - + &.is-style-o-c-half { flex-basis: calc( 50% - var( --inputs-gap ) ); max-width: 50%; } - + &.is-style-o-c-one-third { flex-basis: calc( 33.33333333333333% - var( --inputs-gap ) ); max-width: 33.33333333333333%; } - + &.is-style-o-c-one-quarter { flex-basis: calc( 25% - var( --inputs-gap ) ); max-width: 25%; @@ -360,27 +360,27 @@ margin-left: 0px; margin-right: 0px; } - + &.is-style-o-c-three-quarters { flex-basis: calc( 75% - var( --inputs-gap ) ); max-width: 75%; } - + &.is-style-o-c-two-thirds { flex-basis: calc( 66.66666666666666% - var( --inputs-gap ) ); max-width: 66.66666666666666%; } - + &.is-style-o-c-half { flex-basis: calc( 50% - var( --inputs-gap ) ); max-width: 50%; } - + &.is-style-o-c-one-third { flex-basis: calc( 33.33333333333333% - var( --inputs-gap ) ); max-width: 33.33333333333333%; } - + &.is-style-o-c-one-quarter { flex-basis: calc( 25% - var( --inputs-gap ) ); max-width: 25%; @@ -485,6 +485,59 @@ .wp-block-themeisle-blocks-form-file input[type="file"] { border: 0px; } + + .wp-block-themeisle-blocks-form-stripe-field { + display: flex; + flex-direction: column; + min-width: 400px; + justify-content: center; + + .o-stripe-checkout { + display: flex; + padding: 10px; + + img { + border-radius: 4px; + margin: 10px; + width: 56px; + height: 56px; + border: 0; + } + + .o-stripe-checkout-description { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex-grow: 1; + + h3 { + font-style: normal; + font-weight: 500; + font-size: calc( var(--label-font-size) * 1.5 ); + line-height: 20px; + letter-spacing: -0.154px; + margin: 0; + padding-top: 0; + color: var(--label-color); + } + + h5 { + opacity: .5; + margin-top: 0; + padding-top: 0; + font-style: normal; + font-weight: 500; + font-size: calc( var(--label-font-size) * 1.5 ); + line-height: 20px; + letter-spacing: -0.154px; + margin: 0; + color: var(--label-color); + } + } + } + } + } .o-form-multiple-choice-field { diff --git a/src/blocks/blocks/form/textarea/index.js b/src/blocks/blocks/form/textarea/index.js index 759d1b88e..f58e3b748 100644 --- a/src/blocks/blocks/form/textarea/index.js +++ b/src/blocks/blocks/form/textarea/index.js @@ -78,6 +78,16 @@ registerBlockType( name, { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-stripe-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-stripe-field', { + ...attrs + }); + } } ] } diff --git a/src/blocks/frontend/form/index.js b/src/blocks/frontend/form/index.js index f002c1b0d..ace2b38c9 100644 --- a/src/blocks/frontend/form/index.js +++ b/src/blocks/frontend/form/index.js @@ -23,7 +23,7 @@ const getFormFieldInputs = ( form ) => { * * @type {Array.} */ - return [ ...form?.querySelectorAll( ':scope > .otter-form__container .wp-block-themeisle-blocks-form-input, :scope > .otter-form__container .wp-block-themeisle-blocks-form-textarea, :scope > .otter-form__container .wp-block-themeisle-blocks-form-multiple-choice, :scope > .otter-form__container .wp-block-themeisle-blocks-form-file, :scope > .otter-form__container > .wp-block-themeisle-blocks-form-hidden-field ' ) ].filter( input => { + return [ ...form?.querySelectorAll( ':scope > .otter-form__container .wp-block-themeisle-blocks-form-input, :scope > .otter-form__container .wp-block-themeisle-blocks-form-textarea, :scope > .otter-form__container .wp-block-themeisle-blocks-form-multiple-choice, :scope > .otter-form__container .wp-block-themeisle-blocks-form-file, :scope > .otter-form__container .wp-block-themeisle-blocks-form-hidden-field, :scope > .otter-form__container .wp-block-themeisle-blocks-form-stripe-field' ) ].filter( input => { return ! innerForms?.some( innerForm => innerForm?.contains( input ) ); }); }; @@ -51,11 +51,13 @@ const extractFormFields = async( form ) => { const labelContainer = input.querySelector( '.otter-form-input-label' ); const labelElem = ( labelContainer ?? input ).querySelector( '.otter-form-input-label__label, .otter-form-textarea-label__label' ); - const label = `(Field ${index + 1}) ${( labelElem ?? labelContainer )?.innerHTML?.replace( /<[^>]*>?/gm, '' )}`; + const fieldNumberLabel = `(Field ${index + 1})`; + let label = `${fieldNumberLabel} ${( labelElem ?? labelContainer )?.innerHTML?.replace( /<[^>]*>?/gm, '' )}`; let value = undefined; let fieldType = undefined; let mappedName = undefined; + let metadata = {}; const { id } = input; const valueElem = input.querySelector( '.otter-form-input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"]), .otter-form-textarea-input' ); @@ -72,6 +74,8 @@ const extractFormFields = async( form ) => { const hiddenInput = input.querySelector( 'input[type="hidden"]' ); + const stripeField = input.classList.contains( 'wp-block-themeisle-blocks-form-stripe-field' ); + if ( fileInput ) { const files = fileInput?.files; const mappedName = fileInput?.name; @@ -104,6 +108,16 @@ const extractFormFields = async( form ) => { value = urlParams.get( paramName ); fieldType = 'hidden'; } + } else if ( stripeField ) { + + // Find more proper selectors instead of h3 and h5 + label = `${fieldNumberLabel} ${input.querySelector( '.o-stripe-checkout-description h3' )?.innerHTML?.replace( /<[^>]*>?/gm, '' )}`; + value = input.querySelector( '.o-stripe-checkout-description h5' )?.innerHTML?.replace( /<[^>]*>?/gm, '' ); + fieldType = 'stripe-field'; + mappedName = input.name; + metadata = { + fieldOptionName: input?.dataset?.fieldOptionName + }; } else { const labels = input.querySelectorAll( '.o-form-multiple-choice-field > label' ); const valuesElem = input.querySelectorAll( '.o-form-multiple-choice-field > input' ); @@ -120,6 +134,7 @@ const extractFormFields = async( form ) => { type: fieldType, id: id, metadata: { + ...metadata, version: METADATA_VERSION, position: index + 1, mappedName: mappedName diff --git a/src/dashboard/components/pages/Integrations.js b/src/dashboard/components/pages/Integrations.js index 85606f549..682729fe1 100644 --- a/src/dashboard/components/pages/Integrations.js +++ b/src/dashboard/components/pages/Integrations.js @@ -41,12 +41,18 @@ const Integrations = () => { useEffect( () => { setStripeAPI( getOption( 'themeisle_stripe_api_key' ) ); + setStripePublicKey( getOption( 'themeisle_stripe_public_api_key' ) ); }, [ getOption( 'themeisle_stripe_api_key' ) ]); + useEffect( () => { + setStripePublicKey( getOption( 'themeisle_stripe_public_api_key' ) ); + }, [ getOption( 'themeisle_stripe_public_api_key' ) ]); + const [ googleMapsAPI, setGoogleMapsAPI ] = useState( '' ); const [ googleCaptchaAPISiteKey, setGoogleCaptchaAPISiteKey ] = useState( '' ); const [ googleCaptchaAPISecretKey, setGoogleCaptchaAPISecretKey ] = useState( '' ); const [ stripeAPI, setStripeAPI ] = useState( '' ); + const [ stripePublicKey, setStripePublicKey ] = useState( '' ); let ProModules = () => { return ( @@ -175,11 +181,20 @@ const Integrations = () => { id="otter-options-stripe-api" className="otter-button-field" > + setStripePublicKey( value ) } + /> + setStripeAPI( value ) } /> @@ -189,7 +204,10 @@ const Integrations = () => { variant="secondary" isSecondary disabled={ 'saving' === status } - onClick={ () => updateOption( 'themeisle_stripe_api_key', stripeAPI ) } + onClick={ () => { + updateOption( 'themeisle_stripe_api_key', stripeAPI ); + updateOption( 'themeisle_stripe_public_api_key', stripePublicKey ); + } } > { __( 'Save', 'otter-blocks' ) } diff --git a/src/pro/blocks/form-hidden-field/index.js b/src/pro/blocks/form-hidden-field/index.js index 2d94bc210..b2088b334 100644 --- a/src/pro/blocks/form-hidden-field/index.js +++ b/src/pro/blocks/form-hidden-field/index.js @@ -80,6 +80,16 @@ registerBlockType( name, { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-stripe-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-stripe-field', { + ...attrs + }); + } } ] } diff --git a/src/pro/blocks/form-stripe-field/block.json b/src/pro/blocks/form-stripe-field/block.json new file mode 100644 index 000000000..fe0108bbb --- /dev/null +++ b/src/pro/blocks/form-stripe-field/block.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-stripe-field", + "title": "Stripe Field", + "category": "themeisle-blocks", + "description": "A field used for adding Stripe products to the form.", + "keywords": [ "product", "stripe", "field" ], + "textdomain": "otter-blocks", + "ancestor": [ "themeisle-blocks/form" ], + "attributes": { + "id": { + "type": "string" + }, + "fieldOptionName": { + "type": "string" + }, + "type": { + "type": "string" + }, + "label": { + "type": "string" + }, + "mappedName": { + "type": "string" + }, + "inputWidth": { + "type": "number" + }, + "product": { + "type": "string" + }, + "price": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/pro/blocks/form-stripe-field/edit.js b/src/pro/blocks/form-stripe-field/edit.js new file mode 100644 index 000000000..bdc7ba669 --- /dev/null +++ b/src/pro/blocks/form-stripe-field/edit.js @@ -0,0 +1,379 @@ +import classnames from 'classnames'; +import hash from 'object-hash'; + +/** + * WordPress dependencies + */ + +import { Fragment, useContext, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; +import { store } from '@wordpress/icons'; +import { dispatch, select, useSelect } from '@wordpress/data'; +import { Button, ExternalLink, Notice, Placeholder, SelectControl, Spinner, TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ + +import metadata from './block.json'; +import Inspector from './inspector'; +import useSettings from '../../../blocks/helpers/use-settings'; +import { blockInit } from '../../../blocks/helpers/block-utility'; + +const { attributes: defaultAttributes } = metadata; + +/** + * Stripe Field component + * @param {import('./types').FormStripeFieldProps} props + * @returns + */ +const Edit = ({ + attributes, + setAttributes, + clientId +}) => { + + useEffect( () => { + const unsubscribe = blockInit( clientId, defaultAttributes ); + return () => unsubscribe( attributes.id ); + }, [ attributes.id ]); + + const [ getOption, updateOption, status ] = useSettings(); + const [ canRetrieveProducts, setCanRetrieveProducts ] = useState( false ); + + useEffect( () => { + if ( 'loaded' === status ) { + const apiKey = getOption( 'themeisle_stripe_api_key' ); + setCanRetrieveProducts( 'loaded' === status && 0 < apiKey?.length ); + } + }, [ status, getOption ]); + + /** + * Create the form identification tag for Otter Options. + */ + useEffect( () => { + if ( attributes.id && select( 'core/edit-widgets' ) ) { + setAttributes({ fieldOptionName: `widget_${ attributes.id.slice( -8 ) }` }); + } else if ( attributes.id ) { + setAttributes({ fieldOptionName: `${ hash({ url: window.location.pathname }) }_${ attributes.id.slice( -8 ) }` }); + } + }, [ attributes.id ]); + + const { products, productsList, hasProductsRequestFailed, productsError, isLoadingProducts } = useSelect( select => { + + const { + getStripeProducts, + getResolutionError, + isResolving + } = select( 'themeisle-gutenberg/data' ); + + const products = getStripeProducts(); + + return { + products, + productsList: products ? products?.map( ( product ) => { + return { + label: `${ product?.name } (id:${ product?.id })`, + value: product?.id + }; + }) : [], + hasProductsRequestFailed: Boolean( getResolutionError( 'getStripeProducts' ) ), + productsError: getResolutionError( 'getStripeProducts' ), + isLoadingProducts: isResolving( 'getStripeProducts' ) + }; + }, [ canRetrieveProducts, status ]); + + const { prices, pricesList, hasPricesRequestFailed, pricesError, isLoadingPrices } = useSelect( select => { + + if ( ! canRetrieveProducts ) { + return { + prices: [], + pricesList: [], + hasPricesRequestFailed: true, + pricesError: null, + isLoadingPrices: false + }; + } + + const { + getStripeProductPrices, + getResolutionError, + isResolving + } = select( 'themeisle-gutenberg/data' ); + + const prices = attributes.product ? getStripeProductPrices( attributes.product ) : []; + + return { + prices, + pricesList: prices ? prices?.map( ( prices ) => { + return { + label: `${ prices?.currency } ${ prices?.unit_amount } (id:${ prices?.id })`, + value: prices?.id + }; + }) : [], + hasPricesRequestFailed: Boolean( getResolutionError( 'getStripeProductPrices', [ attributes.product ]) ), + pricesError: getResolutionError( 'getStripeProductPrices', [ attributes.product ]), + isLoadingPrices: isResolving( 'getStripeProductPrices', [ attributes.product ]) + }; + }, [ attributes.product, canRetrieveProducts ]); + + const [ view, setView ] = useState( 'default' ); + const [ meta, setMeta ] = useState({}); + + useEffect( () => { + const product = products?.find( ( i ) => attributes.product === i.id ); + const price = prices?.find( ( i ) => attributes.price === i.id ); + + let unitAmount; + + if ( price?.unit_amount ) { + unitAmount = price?.unit_amount / 100; + unitAmount = unitAmount.toLocaleString( 'en-US', { style: 'currency', currency: price?.currency }); + } + + setMeta({ + name: product?.name, + price: unitAmount, + description: product?.description, + image: product?.images?.[0] || undefined + }); + }, [ products, prices, attributes.price ]); + + const showPlaceholder = ( isLoadingProducts || isLoadingPrices || hasProductsRequestFailed || hasPricesRequestFailed || undefined === attributes.product || undefined === attributes.price || 'loaded' !== status || ! canRetrieveProducts ); + + const blockProps = useBlockProps({ + className: classnames({ 'is-placeholder': showPlaceholder }), + id: attributes.id + }); + + const [ apiKey, setAPIKey ] = useState( '' ); + + const reset = () => { + dispatch( 'themeisle-gutenberg/data' ).invalidateResolutionForStoreSelector( 'getStripeProducts' ); + dispatch( 'themeisle-gutenberg/data' ).invalidateResolutionForStoreSelector( 'getStripeProductPrices' ); + setCanRetrieveProducts( 0 < apiKey?.length ); + setAPIKey( '' ); + }; + + const saveApiKey = () => { + setCanRetrieveProducts( false ); + updateOption( 'themeisle_stripe_api_key', apiKey?.replace?.( /\s/g, '' ), __( 'Stripe API Key saved!', 'otter-blocks' ), 'stripe-api-key', reset ); + }; + + + const saveProduct = ( fieldOptionName, product, price ) => { + if ( ! product || ! price || ! fieldOptionName || ! Boolean( window.themeisleGutenberg?.hasPro ) ) { + return; + } + + /** @type{import('../../../blocks/blocks/form/common').FieldOption[]} */ + const fieldOptions = getOption?.( 'themeisle_blocks_form_fields_option' ) ?? []; + + const fieldIndex = fieldOptions?.findIndex( field => field.fieldOptionName === fieldOptionName ); + + if ( fieldIndex === undefined ) { + return; + } + + if ( -1 !== fieldIndex ) { + fieldOptions[fieldIndex] = { + ...fieldOptions[fieldIndex], + stripe: { + product: attributes.product ? attributes.product : undefined, + price: attributes.price ? attributes.price : undefined + } + }; + } else { + fieldOptions.push({ + fieldOptionName: attributes.fieldOptionName, + fieldOptionType: 'stripe', + stripe: { + product: attributes.product ? attributes.product : undefined, + price: attributes.price ? attributes.price : undefined + } + }); + } + + updateOption( 'themeisle_blocks_form_fields_option', fieldOptions, __( 'Field settings saved.', 'otter-blocks' ), 'field-option' ); + }; + + const { canSaveData } = useSelect( select => { + const isSavingPost = select( 'core/editor' )?.isSavingPost(); + const isPublishingPost = select( 'core/editor' )?.isPublishingPost(); + const isAutosaving = select( 'core/editor' )?.isAutosavingPost(); + const widgetSaving = select( 'core/edit-widgets' )?.isSavingWidgetAreas(); + + return { + canSaveData: ( ! isAutosaving && ( isSavingPost || isPublishingPost ) ) || widgetSaving + }; + }); + + useEffect( () => { + if ( canSaveData ) { + saveProduct( attributes.fieldOptionName, attributes.product, attributes.price ); + } + }, [ canSaveData ]); + + if ( showPlaceholder ) { + return ( +
+ + { + ( 'loading' === status || 'saving' === status ) && ( +
+ + { __( 'Checking the API Key...', 'otter-blocks' ) } +

+
+ ) + } + + { + ( + ( hasProductsRequestFailed || hasPricesRequestFailed ) && + ( 'loaded' === status ) && + ( productsError?.message?.length || pricesError?.message?.length ) + ) && ( +
+ + { + ( hasProductsRequestFailed && productsError?.message ) || ( hasPricesRequestFailed && pricesError?.message ) + } + +
+ ) + } + + { + ( ( 'loaded' === status ) && ( ( hasProductsRequestFailed && productsError?.message?.includes( 'Invalid API Key' ) ) || ! canRetrieveProducts ) ) && ( +
+ + + +
+ +
+ +
+ + { __( 'You can also set it from Dashboard', 'otter-blocks' ) } +
+ ) + } + + { + 'error' === status && ( + + {__( 'An error occurred during API Key checking.', 'otter-blocks' )} + + ) + } + + { + ( 'loaded' === status && false === hasProductsRequestFailed && canRetrieveProducts ) && ( + + { ! isLoadingProducts && ( + { + setAttributes({ product: 'none' !== product ? product : undefined }); + } } + /> + ) } + + { ( ! isLoadingPrices && attributes.product ) && ( + { + setAttributes({ price: 'none' !== price ? price : undefined }); + } } + /> + ) } + + { ( isLoadingProducts || isLoadingPrices ) && } + + ) + } +
+
+ ); + } + + return ( + + + +
+ { 'default' === view && ( + +
+ { undefined !== meta?.image && ( + { + )} + +
+

{ meta?.name }

+
{ meta?.price }
+
+
+
+ )} + + { 'success' === view && ( attributes.successMessage || __( 'Your payment was successful. If you have any questions, please email orders@example.com.', 'otter-blocks' ) ) } + { 'cancel' === view && ( attributes.cancelMessage || __( 'Your payment was unsuccessful. If you have any questions, please email orders@example.com.', 'otter-blocks' ) ) } +
+
+ ); +}; + +export default Edit; diff --git a/src/pro/blocks/form-stripe-field/index.js b/src/pro/blocks/form-stripe-field/index.js new file mode 100644 index 000000000..b5e32170f --- /dev/null +++ b/src/pro/blocks/form-stripe-field/index.js @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { createBlock, registerBlockType } from '@wordpress/blocks'; + +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../blocks/helpers/icons.js'; +import edit from './edit.js'; +import { omit } from 'lodash'; +import Inactive from '../../components/inactive'; + + +const { name } = metadata; + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +if ( ! ( Boolean( window.otterPro.isActive ) && ! Boolean( window.otterPro.isExpired ) ) ) { + edit = () => ; +} + + +registerBlockType( name, { + ...metadata, + title: __( 'Stripe Field', 'otter-blocks' ), + description: __( 'A field used for adding Stripe products to the form.', 'otter-blocks' ), + icon, + edit, + save: () => null, + transforms: { + to: [ + { + type: 'block', + blocks: [ 'themeisle-blocks/form-input' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + + return createBlock( 'themeisle-blocks/form-input', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-textarea' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-textarea', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-multiple-choice' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-multiple-choice', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-file' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-file', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-hidden-field' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-hidden-field', { + ...attrs + }); + } + } + ] + } +}); diff --git a/src/pro/blocks/form-stripe-field/inspector.js b/src/pro/blocks/form-stripe-field/inspector.js new file mode 100644 index 000000000..be78ff1cf --- /dev/null +++ b/src/pro/blocks/form-stripe-field/inspector.js @@ -0,0 +1,146 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls +} from '@wordpress/block-editor'; + +import { + Button, + PanelBody, Placeholder, + SelectControl, Spinner, + TextControl +} from '@wordpress/components'; +import { applyFilters } from '@wordpress/hooks'; +import { Fragment } from '@wordpress/element'; +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ + +import { Notice as OtterNotice } from '../../../blocks/components'; +import { fieldTypesOptions, switchFormFieldTo } from '../../../blocks/blocks/form/common'; + + +/** + * + * @param {import('./types').FormStripeFieldInspectorPros} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes, + clientId, + productsList, + pricesList, + isLoadingProducts, + isLoadingPrices +}) => { + + // FormContext is not available here. This is a workaround. + const selectForm = () => { + const formParentId = Array.from( document.querySelectorAll( `.wp-block-themeisle-blocks-form:has(#block-${clientId})` ) )?.pop()?.dataset?.block; + dispatch( 'core/block-editor' ).selectBlock( formParentId ); + }; + + return ( + + + + + { + if ( 'stripe' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + + setAttributes({ mappedName }) } + placeholder={ __( 'product', 'otter-blocks' ) } + /> + + { ! Boolean( window?.otterPro?.isActive ) && ( + + + + ) + + } + +
+ { applyFilters( 'otter.feedback', '', 'form' ) } + { applyFilters( 'otter.poweredBy', '' ) } +
+
+ + + { ! isLoadingProducts && ( + { + setAttributes({ + product: 'none' !== product ? product : undefined, + price: undefined + }); + } } + /> + ) } + + { ( ! isLoadingPrices && attributes.product ) && ( + { + setAttributes({ price: 'none' !== price ? price : undefined }); + } } + /> + ) } + + { ( isLoadingProducts || isLoadingPrices ) && } + + + +
+ ); +}; + +export default Inspector; diff --git a/src/pro/blocks/form-stripe-field/types.d.ts b/src/pro/blocks/form-stripe-field/types.d.ts new file mode 100644 index 000000000..90414f009 --- /dev/null +++ b/src/pro/blocks/form-stripe-field/types.d.ts @@ -0,0 +1,17 @@ +import { BlockProps, InspectorProps } from '../../helpers/blocks'; + + +type Attributes = { + id: string + formId: string + label: string + paramName: string + mappedName: string + type: string + product: string + price: string + fieldOptionName: string +} + +export type FormStripeFieldProps = BlockProps +export type FormStripeFieldInspectorPros = InspectorProps diff --git a/src/pro/blocks/index.js b/src/pro/blocks/index.js index c5f5e0b4f..9c726a052 100644 --- a/src/pro/blocks/index.js +++ b/src/pro/blocks/index.js @@ -7,3 +7,4 @@ import './review-comparison/index.js'; import './woo-comparison/index.js'; import './file/index.js'; import './form-hidden-field/index.js'; +import './form-stripe-field/index.js';