diff --git a/assets/css/dist/admin.css b/assets/css/dist/admin.css
new file mode 100644
index 0000000..c51e93d
--- /dev/null
+++ b/assets/css/dist/admin.css
@@ -0,0 +1,25 @@
+/*
+----------------------------------------------------------------
+
+admin.css
+Gravity Forms Administration Styles
+For the Turnstile Add-On
+https://www.gravityforms.com
+
+Gravity Forms is a Rocketgenius project
+copyright 2008-2023 Rocketgenius Inc.
+https://www.rocketgenius.com
+this may not be re-distributed without the
+express written permission of the author.
+
+NOTE: DO NOT EDIT THIS FILE!
+THIS FILE IS REPLACED DURING AUTO UPGRADE
+AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN.
+
+----------------------------------------------------------------
+*/
+
+.gform-compact-view .gfield--turnstile-message, .gform-compact-view .gfield--turnstile-preview {
+ display: none;
+ }
+/*# sourceMappingURL=admin.css.map */
diff --git a/assets/css/dist/admin.min.css b/assets/css/dist/admin.min.css
new file mode 100644
index 0000000..328bc09
--- /dev/null
+++ b/assets/css/dist/admin.min.css
@@ -0,0 +1 @@
+.gform-compact-view .gfield--turnstile-message,.gform-compact-view .gfield--turnstile-preview{display:none}
\ No newline at end of file
diff --git a/assets/css/dist/assets.php b/assets/css/dist/assets.php
new file mode 100644
index 0000000..a930dad
--- /dev/null
+++ b/assets/css/dist/assets.php
@@ -0,0 +1 @@
+ array('admin.css' => array('version' => '7f13ab7c6cda88879e6c12d6fcfa496d', 'file' => 'admin.css'), 'theme-foundation.css' => array('version' => 'b00f7568f4a57bcfec5fedf2de154d8a', 'file' => 'theme-foundation.css'), 'theme-framework.css' => array('version' => 'f677f687672ce6c37be17cc28cae00b9', 'file' => 'theme-framework.css'), 'theme.css' => array('version' => '772606f5d781debc2a769d1963348aba', 'file' => 'theme.css')));
\ No newline at end of file
diff --git a/assets/css/dist/theme-foundation.css b/assets/css/dist/theme-foundation.css
new file mode 100644
index 0000000..b40942b
--- /dev/null
+++ b/assets/css/dist/theme-foundation.css
@@ -0,0 +1,35 @@
+/*
+----------------------------------------------------------------
+
+theme-foundation.css
+Gravity Forms Theme Foundation Styles & CSS API
+For the Stripe Add-On
+A Gravity Forms theme framework foundation responsible for layout,
+out-of-the-box enhanced ui, and other basic required styles for
+the Stripe Add-on.
+https://www.gravityforms.com
+
+Theme dependencies:
+- Gravity Forms Theme Reset: gravity-forms-theme-reset.css
+- Gravity Forms Theme Foundation: gravity-forms-theme-foundation.css
+
+Gravity Forms is a Rocketgenius project
+copyright 2008-2023 Rocketgenius Inc.
+https://www.rocketgenius.com
+this may not be re-distributed without the
+express written permission of the author.
+
+NOTE: DO NOT EDIT THIS FILE!
+THIS FILE IS REPLACED DURING AUTO UPGRADE
+AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN.
+
+----------------------------------------------------------------
+*/
+
+/*
+.gform-theme--foundation .gfield--type-turnstile {
+
+}
+*/
+
+/*# sourceMappingURL=theme-foundation.css.map */
diff --git a/assets/css/dist/theme-foundation.min.css b/assets/css/dist/theme-foundation.min.css
new file mode 100644
index 0000000..e69de29
diff --git a/assets/css/dist/theme-framework.css b/assets/css/dist/theme-framework.css
new file mode 100644
index 0000000..4197eda
--- /dev/null
+++ b/assets/css/dist/theme-framework.css
@@ -0,0 +1,33 @@
+/*
+----------------------------------------------------------------
+
+theme-framework.css
+Gravity Forms Theme Framework & CSS API
+For the Stripe Add-On
+https://www.gravityforms.com
+
+Theme dependencies:
+- Gravity Forms Theme Reset: gravity-forms-theme-reset.css
+- Gravity Forms Theme Foundation: gravity-forms-theme-foundation.css
+- Gravity Forms Theme Foundation for the Stripe Add-On: theme-foundation.css
+
+Gravity Forms is a Rocketgenius project
+copyright 2008-2023 Rocketgenius Inc.
+https://www.rocketgenius.com
+this may not be re-distributed without the
+express written permission of the author.
+
+NOTE: DO NOT EDIT THIS FILE!
+THIS FILE IS REPLACED DURING AUTO UPGRADE
+AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN.
+
+----------------------------------------------------------------
+*/
+
+/*
+.gform-theme--framework .gfield--type-turnstile {
+
+}
+*/
+
+/*# sourceMappingURL=theme-framework.css.map */
diff --git a/assets/css/dist/theme-framework.min.css b/assets/css/dist/theme-framework.min.css
new file mode 100644
index 0000000..e69de29
diff --git a/assets/css/dist/theme.css b/assets/css/dist/theme.css
new file mode 100644
index 0000000..097c913
--- /dev/null
+++ b/assets/css/dist/theme.css
@@ -0,0 +1,38 @@
+/*
+----------------------------------------------------------------
+
+theme.css
+Gravity Forms Gravity Theme Styles
+For the Stripe Add-On
+A light theme for the frontend engineered to get reasonably
+nice look and feel in all our standard theme targets.
+https://www.gravityforms.com
+
+Theme dependencies:
+- Gravity Forms Basic Theme: basic.css
+
+Gravity Forms is a Rocketgenius project
+copyright 2008-2023 Rocketgenius Inc.
+https://www.rocketgenius.com
+this may not be re-distributed without the
+express written permission of the author.
+
+NOTE: DO NOT EDIT THIS FILE!
+THIS FILE IS REPLACED DURING AUTO UPGRADE
+AND ANY CHANGES MADE HERE WILL BE OVERWRITTEN.
+
+----------------------------------------------------------------
+*/
+
+/*
+.gravity-theme,
+.gform_legacy_markup_wrapper {
+}
+
+html[dir="rtl"] .gravity-theme,
+html[dir="rtl"] .gform_legacy_markup_wrapper {
+
+}
+*/
+
+/*# sourceMappingURL=theme.css.map */
diff --git a/assets/css/dist/theme.min.css b/assets/css/dist/theme.min.css
new file mode 100644
index 0000000..e69de29
diff --git a/assets/img/cloudflare.svg b/assets/img/cloudflare.svg
new file mode 100644
index 0000000..695b4cc
--- /dev/null
+++ b/assets/img/cloudflare.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/img/preview-dark.svg b/assets/img/preview-dark.svg
new file mode 100644
index 0000000..4761767
--- /dev/null
+++ b/assets/img/preview-dark.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/img/preview-light.svg b/assets/img/preview-light.svg
new file mode 100644
index 0000000..7caa7a5
--- /dev/null
+++ b/assets/img/preview-light.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/js/dist/assets.php b/assets/js/dist/assets.php
new file mode 100644
index 0000000..23d08b2
--- /dev/null
+++ b/assets/js/dist/assets.php
@@ -0,0 +1 @@
+ array('scripts-admin.js' => array('version' => '929a63252012f58b36a9936a29424f2c', 'file' => 'scripts-admin.js'), 'scripts-admin.min.js' => array('version' => '962cc0fe0fd5e66e96abc549a17e5591', 'file' => 'scripts-admin.min.js'), 'scripts-theme.js' => array('version' => '1d94bcbec13e553c7962f5779f3b0609', 'file' => 'scripts-theme.js'), 'scripts-theme.min.js' => array('version' => 'b1d3dcbc3fdd1a32447b4c4c11b79ebd', 'file' => 'scripts-theme.min.js'), 'vendor-admin.js' => array('version' => '14df23cc93325f8b9387510d6bd0ea09', 'file' => 'vendor-admin.js'), 'vendor-admin.min.js' => array('version' => '658d07a2f09cecbd827befcc922a673d', 'file' => 'vendor-admin.min.js'), 'vendor-theme.js' => array('version' => 'b14d50aa9f0d6f5fd66ce1e37eb01002', 'file' => 'vendor-theme.js'), 'vendor-theme.min.js' => array('version' => '89f58fdda96bf36c0f7a05b20d92e7bc', 'file' => 'vendor-theme.min.js')));
\ No newline at end of file
diff --git a/assets/js/dist/scripts-admin.js b/assets/js/dist/scripts-admin.js
new file mode 100644
index 0000000..cfd7b47
--- /dev/null
+++ b/assets/js/dist/scripts-admin.js
@@ -0,0 +1,2 @@
+!function(){"use strict";var e,t={5154:function(e,t,n){var r,i,o,l,d,a,u,f=gform.utils,s=window.turnstile||{},c=(null===(r=window)||void 0===r?void 0:r.gform_turnstile_config)||{},v=function(){var e;(0,f.trigger)({event:"gform/turnstile/before_render_preview",el:document,data:(null==c?void 0:c.data)||{},native:!1}),s.render("#gform_turnstile_preview",{sitekey:(null==c||null===(e=c.data)||void 0===e?void 0:e.site_key)||""})},g=function(e){var t,n;if(e.target.src&&-1!==e.target.src.indexOf("challenges.cloudflare")){var r;(0,f.trigger)({event:"gform/turnstile/after_render_preview",el:document,data:(null==c?void 0:c.data)||{},native:!1}),(0,f.getNodes)('#gform_turnstile_preview iframe[style*="display: none"]',!0,document,!0).length&&(document.getElementById("gform_turnstile_preview").innerHTML='\n\t
' . esc_html__( 'Below is a preview of how the field will appear in your forms. If you see an error message, check your credentials and try again.', 'gravityformsturnstile' ) . '
' . esc_html__( 'Note: ', 'gravityformsturnstile' ) . '' . esc_html__( 'If your field is set to the "Invisible" type in Cloudflare, this preview will appear empty.', 'gravityformsturnstile' ),
+ 'dependency' => array(
+ 'live' => false,
+ 'fields' => array(
+ array( 'field' => 'site_key' ),
+ array( 'field' => 'site_secret' ),
+ ),
+ ),
+ 'fields' => array(
+ array(
+ 'name' => 'preview',
+ 'type' => 'html',
+ 'html' => array( $this, 'get_preview_html' ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Dequeue other captcha scripts if no-conflict is enabled.
+ *
+ * @since 1.0
+ *
+ * @action wp_enqueue_scripts 999, 0
+ *
+ * @return void
+ */
+ public function handle_no_conflict() {
+ /**
+ * Allows users to enable a No-Conflict mode for turnstile, which dequeues any other popular captcha
+ * scripts to avoid conflicts. Should only be used at support's direction.
+ *
+ * Example: add_filter( 'gform_turnstile_enable_no_conflict', '__return_true' );
+ *
+ * @since 1.0
+ *
+ * @param bool $enabled Whether no-conflict is enabled.
+ *
+ * @return bool
+ */
+ $enabled = apply_filters( 'gform_turnstile_enable_no_conflict', false );
+
+ if ( ! $enabled ) {
+ return;
+ }
+
+ $this->log_debug( __METHOD__ . '(): Beginning Turnstile no-conflict process.' );
+
+ $scripts = wp_scripts();
+ $urls_to_check = array(
+ 'google.com/recaptcha',
+ 'gstatic.com/recaptcha',
+ 'hcaptcha.com/1'
+ );
+
+ foreach ( $scripts->queue as $script ) {
+ $src = $scripts->registered[ $script ]->src;
+
+ foreach ( $urls_to_check as $check ) {
+ if ( strpos( $src, $check ) === false ) {
+ continue;
+ }
+
+ $this->log_debug( __METHOD__ . '(): Turnstile no-conflict is dequeueing script: ' . $script );
+
+ wp_deregister_script( $script );
+ wp_dequeue_script( $script );
+ }
+ }
+ }
+
+ /**
+ * Store the API URL from the field preview for checking credentials on load.
+ *
+ * @since 1.0
+ *
+ * @return void
+ */
+ public function store_api_url() {
+ check_ajax_referer( 'save_api_url', 'secret' );
+
+ $url = filter_input( INPUT_POST, 'url', FILTER_SANITIZE_URL );
+
+ update_option( 'gf_turnstile_api_url', $url );
+
+ wp_send_json_success( array( 'url' => $url ) );
+ }
+
+ /**
+ * Moves the turnstile field to be the last field of the form.
+ *
+ * Turnstile field must be the last field to be validated, because if another field failed validation after turnstile passed, this means turnstile validation will run again during the next request, consuming the frontend verification token again, which should be verified only once.
+ *
+ * @since 1.0
+ *
+ * @param array $form The current form being validated.
+ *
+ * @return array.
+ */
+ public function move_turnstile_field_to_last( $form ) {
+ if ( ! $this->has_turnstile_field( $form ) ) {
+ return $form;
+ }
+
+ $idx = null;
+ $fields = $form['fields'];
+
+ foreach ( $fields as $i => $field ) {
+ if ( $field->type === 'turnstile' ) {
+ $form['turnstile_original_position'] = $i;
+ $idx = $i;
+ break;
+ }
+ }
+
+ if ( is_null( $idx ) ) {
+ return $form;
+ }
+
+ unset( $fields[ $idx ] );
+ $fields[] = $field;
+
+ $form['fields'] = $fields;
+
+ return $form;
+ }
+
+ /**
+ * Put the turnstile field back to its original position.
+ *
+ * We put the field at the end of the form fields array to make sure it gets validated after all other fields passed validation.
+ * If one of the fields fails validation we postpone sending a request to verify the turnstile token.
+ *
+ * @see GFTurnstile::move_turnstile_field_to_last()
+ *
+ * @since 1.0
+ *
+ * @param array $validation_result the validation result after all the fields in the form have been validated.
+ *
+ * @return array The validation result that contains the form after resetting the turnstile position.
+ */
+ public function reset_turnstile_field_position( $validation_result ) {
+ $form = $validation_result['form'];
+
+ if ( ! $this->has_turnstile_field( $form ) ) {
+ return $validation_result;
+ }
+
+ $field_position = $validation_result['form']['turnstile_original_position'];
+ unset( $form['turnstile_original_position'] );
+ if ( $field_position !== 0 && ! $field_position ) {
+ return $validation_result;
+ }
+
+ $fields = $form['fields'];
+ $turnstile_field = array_pop( $fields );
+
+ // Put the field back to its original index.
+ $fields = array_merge(
+ array_slice(
+ $fields,
+ 0,
+ $field_position
+ ),
+ array( $turnstile_field ),
+ array_slice( $fields, $field_position )
+ );
+
+ $form['fields'] = $fields;
+ $validation_result['form'] = $form;
+
+ return $validation_result;
+ }
+
+ /**
+ * Checks if any of the form fields has failed validation so we can postpone turnstile validation to the next request if so.
+ *
+ * @param array $form The current form being validated.
+ *
+ * @return bool whether any of the form fields failed validation or not.
+ */
+ public function form_has_errors( $form ) {
+ foreach ( $form['fields'] as $field ) {
+ if ( $field->failed_validation ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // --------------------------------------------------------------
+ // # Markup -----------------------------------------------------
+ // --------------------------------------------------------------
+
+ /**
+ * Get the HTML to display when previewing the widget on the settings page.
+ *
+ * @since 1.0
+ *
+ * @return string
+ */
+ public function get_preview_html() {
+ $key = $this->get_plugin_setting( 'site_key' );
+ $secret = $this->get_plugin_setting( 'site_secret' );
+ $theme = $this->get_plugin_setting( 'theme' );
+
+ if ( empty( $key ) || empty( $secret ) ) {
+ $this->log_debug( __METHOD__ . '(): Missing secret or key values, returning empty preview.' );
+ return '';
+ }
+
+ return '
';
+ }
+
+ /**
+ * Render the setting field for choosing a Widget Theme.
+ *
+ * @since 1.0
+ *
+ * @action gform_field_appearance_settings 0, 1
+ *
+ * @param int $position The current position being rendered in the sidebar.
+ *
+ * @return void
+ */
+ public function render_widget_theme_field_setting( $position ) {
+ if ( (int) $position !== 20 ) {
+ return;
+ }
+
+ ?>
+
+
+
+
+ has_turnstile_field( $form ) ) {
+ return $form_string;
+ }
+
+ ob_start(); ?>
+
+ get_plugin_setting( 'site_key' ) ) || empty( $this->get_plugin_setting( 'site_secret' ) ) ) {
+ $this->log_debug( __METHOD__ . '(): Missing Turnstile credentials, aborting render.' );
+ return false;
+ }
+
+ // Static caching to avoid multiple calls.
+ static $server_response;
+
+ if ( ! empty( $server_response ) ) {
+ return $server_response === 200;
+ }
+
+ $api_url = get_option( 'gf_turnstile_api_url' );
+
+ // If we don't have an API URL stored for some reason, bail.
+ if ( empty( $api_url ) ) {
+ $this->log_debug( __METHOD__ . '(): No Turnstile API URL stored, aborting render.' );
+ return false;
+ }
+
+ $response = wp_remote_get( $api_url );
+
+ // Something went wrong with the request.
+ if ( is_wp_error( $response ) ) {
+ $this->log_debug( __METHOD__ . '(): Could not reach turnstile API server, aborting render.' );
+
+ return false;
+ }
+
+ $server_response = (int) wp_remote_retrieve_response_code( $response );
+
+ // Invalid credentials will return a 400 when hitting the API endpoint.
+ return $server_response === 200;
+ }
+
+ /**
+ * Determine if a given form has a turnstile field.
+ *
+ * @since 1.0
+ *
+ * @param array $form The form being evaluated.
+ *
+ * @return bool
+ */
+ public function has_turnstile_field( $form ) {
+ $fields = \GFAPI::get_fields_by_type( $form, array( 'turnstile' ) );
+
+ return ! empty( $fields );
+ }
+
+ /**
+ * Get the min string for enqueued assets.
+ *
+ * @since 1.0
+ *
+ * @return string
+ */
+ private function get_min() {
+ return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min';
+ }
+
+ /**
+ * Return the plugin's icon for the plugin/form settings menu.
+ *
+ * @since 1.0
+ *
+ * @return string
+ */
+ public function get_menu_icon() {
+ return $this->is_gravityforms_supported( '2.7.8.1' ) ? 'gform-icon--cloudflare-turnstile' : file_get_contents( $this->get_base_path() . '/assets/img/cloudflare.svg' );
+ }
+}
diff --git a/includes/class-gf-field-turnstile.php b/includes/class-gf-field-turnstile.php
new file mode 100644
index 0000000..d7473ae
--- /dev/null
+++ b/includes/class-gf-field-turnstile.php
@@ -0,0 +1,324 @@
+is_gravityforms_supported( '2.7.8.1' ) ? 'gform-icon--cloudflare-turnstile' : gf_turnstile()->get_base_url() . '/assets/img/cloudflare.svg';
+ }
+
+ /**
+ * Returns the field's form editor description.
+ *
+ * @since 1.0
+ *
+ * @return string
+ */
+ public function get_form_editor_field_description() {
+ return esc_attr__( 'Protects your form from spam submissions using Cloudflare\'s Turnstile system.', 'gravityformsturnstile' );
+ }
+
+ /**
+ * Get field settings in the form editor.
+ *
+ * @since 1.0
+ *
+ * @return array
+ */
+ public function get_form_editor_field_settings() {
+ return array(
+ 'error_message_setting',
+ 'turnstile_widget_theme_setting',
+ 'label_setting',
+ );
+ }
+
+ /**
+ * Get form editor button.
+ *
+ * @since 1.0
+ *
+ * @return array
+ */
+ public function get_form_editor_button() {
+ return array(
+ 'group' => 'advanced_fields',
+ 'text' => $this->get_form_editor_field_title(),
+ );
+ }
+
+ /**
+ * Returns the warning message to be displayed in the form editor sidebar.
+ *
+ * @since 1.1
+ *
+ * @return string
+ */
+ public function get_field_sidebar_messages() {
+ if ( ! empty( gf_turnstile()->get_plugin_setting( 'site_key' ) ) && ! empty( gf_turnstile()->get_plugin_setting( 'site_secret' ) ) ) {
+ return '';
+ }
+
+ // Translators: 1. Opening tag with link to the Forms > Settings > Cloudflare Turnstile page. 2. closing tag.
+ return sprintf( __( 'To use Turnstile you must configure the site and secret keys on the %1$sTurnstile Settings%2$s page.', 'gravityformsturnstile' ), "", '' );
+ }
+
+ /**
+ * Get the field input markup.
+ *
+ * @since 1.0
+ *
+ * @return string
+ */
+ public function field_input_markup() {
+ if ( ! $this->failed_validation && ! empty( $this->get_value_submission( array() ) ) ) {
+ return '';
+ }
+
+ $key = gf_turnstile()->get_plugin_setting( 'site_key' );
+ $theme = $this->turnstileWidgetTheme;
+
+ if ( empty( $theme ) ) {
+ $theme = gf_turnstile()->get_plugin_setting( 'theme' );
+ }
+
+ $div = '';
+
+ return sprintf( "
%s
", $div );
+ }
+
+ /**
+ * Get field input.
+ *
+ * @since 1.0
+ *
+ * @param array $form The Form Object currently being processed.
+ * @param array $value The field value. From default/dynamic population, $_POST, or a resumed incomplete submission.
+ * @param null|array $entry Null or the Entry Object currently being edited.
+ *
+ * @return string
+ */
+ public function get_field_input( $form, $value = array(), $entry = null ) {
+ $response = $this->field_input_markup();
+
+ if ( $this->failed_validation ) {
+ $response .= sprintf( '
%1$s
', $this->validation_message );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns the field markup; including field label, description, validation, and the form editor admin buttons.
+ *
+ * The {FIELD} placeholder will be replaced in GFFormDisplay::get_field_content with the markup returned by GF_Field::get_field_input().
+ *
+ * @since 1.0
+ *
+ * @param string|array $value The field value. From default/dynamic population, $_POST, or a resumed incomplete submission.
+ * @param bool $force_frontend_label Should the frontend label be displayed in the admin even if an admin label is configured.
+ * @param array $form The Form Object currently being processed.
+ *
+ * @return string
+ */
+ public function get_field_content( $value, $force_frontend_label, $form ) {
+ $form_id = $form['id'];
+ $admin_buttons = $this->get_admin_buttons();
+ $is_entry_detail = $this->is_entry_detail();
+ $is_form_editor = $this->is_form_editor();
+ $is_admin = $is_entry_detail || $is_form_editor || ( rgget( 'context' ) === 'edit' && ! empty( rgget( 'post_id' ) ) );
+ $field_label = $this->get_field_label( $force_frontend_label, $value );
+ $field_id = $is_admin || $form_id == 0 ? "input_{$this->id}" : 'input_' . $form_id . "_{$this->id}";
+ $admin_hidden_markup = ( $this->visibility == 'hidden' ) ? $this->get_hidden_admin_markup() : '';
+ $field_content = ! $is_admin ? '{FIELD}' : sprintf( "%s%s