Repository: weDevsOfficial/wedocs-plugin
Pull request #209: Add Promotional Banner to weDocs Dashboard
Author: login: haideralibd
Labels: name: QA Approved
name: Ready To Merge
Created: 2025-04-10T09:18:34Z | Merged: 2025-04-23T11:36:47Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a unified promotional notice experience across the application, displaying dynamic offers in admin and public pages.
  - Added mechanisms for retrieving and dismissing promotional notices, including interactive, dismissible UI elements.
  - Enhanced overall functionality with new endpoints and components to improve the visibility and management of promotional content.
  - New API routes for managing promotional notices.
  - Added a new component for displaying promotional notices in various sections of the application.
  - Introduced a new function for converting UTC time to Eastern Standard Time (EST).

- **Bug Fixes**
  - Improved error handling for promotional notice retrieval and dismissal.

- **Chores**
  - Added constants for promotional notice management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/assets/img/wedocs-icon-128x128.png b/assets/img/wedocs-icon-128x128.png
new file mode 100644
index 00000000..bfab17ea
Binary files /dev/null and b/assets/img/wedocs-icon-128x128.png differ
diff --git a/includes/API/API.php b/includes/API/API.php
index 14d7a88d..a2d577a1 100644
--- a/includes/API/API.php
+++ b/includes/API/API.php
@@ -233,6 +233,22 @@ public function register_routes() {
                 ],
             ],
         ] );
+
+        register_rest_route( $this->namespace, '/' . $this->rest_base . '/promotion-notice', [
+            [
+                'methods'             => WP_REST_Server::READABLE,
+                'callback'            => [ $this, 'get_promotional_notice' ],
+                'permission_callback' => [ $this, 'get_promotional_notice_check' ],
+            ]
+        ] );
+
+        register_rest_route( $this->namespace, '/' . $this->rest_base . '/hide-promotion-notice', [
+            [
+                'methods'             => WP_REST_Server::CREATABLE,
+                'callback'            => [ $this, 'handle_hide_promotion_notice' ],
+                'permission_callback' => [ $this, 'get_promotional_notice_check' ],
+            ]
+        ] );
     }
 
     /**
@@ -835,4 +851,98 @@ public function remove_child_docs( $parent_id ) {
             }
         }
     }
+
+    /**
+     * Check permissions for getting
+     *  promotion notice.
+     *
+     * @since 2.1.11
+     *
+     * @param WP_REST_Request $request Current request.
+     *
+     * @return bool|WP_Error
+     */
+    public function get_promotional_notice_check( $request ) {
+        if ( ! current_user_can( 'manage_options' ) ) {
+            return new WP_Error(
+                'wedocs_permission_failure',
+                __( 'You cannot see promotion notices.', 'wedocs' )
+            );
+        }
+
+        return true;
+    }
+
+    /**
+     * Get the data needed for promotional notice.
+     * 
+     * @since 2.1.11
+     *
+     * @return bool|WP_Error|WP_REST_Response response object on success, or WP_Error object on failure.
+     */
+    public function get_promotional_notice() {
+        if ( ! current_user_can( 'manage_options' ) ) {
+            return false;
+        }
+
+		$promos = get_transient( WEDOCS_PROMO_KEY );
+
+		if ( false === $promos ) {
+            $promo_notice_url   = WEDOCS_PROMO_URL;
+            $response           = wp_remote_get( $promo_notice_url, array( 'timeout'  => 15 ) );
+            $promos             = wp_remote_retrieve_body( $response );
+            $promos             = json_decode( $promos, true );
+
+            if ( is_wp_error( $response ) || $response['response']['code'] !== 200 ) {
+                $promos = '[]';
+            }
+
+            set_transient( WEDOCS_PROMO_KEY, $promos, DAY_IN_SECONDS );
+        }
+
+        $promos = ! is_array( $promos ) ? json_decode( $promos, true ) : $promos;
+
+        if ( empty( $promos ) || ! is_array( $promos ) ) {
+            return false;
+        }
+
+        if ( ! isset( $promos['key'] ) || 'hide' === get_option( $promos['key'], 'no' ) ) {
+            return false;
+        }
+
+        $promos['logo_url'] = WEDOCS_LOGO_URL;
+
+		$current_time = wedocs_convert_utc_to_est();
+
+		if (
+			isset( $promos['start_date'] )
+			&& isset( $promos['end_date'] )
+            && strtotime( $promos['start_date'] ) < strtotime( $current_time )
+            && strtotime( $current_time ) < strtotime( $promos['end_date'] )
+            ) {
+            return rest_ensure_response( $promos );
+        }
+
+        return false;
+    }
+
+     /**
+     * Handle promotional notice hidden action
+     * 
+     * @since 2.1.11
+     * 
+     * @param WP_REST_Request $request Current request.
+     *
+     */
+    public function handle_hide_promotion_notice( $request ) {
+        if ( ! empty( $request->get_param('option_name') ) ) {
+			$offer_key = sanitize_text_field( wp_unslash( $request->get_param('option_name') ) );
+
+			update_option( $offer_key, 'hide' );
+
+            wp_send_json_success( 'Successfully dismissed.' );
+		}
+
+        wp_send_json_error( 'Faild to dismiss.' );
+    }
 }
diff --git a/includes/Admin/Admin.php b/includes/Admin/Admin.php
index 16c2d55e..d426fc8e 100644
--- a/includes/Admin/Admin.php
+++ b/includes/Admin/Admin.php
@@ -17,6 +17,10 @@ public function __construct() {
         add_filter( 'submenu_file', [ $this, 'highlight_admin_submenu' ] );
         add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ], 1 );
         add_action( 'admin_notices', [ $this, 'show_wedocs_pro_available_notice' ] );
+
+		if ( ! wedocs_pro_exists() ) {
+			new Promotion();
+		}
     }
 
     /**
diff --git a/includes/Admin/Promotion.php b/includes/Admin/Promotion.php
new file mode 100644
index 00000000..613724e4
--- /dev/null
+++ b/includes/Admin/Promotion.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Wedevs\WeDocs\Admin;
+
+/**
+ * Promotional offer class
+ */
+class Promotion {
+
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		add_action( 'wp_ajax_wedocs_dismiss_promotional_offer_notice', array( $this, 'dismiss_promotional_offer' ) );
+		add_action( 'admin_notices', array( $this, 'promotional_offer' ) );
+	}
+
+	/**
+	 * Promotional offer notice
+	 *
+	 * @since 2.1.11
+	 *
+	 * @return void
+	 */
+	public function promotional_offer() {
+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		$promos = get_transient( WEDOCS_PROMO_KEY );
+
+		if ( false === $promos ) {
+			$promo_notice_url = WEDOCS_PROMO_URL;
+			$response         = wp_remote_get( $promo_notice_url, array( 'timeout' => 15 ) );
+			$promos           = wp_remote_retrieve_body( $response );
+
+			if ( is_wp_error( $response ) || $response['response']['code'] !== 200 ) {
+				$promos = '[]';
+			}
+
+			set_transient( WEDOCS_PROMO_KEY, $promos, DAY_IN_SECONDS );
+		}
+
+		$promos = ! is_array( $promos ) ? json_decode( $promos, true ) : $promos;
+
+		if ( empty( $promos ) || ! is_array( $promos ) ) {
+			return;
+		}
+
+		$promos['logo_url'] = WEDOCS_LOGO_URL;
+
+		$current_time = wedocs_convert_utc_to_est();
+
+		if (
+			isset( $promos['start_date'] )
+			&& isset( $promos['end_date'] )
+			&& strtotime( $promos['start_date'] ) < strtotime( $current_time )
+			&& strtotime( $current_time ) < strtotime( $promos['end_date'] )
+			) {
+			$this->generate_notice( $promos );
+		}
+	}
+
+	/**
+	 * Show admin notice
+	 *
+	 * @param array $args arguments for notice template.
+	 *
+	 * @return void
+	 */
+	public function generate_notice( $args ) {
+		$option_name  = ! empty( $args['key'] ) ? $args['key'] : '';
+		$content      = ! empty( $args['content'] ) ? $args['content'] : '';
+		$title        = ! empty( $args['title'] ) ? $args['title'] : '';
+		$action_url   = ! empty( $args['action_url'] ) ? $args['action_url'] : '';
+		$action_title = ! empty( $args['action_title'] ) ? $args['action_title'] : '';
+		$hide_notice  = get_option( $option_name, 'no' );
+
+		if ( 'hide' === $hide_notice ) {
+			return;
+		}
+		?>
+		<div class="notice notice-success wedocs-notice" id="wedocs-promotion-notice">
+			<div class="wedocs-logo-wrapper">
+				<img src="<?php echo esc_url( WEDOCS_LOGO_URL ); ?>" alt="weDocs Icon">
+			</div>
+			<div class="wedocs-notice-content-wrapper">
+				<h3><?php echo esc_html( $title ); ?></h3>
+				<p><b><?php echo esc_html( $content ); ?></b></p>
+				<a href="<?php echo esc_url_raw( $action_url ); ?>" class="button button-primary"><?php echo esc_html( $action_title ); ?></a>
+			</div>
+			<button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_attr_e( 'Dismiss this notice.', 'wedocs' ); ?></span></button>
+		</div>
+		<style>
+			.wedocs-notice {
+				position: relative;
+				display: flex;
+				padding: 0;
+			}
+
+			.wedocs-logo-wrapper {
+				display: flex;
+				margin-right: 20px;
+			}
+			.wedocs-notice-content-wrapper h3 {
+				font-size: 1.3em;
+				font-weight: 600;
+				margin-top: 1em;
+				margin-bottom: 0.5em;
+			}
+
+			@media screen and (max-width: 768px) {
+				.wedocs-notice-content-wrapper {
+					margin-top: 30px;
+					padding-right: 5px;
+				}
+
+				.wedocs-logo-wrapper {
+					height: auto !important;
+				}
+
+				.wedocs-notice-content-wrapper {
+					margin: 0px !important;
+				}
+			}
+		</style>
+
+		<script type='text/javascript'>
+			jQuery(function($) {
+				$('body').on('click', '#wedocs-promotion-notice .notice-dismiss', function (e) {
+					e.preventDefault();
+					$('#wedocs-promotion-notice').remove();
+					
+					const data = {
+						action   : 'wedocs_dismiss_promotional_offer_notice',
+						_wpnonce : weDocsAdminScriptVars.nonce,
+						dismissed: true,
+						option_name: '<?php echo esc_html( $option_name ); ?>',
+					};
+
+					$.ajax({
+						data,
+						url      : weDocsAdminScriptVars.ajaxurl,
+						type     : 'POST',
+					});
+				});
+			});
+		</script>
+		<?php
+	}
+
+	/**
+	 * Dismiss promotion notice
+	 *
+	 * @since 2.1.11
+	 *
+	 * @return void
+	 */
+	public function dismiss_promotional_offer() {
+		if ( empty( $_POST['_wpnonce'] ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'wedocs-ajax' ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( ! empty( $_POST['dismissed'] ) ) {
+			$offer_key = ! empty( $_POST['option_name'] ) ? sanitize_text_field( wp_unslash( $_POST['option_name'] ) ) : '';
+
+			update_option( $offer_key, 'hide' );
+		}
+	}
+}
\ No newline at end of file
diff --git a/includes/Assets.php b/includes/Assets.php
index b318b4bc..d760d858 100644
--- a/includes/Assets.php
+++ b/includes/Assets.php
@@ -36,6 +36,15 @@ public function register() {
             true
         );
 
+        wp_localize_script(
+            'wedocs-admin-script',
+            'weDocsAdminScriptVars',
+            array(
+                'ajaxurl' => admin_url( 'admin-ajax.php' ),
+                'nonce'   => wp_create_nonce( 'wedocs-ajax' ),
+            ),
+        );
+
         if ( file_exists( WEDOCS_PATH . '/assets/build/index.asset.php' ) ) {
             $react_dependencies = require WEDOCS_PATH . '/assets/build/index.asset.php';
 
diff --git a/includes/functions.php b/includes/functions.php
index 7fcb8009..26be05c4 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -579,3 +579,16 @@ function wedocs_apply_extracted_content( $content, $max_content_number ) {
     // Control content length by substr.
     return ( strlen( $content ) > $max_content_number ) ? substr( $content, 0, $max_content_number ) . '...' : $content;
 }
+
+/**
+ * Convert UTC Time zone to EST timezone
+ *
+ * @param string $date_time
+ * @return string
+ */
+function wedocs_convert_utc_to_est() {
+	$current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
+	$current_time->setTimezone( new DateTimeZone( 'EST' ) );
+
+	return $current_time->format( 'Y-m-d H:i:s T' );
+}
diff --git a/src/components/DocListing/index.js b/src/components/DocListing/index.js
index 36a81aa7..54e7ca93 100644
--- a/src/components/DocListing/index.js
+++ b/src/components/DocListing/index.js
@@ -19,6 +19,7 @@ import settingsStore from '../../data/settings';
 import Swal from "sweetalert2";
 import { userIsAdmin } from "../../utils/helper";
 import NotFound from '../NotFound';
+import WedocsPromoNotice from "../WedocsPromoNotice";
 
 const ListingPage = () => {
   const { id } = useParams();
@@ -168,6 +169,8 @@ const ListingPage = () => {
             />
           </div>
 
+          <WedocsPromoNotice />
+
           <ListingHeader doc={ parentDoc } />
 
           { loading && <DocsListingPlaceholder /> }
diff --git a/src/components/Documentations/index.js b/src/components/Documentations/index.js
index cce39494..e89bf370 100644
--- a/src/components/Documentations/index.js
+++ b/src/components/Documentations/index.js
@@ -13,6 +13,7 @@ import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
 import settingsStore from '../../data/settings';
 import Swal from "sweetalert2";
 import { userIsAdmin } from "../../utils/helper";
+import WedocsPromoNotice from '../WedocsPromoNotice';
 
 const Documentations = () => {
   const docs = useSelect(
@@ -143,6 +144,8 @@ const Documentations = () => {
         </h1>
       </div>
 
+      <WedocsPromoNotice />
+
       { !loading && showActions && need_upgrade && <Upgrade status={ status } /> }
 
       { loading ? <DocsPlaceholder /> : (
diff --git a/src/components/Migrations/index.js b/src/components/Migrations/index.js
index 401cd01d..98109993 100644
--- a/src/components/Migrations/index.js
+++ b/src/components/Migrations/index.js
@@ -7,6 +7,7 @@ import { dispatch, resolveSelect, useSelect } from '@wordpress/data';
 import docsStore from '../../data/docs';
 import MigrationContentMappingModal from './Modals/MigrationContentMappingModal';
 import apiFetch from '@wordpress/api-fetch';
+import WedocsPromoNotice from '../WedocsPromoNotice';
 
 const Migrate = () => {
     const [ migratedId, setMigratedId ] = useState( 0 );
@@ -92,6 +93,7 @@ const Migrate = () => {
 
     return (
         <div className='w-full mt-7'>
+          <WedocsPromoNotice />
             <div className='shadow sm:overflow-hidden sm:rounded-md'>
                 <div className='space-y-6 h-[80vh] flex justify-center align-center bg-white px-4 py-5 sm:p-6'>
                     <div className='w-[800px] text-center self-center mt-1 px-6 py-12'>
diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js
index d55eda50..6eebc98e 100644
--- a/src/components/Settings/index.js
+++ b/src/components/Settings/index.js
@@ -9,6 +9,7 @@ import Swal from 'sweetalert2';
 import { __ } from '@wordpress/i18n';
 import Upgrade from '../Upgrade';
 import { useParams } from 'react-router-dom';
+import WedocsPromoNotice from '../WedocsPromoNotice';
 
 const SettingsPage = () => {
   const { panel } = useParams();
@@ -137,7 +138,7 @@ const SettingsPage = () => {
   return (
     <div className="min-h-full pt-7">
       { showActions && need_upgrade && <Upgrade status={ status } /> }
-
+      <WedocsPromoNotice />
       <main>
         <div className="pb-10 pt-3 sm:px-0">
           <div className="h-100">
diff --git a/src/components/WedocsPromoNotice.js b/src/components/WedocsPromoNotice.js
new file mode 100644
index 00000000..c9f9e8b4
--- /dev/null
+++ b/src/components/WedocsPromoNotice.js
@@ -0,0 +1,55 @@
+import { __ } from '@wordpress/i18n';
+import { dispatch, select, useSelect } from '@wordpress/data';
+import docStore from '../data/docs';
+import { useState } from '@wordpress/element';
+import settingsStore from '../data/settings';
+
+const WedocsPromoNotice = () => {
+  const [ hideNotice, setHideNotice ] = useState( false );
+  const promoNotice = useSelect(
+    ( select ) => select( settingsStore ).getPromoNotice(),
+    []
+  );
+  
+  if ( ! promoNotice ) {
+    return null;
+  }
+
+  const handleDismiss = () => {
+    setHideNotice( true );
+
+    dispatch(docStore)
+    .dismissPromoNotice(promoNotice.key)
+    .then( (result) => {
+      if ( ! result.success ) {
+        return;
+      }
+      
+      dispatch('wedocs/settings').invalidateResolution(
+        'getPromoNotice'
+       );
+    
+      const promo = select( settingsStore ).getPromoNotice();
+    } );
+  }
+
+  if ( hideNotice ) {
+    return null;
+  }
+
+  return (
+    <div className="notice notice-success wedocs-notice flex p-0 relative" id="wedocs-promotion-notice">
+        <div className="wedocs-logo-wrapper flex mr-[20px]">
+            <img src={promoNotice.logo_url} alt="weDocs Icon" className="max-w-none"/>
+        </div>
+        <div className="wedocs-notice-content-wrapper pr-[5px]">
+            <h3 className="text-lg font-semibold mt-4 mb-2">{ promoNotice.title }</h3>
+            <p><b className="text-#3c434a font-[600]">{ promoNotice.content }</b></p>
+            <a href={ promoNotice.action_url } className="button button-primary">{ promoNotice.action_title }</a>
+        </div>
+        <button type="button" className="notice-dismiss" onClick={handleDismiss}><span className="screen-reader-text">{ __( 'Dismiss this notice', 'wedocs' ) }</span></button>
+    </div>
+  );
+}
+
+export default WedocsPromoNotice;
\ No newline at end of file
diff --git a/src/data/docs/actions.js b/src/data/docs/actions.js
index fd100639..5ea0ff55 100644
--- a/src/data/docs/actions.js
+++ b/src/data/docs/actions.js
@@ -157,6 +157,18 @@ const actions = {
     const response = yield { type: 'UPDATE_TO_API', path, data };
     return response;
   },
+
+  *dismissPromoNotice( optionName ) {
+    const response = yield actions.dismissPromoNoticeAPI( optionName );
+
+    return response;
+  },
+
+  dismissPromoNoticeAPI( optionName ) {
+    const path = '/wp/v2/docs/hide-promotion-notice';
+
+    return { type: 'UPDATE_TO_API', path, data: { option_name: optionName } };
+  },
 };
 
 export default actions;
diff --git a/src/data/settings/actions.js b/src/data/settings/actions.js
index 6184eb8a..7df3a149 100644
--- a/src/data/settings/actions.js
+++ b/src/data/settings/actions.js
@@ -15,6 +15,10 @@ const actions = {
     return { type: 'SET_UPGRADE_INFO', needUpgrade };
   },
 
+  setPromoNotice( promoNotice ) {
+    return { type: 'SET_PROMO_NOTICE', promoNotice };
+  },
+
   setMigrateInfo( needMigrate ) {
     return { type: 'SET_MIGRATE_INFO', needMigrate };
   },
diff --git a/src/data/settings/controls.js b/src/data/settings/controls.js
index 1c69f933..8aa1fea4 100644
--- a/src/data/settings/controls.js
+++ b/src/data/settings/controls.js
@@ -1,5 +1,10 @@
 import apiFetch from '@wordpress/api-fetch';
 
+const isProLoaded = wp.hooks.applyFilters(
+  'wedocs_pro_loaded',
+  false
+);
+
 const controls = {
   FETCH_SETTINGS() {
     return apiFetch( {
@@ -32,6 +37,16 @@ const controls = {
       method: 'POST',
     } );
   },
+
+  FETCH_PROMO_NOTICE() {
+    if ( isProLoaded ) {
+      return null;
+    }
+
+    return apiFetch( {
+      path: '/wp/v2/docs/promotion-notice',
+    } );
+  },
 };
 
 export default controls;
diff --git a/src/data/settings/reducer.js b/src/data/settings/reducer.js
index 3a4a91e6..52f7cafe 100644
--- a/src/data/settings/reducer.js
+++ b/src/data/settings/reducer.js
@@ -82,6 +82,8 @@ const reducer = ( state = DEFAULT_SETTINGS_STATE, action ) => {
       };
     case 'SET_UPGRADE_INFO':
       return { ...state, needUpgrade: action.needUpgrade };
+    case 'SET_PROMO_NOTICE':
+      return { ...state, promoNotice: action.promoNotice };
     case 'SET_SAVING':
       return { ...state, saving: action.saving };
     case 'SET_TURNSTILE_SITE_KEY':
diff --git a/src/data/settings/resolvers.js b/src/data/settings/resolvers.js
index 9358aaa3..63fa92b3 100644
--- a/src/data/settings/resolvers.js
+++ b/src/data/settings/resolvers.js
@@ -28,6 +28,13 @@ const resolvers = {
     yield actions.setTurnstileSiteKey( siteKey );
     return actions.setLoading( false );
   },
+
+  *getPromoNotice() {
+    yield actions.setLoading( true );
+    const promoNotice = yield { type: 'FETCH_PROMO_NOTICE' };
+    yield actions.setPromoNotice( promoNotice );
+    return actions.setLoading( false );
+  },
 };
 
 export default resolvers;
diff --git a/src/data/settings/selectors.js b/src/data/settings/selectors.js
index 5aaba74e..fae18e23 100644
--- a/src/data/settings/selectors.js
+++ b/src/data/settings/selectors.js
@@ -48,6 +48,11 @@ const selectors = {
     const { settings } = state;
     return settings?.assistant?.message?.turnstile_site_key;
   },
+
+  getPromoNotice( state ) {
+    const { promoNotice } = state;
+    return promoNotice;
+  },
 };
 
 export default selectors;
diff --git a/wedocs.php b/wedocs.php
index 5c7b75e9..94a32729 100644
--- a/wedocs.php
+++ b/wedocs.php
@@ -138,6 +138,9 @@ public function define_constants() {
         define( 'WEDOCS_PATH', __DIR__ );
         define( 'WEDOCS_URL', plugins_url( '', WEDOCS_FILE ) );
         define( 'WEDOCS_ASSETS', WEDOCS_URL . '/assets' );
+        define( 'WEDOCS_PROMO_KEY', 'wedocs_promo_notices' );
+        define( 'WEDOCS_PROMO_URL', 'https://raw.githubusercontent.com/weDevsOfficial/wedocs-util/refs/heads/master/promotion.json' );
+        define( 'WEDOCS_LOGO_URL', WEDOCS_URL . '/assets/img/wedocs-icon-128x128.png' );
     }
 
     /**


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/components/Settings/index.js (1)</summary><blockquote>

`141-141`: **Consider adding a comment for the promotional notice**

The component is being rendered unconditionally. While the component has internal logic to determine visibility, a comment explaining its purpose would improve code readability, similar to the comment on line 177 for settings footer.

```diff
-      { <WedocsPromoNotice />}
+      {/* Render promotional notice for non-Pro users */}
+      { <WedocsPromoNotice />}
```

</blockquote></details>
<details>
<summary>src/components/DocListing/index.js (1)</summary><blockquote>

`172-173`: **Ensure consistent wrapping of conditional renders**

While the component has internal conditional logic, the JSX expression uses curly braces which are redundant since there's no condition being evaluated here. This differs from the conditional rendering pattern used elsewhere in the file (e.g., line 161).

```diff
-          { <WedocsPromoNotice /> }
+          <WedocsPromoNotice />
```

</blockquote></details>
<details>
<summary>includes/Admin/Promotion.php (1)</summary><blockquote>

`128-149`: <details>
<summary>❓ Verification inconclusive</summary>

**Ensure nonce consistency in AJAX operations**

The JavaScript code uses `weDocsAdminScriptVars.nonce` for the AJAX request, but line 168 verifies against `'wedocs-ajax'`. Ensure these nonces are consistent.




Consider moving the JavaScript to an external file for better maintainability rather than embedding it within PHP.

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for where weDocsAdminScriptVars is defined
echo "Searching for weDocsAdminScriptVars definition..."
rg -A 3 "weDocsAdminScriptVars\s*=" --type php
```

Length of output: 157

---



</details>

**Ensure Nonce Consistency & Externalize Inline JS**

The inline JavaScript in `includes/Admin/Promotion.php` (lines 128–149) uses `weDocsAdminScriptVars.nonce` for the AJAX request, yet elsewhere (line 168) the nonce is being verified against `'wedocs-ajax'`. This discrepancy suggests a potential inconsistency. Notably, our initial search did not reveal any definition for `weDocsAdminScriptVars`, so please verify manually whether this variable is correctly defined (for example, via a `wp_localize_script` call) and that its properties, especially `nonce` and `ajaxurl`, match the nonce expected on the server side.

Additionally, please consider moving the inline JavaScript into an external file for easier maintenance.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 96e78b262beeccd53145a1c1ec257ca6389f5f3b and 1649f2d1bf01e4d8b6601adbeb6640b0d8a96a5b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `assets/images/wedocs-icon-128x128.png` is excluded by `!**/*.png`
* `assets/img/wedocs-logo.svg` is excluded by `!**/*.svg`

</details>

<details>
<summary>📒 Files selected for processing (17)</summary>

* `includes/API/API.php` (2 hunks)
* `includes/Admin/Admin.php` (1 hunks)
* `includes/Admin/Promotion.php` (1 hunks)
* `includes/Assets.php` (1 hunks)
* `includes/functions.php` (1 hunks)
* `src/components/DocListing/index.js` (2 hunks)
* `src/components/Documentations/index.js` (2 hunks)
* `src/components/Migrations/index.js` (2 hunks)
* `src/components/Settings/index.js` (2 hunks)
* `src/components/WedocsPromoNotice.js` (1 hunks)
* `src/data/docs/actions.js` (1 hunks)
* `src/data/settings/actions.js` (1 hunks)
* `src/data/settings/controls.js` (2 hunks)
* `src/data/settings/reducer.js` (1 hunks)
* `src/data/settings/resolvers.js` (1 hunks)
* `src/data/settings/selectors.js` (1 hunks)
* `wedocs.php` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (10)</summary>

<details>
<summary>src/data/settings/actions.js (1)</summary><blockquote>

<details>
<summary>src/data/settings/resolvers.js (1)</summary>

* `promoNotice` (34-34)

</details>

</blockquote></details>
<details>
<summary>src/data/settings/reducer.js (1)</summary><blockquote>

<details>
<summary>src/data/settings/selectors.js (11)</summary>

* `state` (3-3)
* `state` (8-8)
* `state` (13-13)
* `state` (18-18)
* `state` (23-23)
* `state` (28-28)
* `state` (33-33)
* `state` (38-38)
* `state` (43-43)
* `state` (48-48)
* `state` (53-53)

</details>

</blockquote></details>
<details>
<summary>src/data/settings/selectors.js (1)</summary><blockquote>

<details>
<summary>src/data/settings/resolvers.js (1)</summary>

* `promoNotice` (34-34)

</details>

</blockquote></details>
<details>
<summary>src/components/Documentations/index.js (1)</summary><blockquote>

<details>
<summary>src/components/WedocsPromoNotice.js (1)</summary>

* `WedocsPromoNotice` (7-53)

</details>

</blockquote></details>
<details>
<summary>src/components/Settings/index.js (1)</summary><blockquote>

<details>
<summary>src/components/WedocsPromoNotice.js (1)</summary>

* `WedocsPromoNotice` (7-53)

</details>

</blockquote></details>
<details>
<summary>src/components/DocListing/index.js (1)</summary><blockquote>

<details>
<summary>src/components/WedocsPromoNotice.js (1)</summary>

* `WedocsPromoNotice` (7-53)

</details>

</blockquote></details>
<details>
<summary>src/components/Migrations/index.js (1)</summary><blockquote>

<details>
<summary>src/components/WedocsPromoNotice.js (1)</summary>

* `WedocsPromoNotice` (7-53)

</details>

</blockquote></details>
<details>
<summary>src/components/WedocsPromoNotice.js (4)</summary><blockquote>

<details>
<summary>src/components/DocListing/index.js (1)</summary>

* `useSelect` (80-83)

</details>
<details>
<summary>src/components/Documentations/index.js (1)</summary>

* `useSelect` (34-37)

</details>
<details>
<summary>src/components/Settings/index.js (1)</summary>

* `useSelect` (27-30)

</details>
<details>
<summary>src/data/settings/index.js (1)</summary>

* `settingsStore` (10-16)

</details>

</blockquote></details>
<details>
<summary>src/data/settings/resolvers.js (1)</summary><blockquote>

<details>
<summary>src/data/settings/actions.js (1)</summary>

* `actions` (1-55)

</details>

</blockquote></details>
<details>
<summary>includes/Admin/Admin.php (2)</summary><blockquote>

<details>
<summary>includes/functions.php (1)</summary>

* `wedocs_pro_exists` (531-548)

</details>
<details>
<summary>includes/Admin/Promotion.php (1)</summary>

* `Promotion` (8-178)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>src/data/settings/selectors.js (1)</summary>

`52-55`: **Good implementation of the `getPromoNotice` selector.**

The selector follows the established pattern of the file, extracting and returning the promotional notice data from the state. This consistency in code structure makes the codebase more maintainable.

</details>
<details>
<summary>src/data/settings/actions.js (1)</summary>

`18-20`: **Well-structured action creator.**

The `setPromoNotice` action creator follows the same pattern as other action creators in the file, maintaining consistency in code style and implementation. It properly creates an action object with a type and payload.

</details>
<details>
<summary>src/components/Documentations/index.js (2)</summary>

`16-16`: **Clean import of the WedocsPromoNotice component.**

The import is properly placed with the other component imports.

---

`147-147`: **Good integration of the WedocsPromoNotice component.**

The promotional notice component is properly placed after the header section, which is a logical position for a notification. The component is rendered conditionally (based on its internal logic) which is a good approach.

</details>
<details>
<summary>includes/Admin/Admin.php (1)</summary>

`20-23`: **Conditional instantiation of Promotion class looks good**

This code properly adds a conditional check to only instantiate the `Promotion` class when the Pro version of weDocs doesn't exist. This approach ensures that promotional notices are only shown to users who don't have the Pro version.

</details>
<details>
<summary>src/components/Migrations/index.js (2)</summary>

`10-10`: **Import statement for WedocsPromoNotice added correctly**

The import statement for the new promotional notice component is properly added at the top of the file.

---

`96-96`: **Promotional notice component integrated correctly**

The `WedocsPromoNotice` component is properly implemented within the main component's render method. The component is conditionally rendered using curly braces and is placed at an appropriate location in the UI.

</details>
<details>
<summary>src/data/settings/resolvers.js (1)</summary>

`32-37`: **Resolver implementation for promotional notices looks good**

The `getPromoNotice` resolver follows the same pattern as other resolvers in the file - setting loading state, fetching data, updating state, and then finishing the loading state. This maintains consistency in the codebase.

</details>
<details>
<summary>src/data/settings/reducer.js (1)</summary>

`85-86`: **Reducer case for promotional notice properly implemented**

The new case for `SET_PROMO_NOTICE` correctly updates the state by adding the `promoNotice` property with the value from the action. This follows the pattern used in other cases and maintains immutability by using the spread operator.

</details>
<details>
<summary>src/data/settings/controls.js (2)</summary>

`3-6`: **Good approach for Pro version detection**

The use of WordPress hooks system to detect if the Pro version is loaded follows best practices for extensibility. This allows the Pro version to inject its state through the filter.

---

`41-49`: **Well-implemented conditional fetch for promotional notices**

The implementation correctly checks if the Pro version is loaded before making the API request, preventing unnecessary API calls for Pro users. The code follows the established pattern in the file for other fetch methods.

</details>
<details>
<summary>src/components/Settings/index.js (1)</summary>

`12-12`: **Clean component import**

The import statement for the WedocsPromoNotice component follows the project's import conventions.

</details>
<details>
<summary>includes/Assets.php (1)</summary>

`39-46`: **Properly localized admin script**

Good implementation of script localization with security nonce. This provides the necessary AJAX URL and security token for the admin script to make authenticated AJAX requests.

</details>
<details>
<summary>src/components/DocListing/index.js (1)</summary>

`22-22`: **Clean component import**

The import statement for the WedocsPromoNotice component follows the project's import conventions.

</details>
<details>
<summary>src/data/docs/actions.js (2)</summary>

`161-165`: **Well-implemented generator function for dismissing promotional notices**

The implementation of `dismissPromoNotice` as a generator function follows the established pattern in this file and correctly uses `yield` to wait for the API response before returning it.

---

`167-171`: **LGTM: API action follows consistent pattern**

The `dismissPromoNoticeAPI` method correctly constructs the action object for the API endpoint, following the same pattern as other API-related methods in this file.

</details>
<details>
<summary>includes/Admin/Promotion.php (1)</summary>

`25-62`: <details>
<summary>✅ Verification successful</summary>

**Verify the existence of wedocs_convert_utc_to_est helper function**

The method uses `wedocs_convert_utc_to_est()` in line 52, but it's not clear if this function is defined elsewhere in the codebase.




Also, the caching mechanism fetches data from a remote URL on every page load if the transient has expired (24 hours). Consider extending the transient duration to reduce external API calls.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the helper function is defined in the codebase
echo "Searching for wedocs_convert_utc_to_est function definition..."
rg -A 2 "function wedocs_convert_utc_to_est"
```

Length of output: 414

---



</details>

**Verification Summary: Helper Function Exists & Caching Duration Suggestion**

- Verified that `wedocs_convert_utc_to_est()` is defined in `includes/functions.php`.
- The transient caching currently uses a 24‐hour duration (via `DAY_IN_SECONDS`), which means that after expiration the API is called on the next page load. Consider whether extending this duration might further reduce external API calls under your site's usage patterns.

</details>
<details>
<summary>includes/API/API.php (3)</summary>

`236-251`: **Well-structured API route registration.**

The routes for promotional notices are appropriately registered with proper HTTP methods, callbacks, and permission checks. Both endpoints use the same permission callback function which maintains consistency.

---

`855-874`: **Good permission implementation.**

The permission check appropriately restricts access to users with `manage_options` capability, which is typically reserved for administrators, ensuring that promotional notices are only visible to authorized users.

---

`876-927`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify constants are defined and consider timezone handling.**

The implementation for retrieving promotional notices includes appropriate caching via transients and proper error handling for API responses. However, there are a few considerations:

1. The code depends on several constants (`WEDOCS_PROMO_KEY`, `WEDOCS_PROMO_URL`, `WEDOCS_LOGO_URL`) that should be defined elsewhere.
2. The timezone conversion is hardcoded to EST which may not be appropriate for all users.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the required constants are defined in the plugin
echo "Searching for constant definitions..."
grep -r "define.*WEDOCS_PROMO_" --include="*.php" .
grep -r "define.*WEDOCS_LOGO_URL" --include="*.php" .

# Look for the timezone conversion function
echo "Checking timezone conversion function..."
grep -r "function wedocs_convert_utc_to_est" --include="*.php" .
```

Length of output: 729

---



</details>

**Attention: Constants verified; review timezone handling for broader applicability**

- The constants `WEDOCS_PROMO_KEY`, `WEDOCS_PROMO_URL`, and `WEDOCS_LOGO_URL` are properly defined in `wedocs.php`.
- The timezone conversion function `wedocs_convert_utc_to_est()` is implemented in `includes/functions.php`.
- While caching and error handling are appropriately addressed, note that the conversion to Eastern Standard Time is hardcoded. Consider whether using the site's configured timezone (or a more flexible approach) might better suit a global user base.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:22:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>includes/functions.php (1)</summary><blockquote>

`582-594`: **Refactor the function to correctly handle the parameter and DST**  
This function docblock references a `$date_time` parameter that's never used and still relies on `'EST'`, which doesn't account for Daylight Saving Time. A better approach might be to either remove the unused parameter or accept and parse it appropriately, and use `'America/New_York'` instead of `'EST'`.

</blockquote></details>
<details>
<summary>includes/API/API.php (1)</summary><blockquote>

`929-947`: **Reintroduce validation for the “option_name” parameter**  
Currently, any arbitrary key can be updated in the options table. Consider validating that the received `option_name` is indeed a recognized promotional notice key, to address potential security and data integrity concerns.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>includes/API/API.php (1)</summary><blockquote>

`946-946`: **Correct the spelling in the error message**  
Typo “Faild” should be “Failed”.


```diff
- wp_send_json_error( 'Faild to dismiss.' );
+ wp_send_json_error( 'Failed to dismiss.' );
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1649f2d1bf01e4d8b6601adbeb6640b0d8a96a5b and 8ac10d71981fbb40fa34289ea8edaaed5c83a552.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `includes/API/API.php` (2 hunks)
* `includes/functions.php` (1 hunks)
* `src/components/DocListing/index.js` (2 hunks)
* `src/components/Documentations/index.js` (2 hunks)
* `src/components/Migrations/index.js` (2 hunks)
* `src/components/Settings/index.js` (2 hunks)
* `src/components/WedocsPromoNotice.js` (1 hunks)
* `wedocs.php` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary>

* src/components/Documentations/index.js
* src/components/Settings/index.js
* src/components/Migrations/index.js
* src/components/DocListing/index.js
* src/components/WedocsPromoNotice.js
* wedocs.php

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>includes/API/API.php (4)</summary>

`236-243`: **Registration of the promotional notice route looks good**  
All parameters for `/promotion-notice` route are straightforward, and permission checks appear in the callback function.

---

`245-251`: **Registration of the hide promotional notice route looks good**  
Route definition is consistent, and permission checks happen in the callback.

---

`855-874`: **Permission check logic is correct**  
Verifying `manage_options` is appropriate for controlling access to promotion notices.

---

`876-927`: **No issues with promotional notice retrieval implementation**  
Remote fetching, transient usage, and date-based checks are implemented correctly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:05:47Z
user: login: iftakharul-islam
body: Looks Good
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T11:36:36Z

Review comments:
diff_hunk: @@ -579,3 +579,16 @@ function wedocs_apply_extracted_content( $content, $max_content_number ) {
     // Control content length by substr.
     return ( strlen( $content ) > $max_content_number ) ? substr( $content, 0, $max_content_number ) . '...' : $content;
 }
+
+/**
+ * Convert EST Time zone to UTC timezone
+ *
+ * @param string $date_time
+ * @return string
+ */
+function wedocs_convert_utc_to_est() {
+	$current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
+	$current_time->setTimezone( new DateTimeZone( 'EST' ) );
+
+	return $current_time->format( 'Y-m-d H:i:s T' );
+}
path: includes/functions.php
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix inconsistencies in the `wedocs_convert_utc_to_est` function.**

There are several issues with this function:

1. The function comment says "Convert EST Time zone to UTC timezone" but the function does the opposite (converts UTC to EST).
2. The function signature includes a `$date_time` parameter that's not used in the function.
3. The function uses 'EST' which doesn't account for daylight saving time.


```diff
 /**
- * Convert EST Time zone to UTC timezone
+ * Convert UTC Time zone to EST timezone
  *
- * @param string $date_time
  * @return string
  */
-function wedocs_convert_utc_to_est() {
+function wedocs_convert_utc_to_est() {
 	$current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
-	$current_time->setTimezone( new DateTimeZone( 'EST' ) );
+	$current_time->setTimezone( new DateTimeZone( 'America/New_York' ) );

 	return $current_time->format( 'Y-m-d H:i:s T' );
 }
```

Consider also adding an optional parameter to allow converting a specific date-time instead of only the current time.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
/**
 * Convert UTC Time zone to EST timezone
 *
 * @return string
 */
function wedocs_convert_utc_to_est() {
	$current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
	$current_time->setTimezone( new DateTimeZone( 'America/New_York' ) );

	return $current_time->format( 'Y-m-d H:i:s T' );
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +138,9 @@ public function define_constants() {
         define( 'WEDOCS_PATH', __DIR__ );
         define( 'WEDOCS_URL', plugins_url( '', WEDOCS_FILE ) );
         define( 'WEDOCS_ASSETS', WEDOCS_URL . '/assets' );
+        define( 'WEDOCS_PROMO_KEY', 'wedocs_promo_notices' );
+        define( 'WEDOCS_PROMO_URL', 'https://raw.githubusercontent.com/welabs-ltd/wedocs-util/master/promotion.json' );
+        define( 'WEDOCS_LOGO_URL', WEDOCS_URL . '/assets/images/wedocs-icon-128x128.png' );
path: wedocs.php
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider security implications of using hardcoded external URL**

The constants are well-defined, but there's a potential security concern with fetching data from an external GitHub URL. If the repository is compromised or moved, it could affect your application.


Consider implementing:
1. A fallback mechanism if the remote URL is unavailable
2. A way to configure the URL via settings rather than hardcoding it
3. A hash verification to ensure the integrity of the retrieved content

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the URL is accessible and returns a valid JSON response
response=$(curl -s -o /dev/null -w "%{http_code}" https://raw.githubusercontent.com/welabs-ltd/wedocs-util/master/promotion.json)
echo "HTTP response code: $response"

if [ "$response" -eq 200 ]; then
  echo "URL is accessible"
  # Check if the response is valid JSON
  content=$(curl -s https://raw.githubusercontent.com/welabs-ltd/wedocs-util/master/promotion.json)
  echo "$content" | jq empty
  if [ $? -eq 0 ]; then
    echo "Response is valid JSON"
  else
    echo "Response is not valid JSON"
  fi
else
  echo "URL is not accessible"
fi
```

Length of output: 1022

---



</details>

**Security Alert: Hardcoded External URL Requires Mitigations**

While the verification script confirms that the current external URL is accessible and returns valid JSON, relying on a hardcoded GitHub URL still poses potential risks. If the repository is compromised or the URL changes in the future, the application could be adversely affected.

**Recommendations:**
- **Fallback Mechanism:** Implement a fallback to a cached or default local version in case the external URL becomes unreachable.
- **Configurable URL:** Allow the URL to be overridden via application settings rather than hardcoding it.
- **Integrity Verification:** Consider adding a hash or digital signature check to verify the integrity of the fetched JSON content.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { __ } from '@wordpress/i18n';
+import { dispatch, select, useSelect } from '@wordpress/data';
+import docStore from '../data/docs';
+import { useState } from '@wordpress/element';
+import settingsStore from '../data/settings';
+
+const WedocsPromoNotice = () => {
+  const [ hideNotice, setHideNotice ] = useState( false );
+  const promoNotice = useSelect(
+    ( select ) => select( settingsStore ).getPromoNotice(),
+    []
+  );
+  
+  if ( ! promoNotice ) {
+    return null;
+  }
+
+  const handleDismiss = () => {
+    setHideNotice( true );
+
+    dispatch(docStore)
+    .dismissPromoNotice(promoNotice.key)
+    .then( (result) => {
+      if ( 'Success' !== result ) {
+        return;
+      }
+      
+      dispatch('wedocs/settings').invalidateResolution(
+        'getPromoNotice'
+       );
+    
+      const promo = select( settingsStore ).getPromoNotice();
+    } );
+  }
+
+  if ( hideNotice ) {
+    return null;
+  }
+
+  return (
+    <div class="notice notice-success wedocs-notice flex p-0 relative" id="wedocs-promotion-notice">
+        <div class="wedocs-logo-wrapper flex mr-[20px]">
+            <img src={promoNotice.logo_url} alt="weDocs Icon" class="max-w-none"/>
+        </div>
+        <div class="wedocs-notice-content-wrapper pr-[5px]">
+            <h3 class="text-lg font-semibold mt-4 mb-2">{ promoNotice.title }</h3>
+            <p><b class="text-#3c434a font-[600]">{ promoNotice.content }</b></p>
+            <a href={ promoNotice.action_url } class="button button-primary">{ promoNotice.action_title }</a>
+        </div>
+        <button type="button" class="notice-dismiss" onClick={handleDismiss}><span class="screen-reader-text">{ __( 'Dismiss this notice', 'wedocs' ) }</span></button>
+    </div>
+  );
path: src/components/WedocsPromoNotice.js
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix React JSX attributes and CSS syntax**

JSX uses `className` instead of `class` for CSS class attributes, and CSS properties like text color should be properly formatted.


```diff
  return (
-    <div class="notice notice-success wedocs-notice flex p-0 relative" id="wedocs-promotion-notice">
-        <div class="wedocs-logo-wrapper flex mr-[20px]">
-            <img src={promoNotice.logo_url} alt="weDocs Icon" class="max-w-none"/>
+    <div className="notice notice-success wedocs-notice flex p-0 relative" id="wedocs-promotion-notice">
+        <div className="wedocs-logo-wrapper flex mr-[20px]">
+            <img src={promoNotice.logo_url} alt="weDocs Icon" className="max-w-none"/>
        </div>
-        <div class="wedocs-notice-content-wrapper pr-[5px]">
-            <h3 class="text-lg font-semibold mt-4 mb-2">{ promoNotice.title }</h3>
-            <p><b class="text-#3c434a font-[600]">{ promoNotice.content }</b></p>
-            <a href={ promoNotice.action_url } class="button button-primary">{ promoNotice.action_title }</a>
+        <div className="wedocs-notice-content-wrapper pr-[5px]">
+            <h3 className="text-lg font-semibold mt-4 mb-2">{ promoNotice.title }</h3>
+            <p><b className="text-[#3c434a] font-[600]">{ promoNotice.content }</b></p>
+            <a href={ promoNotice.action_url } className="button button-primary">{ promoNotice.action_title }</a>
        </div>
-        <button type="button" class="notice-dismiss" onClick={handleDismiss}><span class="screen-reader-text">{ __( 'Dismiss this notice', 'wedocs' ) }</span></button>
+        <button type="button" className="notice-dismiss" onClick={handleDismiss}><span className="screen-reader-text">{ __( 'Dismiss this notice', 'wedocs' ) }</span></button>
    </div>
  );
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  return (
    <div className="notice notice-success wedocs-notice flex p-0 relative" id="wedocs-promotion-notice">
        <div className="wedocs-logo-wrapper flex mr-[20px]">
            <img src={promoNotice.logo_url} alt="weDocs Icon" className="max-w-none"/>
        </div>
        <div className="wedocs-notice-content-wrapper pr-[5px]">
            <h3 className="text-lg font-semibold mt-4 mb-2">{ promoNotice.title }</h3>
            <p><b className="text-[#3c434a] font-[600]">{ promoNotice.content }</b></p>
            <a href={ promoNotice.action_url } className="button button-primary">{ promoNotice.action_title }</a>
        </div>
        <button type="button" className="notice-dismiss" onClick={handleDismiss}>
            <span className="screen-reader-text">{ __( 'Dismiss this notice', 'wedocs' ) }</span>
        </button>
    </div>
  );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits ddec648 to 8ac10d7
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+import { __ } from '@wordpress/i18n';
+import { dispatch, select, useSelect } from '@wordpress/data';
+import docStore from '../data/docs';
+import { useState } from '@wordpress/element';
+import settingsStore from '../data/settings';
+
+const WedocsPromoNotice = () => {
+  const [ hideNotice, setHideNotice ] = useState( false );
+  const promoNotice = useSelect(
+    ( select ) => select( settingsStore ).getPromoNotice(),
+    []
+  );
+  
+  if ( ! promoNotice ) {
+    return null;
+  }
+
+  const handleDismiss = () => {
+    setHideNotice( true );
+
+    dispatch(docStore)
+    .dismissPromoNotice(promoNotice.key)
+    .then( (result) => {
+      if ( 'Success' !== result ) {
+        return;
+      }
+      
+      dispatch('wedocs/settings').invalidateResolution(
+        'getPromoNotice'
+       );
+    
+      const promo = select( settingsStore ).getPromoNotice();
+    } );
+  }
path: src/components/WedocsPromoNotice.js
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix React best practices and unused variable**

There are a few issues with this block of code:

1. The `promo` variable on line 32 is defined but never used
2. The success check is hardcoded to the string 'Success', which might be fragile



```diff
    dispatch(docStore)
    .dismissPromoNotice(promoNotice.key)
    .then( (result) => {
-      if ( 'Success' !== result ) {
+      if ( !result || result.error ) {
        return;
      }
      
      dispatch('wedocs/settings').invalidateResolution(
        'getPromoNotice'
       );
    
-      const promo = select( settingsStore ).getPromoNotice();
    } );
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    dispatch(docStore)
    .dismissPromoNotice(promoNotice.key)
    .then( (result) => {
      if ( !result || result.error ) {
        return;
      }
      
      dispatch('wedocs/settings').invalidateResolution(
        'getPromoNotice'
      );
    } );
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,178 @@
+<?php
+
+namespace Wedevs\WeDocs\Admin;
+
+/**
+ * Promotional offer class
+ */
+class Promotion {
+
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		add_action( 'wp_ajax_wedocs_dismiss_promotional_offer_notice', array( $this, 'dismiss_promotional_offer' ) );
+		add_action( 'admin_notices', array( $this, 'promotional_offer' ) );
+	}
+
+	/**
+	 * Promotional offer notice
+	 *
+	 * @since 2.1.11
+	 *
+	 * @return void
+	 */
+	public function promotional_offer() {
+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		$promos = get_transient( WEDOCS_PROMO_KEY );
+
+		if ( false === $promos ) {
+			$promo_notice_url = WEDOCS_PROMO_URL;
+			$response         = wp_remote_get( $promo_notice_url, array( 'timeout' => 15 ) );
+			$promos           = wp_remote_retrieve_body( $response );
+
+			if ( is_wp_error( $response ) || $response['response']['code'] !== 200 ) {
+				$promos = '[]';
+			}
+
+			set_transient( WEDOCS_PROMO_KEY, $promos, DAY_IN_SECONDS );
+		}
+
+		$promos = ! is_array( $promos ) ? json_decode( $promos, true ) : $promos;
+
+		if ( empty( $promos ) || ! is_array( $promos ) ) {
+			return;
+		}
+
+		$promos['logo_url'] = WEDOCS_LOGO_URL;
+
+		$current_time = wedocs_convert_utc_to_est();
+
+		if (
+			isset( $promos['start_date'] )
+			&& isset( $promos['end_date'] )
+			&& strtotime( $promos['start_date'] ) < strtotime( $current_time )
+			&& strtotime( $current_time ) < strtotime( $promos['end_date'] )
+			) {
+			$this->generate_notice( $promos );
+		}
+	}
+
+	/**
+	 * Show admin notice
+	 *
+	 * @param array $args arguments for notice template.
+	 *
+	 * @return void
+	 */
+	public function generate_notice( $args ) {
+		$option_name  = ! empty( $args['key'] ) ? $args['key'] : '';
+		$content      = ! empty( $args['content'] ) ? $args['content'] : '';
+		$title        = ! empty( $args['title'] ) ? $args['title'] : '';
+		$action_url   = ! empty( $args['action_url'] ) ? $args['action_url'] : '';
+		$action_title = ! empty( $args['action_title'] ) ? $args['action_title'] : '';
+		$hide_notice  = get_option( $option_name, 'no' );
+
+		if ( 'hide' === $hide_notice ) {
+			return;
+		}
+		?>
+		<div class="notice notice-success wedocs-notice" id="wedocs-promotion-notice">
+			<div class="wedocs-logo-wrapper">
+				<img src="<?php echo esc_url( WEDOCS_LOGO_URL ); ?>" alt="weDocs Icon">
+			</div>
+			<div class="wedocs-notice-content-wrapper">
+				<h3><?php echo esc_html( $title ); ?></h3>
+				<p><b><?php echo esc_html( $content ); ?></b></p>
+				<a href="<?php echo esc_url_raw( $action_url ); ?>" class="button button-primary"><?php echo esc_html( $action_title ); ?></a>
+			</div>
+			<button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_attr_e( 'Dismiss this notice.', 'wedocs' ); ?></span></button>
+		</div>
+		<style>
+			.wedocs-notice {
+				position: relative;
+				display: flex;
+				padding: 0;
+			}
+
+			.wedocs-logo-wrapper {
+				display: flex;
+				margin-right: 20px;
+			}
+			.wedocs-notice-content-wrapper h3 {
+				font-size: 1.3em;
+				font-weight: 600;
+				margin-top: 1em;
+				margin-bottom: 0.5em;
+			}
+
+			@media screen and (max-width: 768px) {
+				.wedocs-notice-content-wrapper {
+					margin-top: 30px;
+					padding-right: 5px;
+				}
+
+				.wedocs-logo-wrapper {
+					height: auto !important;
+				}
+
+				.wedocs-notice-content-wrapper {
+					margin: 0px !important;
+				}
+			}
+		</style>
+
+		<script type='text/javascript'>
+			jQuery(function($) {
+				$('body').on('click', '#wedocs-promotion-notice .notice-dismiss', function (e) {
+					e.preventDefault();
+					$('#wedocs-promotion-notice').remove();
+					
+					const data = {
+						action   : 'wedocs_dismiss_promotional_offer_notice',
+						_wpnonce : weDocsAdminScriptVars.nonce,
+						dismissed: true,
+						option_name: '<?php echo esc_html( $option_name ); ?>',
+					};
+
+					$.ajax({
+						data,
+						url      : weDocsAdminScriptVars.ajaxurl,
+						type     : 'POST',
+					});
+				});
+			});
+		</script>
+		<?php
+	}
+
+	/**
+	 * Dismiss promotion notice
+	 *
+	 * @since 2.1.11
+	 *
+	 * @return void
+	 */
+	public function dismiss_promotional_offer() {
+		if ( empty( $_POST['_wpnonce'] ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'wedocs-ajax' ) ) {
+			wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
+		}
+
+		if ( ! empty( $_POST['dismissed'] ) ) {
+			$offer_key = ! empty( $_POST['option_name'] ) ? sanitize_text_field( wp_unslash( $_POST['option_name'] ) ) : '';
+
+			update_option( $offer_key, 'hide' );
+		}
+	}
path: includes/Admin/Promotion.php
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve security check flow**

The security checks could be more efficient by combining them and providing more specific error messages.


```diff
public function dismiss_promotional_offer() {
-    if ( empty( $_POST['_wpnonce'] ) ) {
-        wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
-    }
-
-    if ( ! current_user_can( 'manage_options' ) ) {
-        wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
-    }
-
-    if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'wedocs-ajax' ) ) {
-        wp_send_json_error( __( 'Unauthorized operation', 'wedocs' ) );
-    }
+    // Check capability first
+    if ( ! current_user_can( 'manage_options' ) ) {
+        wp_send_json_error( __( 'You do not have permission to perform this action', 'wedocs' ) );
+    }
+
+    // Then verify nonce
+    if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'wedocs-ajax' ) ) {
+        wp_send_json_error( __( 'Security check failed', 'wedocs' ) );
+    }

    if ( ! empty( $_POST['dismissed'] ) ) {
        $offer_key = ! empty( $_POST['option_name'] ) ? sanitize_text_field( wp_unslash( $_POST['option_name'] ) ) : '';

        update_option( $offer_key, 'hide' );
+       wp_send_json_success( __( 'Notice dismissed', 'wedocs' ) );
    }
+   wp_send_json_error( __( 'Invalid request parameters', 'wedocs' ) );
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	/**
	 * Dismiss promotion notice
	 *
	 * @since 2.1.11
	 *
	 * @return void
	 */
	public function dismiss_promotional_offer() {
		// Check capability first
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'You do not have permission to perform this action', 'wedocs' ) );
		}
		
		// Then verify nonce
		if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'wedocs-ajax' ) ) {
			wp_send_json_error( __( 'Security check failed', 'wedocs' ) );
		}
		
		if ( ! empty( $_POST['dismissed'] ) ) {
			$offer_key = ! empty( $_POST['option_name'] ) ? sanitize_text_field( wp_unslash( $_POST['option_name'] ) ) : '';
			
			update_option( $offer_key, 'hide' );
			wp_send_json_success( __( 'Notice dismissed', 'wedocs' ) );
		}
		wp_send_json_error( __( 'Invalid request parameters', 'wedocs' ) );
	}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -835,4 +851,98 @@ public function remove_child_docs( $parent_id ) {
             }
         }
     }
+
+    /**
+     * Check permissions for getting
+     *  promotion notice.
+     *
+     * @since 2.1.11
+     *
+     * @param WP_REST_Request $request Current request.
+     *
+     * @return bool|WP_Error
+     */
+    public function get_promotional_notice_check( $request ) {
+        if ( ! current_user_can( 'manage_options' ) ) {
+            return new WP_Error(
+                'wedocs_permission_failure',
+                __( 'You cannot see promotion notices.', 'wedocs' )
+            );
+        }
+
+        return true;
+    }
+
+    /**
+     * Get the data needed for promotional notice.
+     * 
+     * @since 2.1.11
+     *
+     * @return bool|WP_Error|WP_REST_Response response object on success, or WP_Error object on failure.
+     */
+    public function get_promotional_notice() {
+        if ( ! current_user_can( 'manage_options' ) ) {
+            return false;
+        }
+
+		$promos = get_transient( WEDOCS_PROMO_KEY );
+
+		if ( false === $promos ) {
+            $promo_notice_url   = WEDOCS_PROMO_URL;
+            $response           = wp_remote_get( $promo_notice_url, array( 'timeout'  => 15 ) );
+            $promos             = wp_remote_retrieve_body( $response );
+            $promos             = json_decode( $promos, true );
+
+            if ( is_wp_error( $response ) || $response['response']['code'] !== 200 ) {
+                $promos = '[]';
+            }
+
+            set_transient( WEDOCS_PROMO_KEY, $promos, DAY_IN_SECONDS );
+        }
+
+        $promos = ! is_array( $promos ) ? json_decode( $promos, true ) : $promos;
+
+        if ( empty( $promos ) || ! is_array( $promos ) ) {
+            return false;
+        }
+
+        if ( ! isset( $promos['key'] ) || 'hide' === get_option( $promos['key'], 'no' ) ) {
+            return false;
+        }
+
+        $promos['logo_url'] = WEDOCS_LOGO_URL;
+
+		$current_time = wedocs_convert_utc_to_est();
+
+		if (
+			isset( $promos['start_date'] )
+			&& isset( $promos['end_date'] )
+            && strtotime( $promos['start_date'] ) < strtotime( $current_time )
+            && strtotime( $current_time ) < strtotime( $promos['end_date'] )
+            ) {
+            return rest_ensure_response( $promos );
+        }
+
+        return false;
+    }
+
+     /**
+     * Handle promotional notice hidden action
+     * 
+     * @since 2.1.11
+     * 
+     * @param WP_REST_Request $request Current request.
+     *
+     */
+    public function handle_hide_promotion_notice( $request ) {
+        if ( ! empty( $request->get_param('option_name') ) ) {
+			$offer_key = sanitize_text_field( wp_unslash( $request->get_param('option_name') ) );
+
+			update_option( $offer_key, 'hide' );
+
+            return rest_ensure_response( 'Success' );
+		}
+
+        return rest_ensure_response( 'Option name is missing.' );
+    }
path: includes/API/API.php
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for option_name parameter.**

While the code correctly sanitizes the `option_name` parameter, it doesn't validate that it's a legitimate promotional notice key before updating the option. This could potentially allow setting arbitrary options.


Consider adding validation to ensure the option_name matches expected promotional notice keys:

```diff
public function handle_hide_promotion_notice( $request ) {
    if ( ! empty( $request->get_param('option_name') ) ) {
        $offer_key = sanitize_text_field( wp_unslash( $request->get_param('option_name') ) );
+       
+       // Verify this is a valid promotion key
+       $promos = get_transient( WEDOCS_PROMO_KEY );
+       if ( $promos && isset( $promos['key'] ) && $promos['key'] === $offer_key ) {
            update_option( $offer_key, 'hide' );
            return rest_ensure_response( 'Success' );
+       }
+       
+       return rest_ensure_response( 'Invalid promotion key.' );
    }

    return rest_ensure_response( 'Option name is missing.' );
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
     /**
     * Handle promotional notice hidden action
     * 
     * @since 2.1.11
     * 
     * @param WP_REST_Request $request Current request.
     *
     */
    public function handle_hide_promotion_notice( $request ) {
        if ( ! empty( $request->get_param('option_name') ) ) {
			$offer_key = sanitize_text_field( wp_unslash( $request->get_param('option_name') ) );
            
            // Verify this is a valid promotion key
            $promos = get_transient( WEDOCS_PROMO_KEY );
            if ( $promos && isset( $promos['key'] ) && $promos['key'] === $offer_key ) {
			    update_option( $offer_key, 'hide' );
                return rest_ensure_response( 'Success' );
            }
            
            return rest_ensure_response( 'Invalid promotion key.' );
		}

        return rest_ensure_response( 'Option name is missing.' );
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-10T09:22:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-10T09:18:42Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request extends the promotional notice functionality across the system. It adds new REST API routes and methods in the API class for retrieving and hiding promotional notices. In the admin area, a new Promotion class is introduced—with conditional instantiation—to display and dismiss notices via AJAX. Additionally, admin scripts are localized and a helper function for converting UTC to EST is added. On the frontend, a new React component (WedocsPromoNotice) is integrated into multiple pages. Finally, corresponding actions, controls, reducers, resolvers, selectors, and constants are added to support and manage promotional notices.

## Changes

| File(s) | Change Summary |
| ------- | -------------- |
| `includes/API/API.php` | Added new REST API routes (`/promotion-notice`, `/hide-promotion-notice`) and methods (`get_promotional_notice`, `get_promotional_notice_check`, `handle_hide_promotion_notice`) to manage promotional notices. |
| `includes/Admin/Admin.php`, `includes/Admin/Promotion.php` | Introduced a conditional check to instantiate a new `Promotion` class. The new class in the Admin namespace handles promotional offer display, dismissal via AJAX, and renders the notice with HTML, CSS, and JS. |
| `includes/Assets.php`, `includes/functions.php` | Localized the `wedocs-admin-script` with AJAX URL and nonce; added `wedocs_convert_utc_to_est()` to convert UTC time to EST. |
| `src/components/DocListing/index.js`, `src/components/Documentations/index.js`, `src/components/Migrations/index.js`, `src/components/Settings/index.js`, `src/components/WedocsPromoNotice.js` | Integrated a new `WedocsPromoNotice` React component to render promotional notices across various pages. |
| `src/data/docs/actions.js`, `src/data/settings/actions.js`, `src/data/settings/controls.js`, `src/data/settings/reducer.js`, `src/data/settings/resolvers.js`, `src/data/settings/selectors.js` | Added new actions, controls, reducer cases, resolvers, and selectors for fetching and dismissing promotional notices via API. |
| `wedocs.php` | Defined new constants: `WEDOCS_PROMO_KEY`, `WEDOCS_PROMO_URL`, and `WEDOCS_LOGO_URL` for promotional notice configuration. |

## Poem
> I'm a hopping rabbit in a code-filled glen,  
> New routes and promos cheer me again.  
> Admin and API join my joyful jam,  
> With React and actions—oh, what a slam!  
> Constants and converts guide my lyrical trot,  
> Each dismissal and notice in a playful plot.  
> I celebrate with a twitch and a hop—let's give it a shot!

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d921e1cbf4784750e5f5833aa788e393eef09a6f and f1fbc6964347754a1f26945e6533672cfe671cd4.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `wedocs.php` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* wedocs.php

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=weDevsOfficial/wedocs-plugin&utm_content=209):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: Rubaiyat-E-Mohammad
created_at: 2025-04-15T04:18:05Z
body: Could not get any promotional banner on wedocs dashboard
author_association: NONE