From 122ad4d495616faf46b900223e2e7489735fded9 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Thu, 28 May 2026 10:00:52 +0530 Subject: [PATCH 1/5] wip --- inc/Core.php | 31 +++++++++++++++++++++ inc/patterns/cover-background.php | 46 ++++++++++++++----------------- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/inc/Core.php b/inc/Core.php index 4d635941..74a4962a 100644 --- a/inc/Core.php +++ b/inc/Core.php @@ -56,6 +56,37 @@ private function run_hooks() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'add_editor_styles' ) ); add_filter( 'raft_strings', array( $this, 'strings' ) ); + add_filter( 'home_template_hierarchy', array( $this, 'home_falls_back_to_archive' ) ); + } + + /** + * Let the blog/posts page render through `archive.html` when no `home.html` + * is present. WP's default hierarchy for the home/posts page is + * [home, index] — archive is not in the chain — so a single archive + * customization wouldn't otherwise show up on the page assigned as Posts + * page. Inserting `archive` ahead of `index` keeps `home.html` winning + * if a child theme ever provides one, while giving the archive template + * one source of truth for all post-listing contexts. + * + * @param array $hierarchy Candidate template slugs in lookup order. + * + * @return array + */ + public function home_falls_back_to_archive( $hierarchy ) { + if ( in_array( 'archive.php', $hierarchy, true ) ) { + return $hierarchy; + } + + $index = array_search( 'index.php', $hierarchy, true ); + + if ( false === $index ) { + $hierarchy[] = 'archive.php'; + return $hierarchy; + } + + array_splice( $hierarchy, $index, 0, 'archive.php' ); + + return $hierarchy; } /** diff --git a/inc/patterns/cover-background.php b/inc/patterns/cover-background.php index 4e038e1f..761d4243 100644 --- a/inc/patterns/cover-background.php +++ b/inc/patterns/cover-background.php @@ -22,36 +22,32 @@ 'title' => __( 'Cover with Background', 'raft' ), 'categories' => array( 'raft/heroes_page_titles' ), 'content' => ' - -
- -
- - -
- -
- -

' . esc_html( $raft_strings['hero_title'] ) . '

- + +
+ + +
+ +
+ +

' . esc_html( $raft_strings['hero_title'] ) . '

+ - -

' . esc_html( $raft_strings['short_text'] ) . '

- + +

' . esc_html( $raft_strings['short_text'] ) . '

+ - - - + + - +
+
-
- + ', ); From 645f3fbaae199b3ff709ec40db77f62ec7720b11 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Fri, 29 May 2026 15:09:12 +0530 Subject: [PATCH 2/5] wip --- assets/js/wizard-promo.js | 320 ++++++++++++++++++++++++++++++++++++++ inc/Admin.php | 2 +- inc/Core.php | 3 + inc/Dashboard.php | 154 ++++++++++++++++++ inc/Pro_Promotions.php | 164 +++++++++++++++++++ inc/Wizard_Promo.php | 102 ++++++++++++ 6 files changed, 744 insertions(+), 1 deletion(-) create mode 100644 assets/js/wizard-promo.js create mode 100644 inc/Dashboard.php create mode 100644 inc/Pro_Promotions.php create mode 100644 inc/Wizard_Promo.php diff --git a/assets/js/wizard-promo.js b/assets/js/wizard-promo.js new file mode 100644 index 00000000..b061eada --- /dev/null +++ b/assets/js/wizard-promo.js @@ -0,0 +1,320 @@ +/** + * Raft Pro upsell injection for the Otter onboarding wizard. + * + * Two surfaces: + * - Sidebar nudge: appended to `.o-sidebar__content` on every step. + * - Finish card: appended to `.o-finish__container` (before its actions) + * when the wizard reaches its done state. + * + * We don't import from Otter — we treat its rendered DOM + Redux store as + * the only interfaces, so changes to Otter's internals can't break us + * unless they rename the BEM classes we target. The script is loaded only + * when `?onboarding=true` is present AND Raft Pro is inactive, so paying + * users never see either nudge. + * + * Re-injection is necessary because React owns the wizard DOM and can + * remount sections on state changes — using marker IDs to avoid duplicates. + * + * @package + */ +( function () { + 'use strict'; + + if ( + 'undefined' === typeof window.wp || + ! window.wp.data || + ! window.raftWizardPromo + ) { + return; + } + + const data = window.wp.data; + const domReady = window.wp.domReady; + const config = window.raftWizardPromo; + const strings = config.strings || {}; + const upgradeUrl = config.upgradeUrl; + + const SIDEBAR_ID = 'raft-pro-wizard-sidebar-nudge'; + const FINISH_ID = 'raft-pro-wizard-finish-card'; + + /** + * Inject inline CSS for the two surfaces. Scoped via the marker IDs so + * we don't leak styles into Otter's own components. + */ + function injectStyles() { + if ( document.getElementById( 'raft-pro-wizard-promo-style' ) ) { + return; + } + const style = document.createElement( 'style' ); + style.id = 'raft-pro-wizard-promo-style'; + style.textContent = + '' + + '#' + + SIDEBAR_ID + + ' { margin-top: 24px; padding: 16px; border: 1px dashed #C26148; border-radius: 8px; background: rgba(194, 97, 72, 0.06); }' + + '#' + + SIDEBAR_ID + + ' .raft-eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; color: #C26148; margin: 0 0 4px; }' + + '#' + + SIDEBAR_ID + + ' .raft-title { font-size: 15px; font-weight: 600; margin: 0 0 8px; color: #1e1e1e; }' + + '#' + + SIDEBAR_ID + + ' a { color: #C26148; font-weight: 600; text-decoration: none; font-size: 13px; }' + + '#' + + SIDEBAR_ID + + ' a:hover { text-decoration: underline; }' + + '#' + + FINISH_ID + + ' { margin: 24px 0; padding: 28px; border: 2px dashed #C26148; border-radius: 12px; background: rgba(194, 97, 72, 0.05); text-align: left; }' + + '#' + + FINISH_ID + + ' .raft-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: #C26148; margin: 0 0 8px; }' + + '#' + + FINISH_ID + + ' h3 { font-size: 22px; font-weight: 600; margin: 0 0 12px; color: #1e1e1e; }' + + '#' + + FINISH_ID + + ' p { font-size: 15px; line-height: 1.5; margin: 0 0 20px; color: #50575e; }' + + '#' + + FINISH_ID + + ' a.raft-cta { display: inline-block; background: #C26148; color: #fff; padding: 10px 22px; border-radius: 999px; text-decoration: none; font-weight: 600; }' + + '#' + + FINISH_ID + + ' a.raft-cta:hover { background: #AC5039; color: #fff; }'; + document.head.appendChild( style ); + } + + /** + * Build a DOM node with the given tag, attributes, and child content. + * Tiny helper to avoid the verbosity of native createElement chains. + * + * @param {string} tag + * @param {Object} attrs + * @param {string|Node|Array} children + * @return {HTMLElement} HTML element + */ + function el( tag, attrs, children ) { + const node = document.createElement( tag ); + if ( attrs ) { + Object.keys( attrs ).forEach( function ( key ) { + if ( 'className' === key ) { + node.className = attrs[ key ]; + } else { + node.setAttribute( key, attrs[ key ] ); + } + } ); + } + if ( children ) { + if ( ! Array.isArray( children ) ) { + children = [ children ]; + } + children.forEach( function ( child ) { + if ( 'string' === typeof child ) { + node.appendChild( document.createTextNode( child ) ); + } else if ( child ) { + node.appendChild( child ); + } + } ); + } + return node; + } + + /** + * Build the sidebar nudge element. Separated from `injectSidebarNudge` + * so we can build-then-position without rebuilding on every re-render. + * + * @return {HTMLElement} HTML structure for the sidebar nudge + */ + function buildSidebarNudge() { + return el( 'div', { id: SIDEBAR_ID }, [ + el( + 'p', + { className: 'raft-eyebrow' }, + strings.sidebarEyebrow || 'Want more?' + ), + el( + 'p', + { className: 'raft-title' }, + strings.sidebarTitle || 'Try Raft Pro' + ), + el( + 'a', + { + href: upgradeUrl, + target: '_blank', + rel: 'noopener noreferrer', + }, + ( strings.sidebarLink || "See what's included" ) + ' →' + ), + ] ); + } + + /** + * Make sure the nudge exists AND is the last child of the sidebar + * content area. React owns this subtree and re-mounts step-specific + * controls between wizard steps — if our node ends up sandwiched + * between `.o-sidebar__info` and the freshly-mounted controls, the + * nudge appears in the middle of the sidebar instead of at the bottom. + * + * Idempotent: if the nudge is already last child, do nothing — so + * MutationObserver loops don't re-trigger themselves. + */ + function injectSidebarNudge() { + const container = document.querySelector( '.o-sidebar__content' ); + if ( ! container ) { + return; + } + + const existing = document.getElementById( SIDEBAR_ID ); + + if ( existing && existing === container.lastElementChild ) { + return; + } + + if ( existing && existing.parentNode ) { + existing.parentNode.removeChild( existing ); + } + + container.appendChild( buildSidebarNudge() ); + } + + /** + * Inject the larger Finish-step card before its action buttons. Returns + * early if the Finish container isn't in the DOM (we're not on Finish + * step yet) or if we already injected. + */ + function injectFinishCard() { + if ( document.getElementById( FINISH_ID ) ) { + return; + } + const container = document.querySelector( '.o-finish__container' ); + if ( ! container ) { + return; + } + + const card = el( 'div', { id: FINISH_ID }, [ + el( + 'p', + { className: 'raft-eyebrow' }, + strings.finishEyebrow || 'Get even more' + ), + el( + 'h3', + null, + strings.finishTitle || 'Take it further with Raft Pro' + ), + el( + 'p', + null, + strings.finishBody || + 'Unlock 17 extra patterns, 8 style variations, 7 ready-made page templates, and a fully designed WooCommerce storefront.' + ), + el( + 'a', + { + href: upgradeUrl, + target: '_blank', + rel: 'noopener noreferrer', + className: 'raft-cta', + }, + strings.finishCta || 'Upgrade to Pro' + ), + ] ); + + // Insert before the actions row so it sits naturally above the + // CTAs rather than after them (lower visual hierarchy). + const actions = container.querySelector( '.o-finish__actions' ); + if ( actions ) { + container.insertBefore( card, actions ); + } else { + container.appendChild( card ); + } + } + + let rafPending = false; + + function tick() { + injectStyles(); + injectSidebarNudge(); + injectFinishCard(); + } + + /** + * Debounce tick() to the next animation frame. wp.data.subscribe fires + * synchronously DURING React's render phase, before commit — running + * tick() at that point can land our nodes between info and a + * not-yet-mounted Controls child. Deferring to rAF runs us after the + * commit phase, when the DOM matches React's intent. + */ + function scheduleTick() { + if ( rafPending ) { + return; + } + rafPending = true; + ( + window.requestAnimationFrame || + function ( cb ) { + return setTimeout( cb, 16 ); + } + )( function () { + rafPending = false; + tick(); + } ); + } + + let observer = null; + + /** + * Watch the wizard root for child changes. Fires AFTER React's commit + * phase, so the DOM is settled when we re-inject. Idempotent: our + * inject functions short-circuit when the nudge is already in the + * right place, so the observer doesn't loop on its own mutations. + */ + function setupObserver() { + if ( observer ) { + return true; + } + const root = + document.getElementById( 'otter-onboarding' ) || document.body; + if ( ! root ) { + return false; + } + observer = new MutationObserver( scheduleTick ); + observer.observe( root, { childList: true, subtree: true } ); + return true; + } + + function start() { + // React commit happens after subscribe fires — defer to next frame. + try { + data.subscribe( scheduleTick ); + } catch ( e ) { + // Subscribe can throw before store registers; the poll below + // covers that window. + } + + setupObserver(); + + // Initial poll covers the brief window before Otter mounts the + // wizard root (the observer needs an existing node to attach to). + let attempts = 0; + const poll = setInterval( function () { + attempts++; + if ( setupObserver() ) { + scheduleTick(); + } + if ( attempts > 30 ) { + clearInterval( poll ); + } + }, 200 ); + } + + if ( domReady ) { + domReady( start ); + } else if ( 'loading' === document.readyState ) { + document.addEventListener( 'DOMContentLoaded', start ); + } else { + start(); + } +} )(); diff --git a/inc/Admin.php b/inc/Admin.php index cff945f4..bf751cb1 100644 --- a/inc/Admin.php +++ b/inc/Admin.php @@ -180,7 +180,7 @@ public function render_welcome_notice() { $notice_html .= ''; - $learn_more = '' . __( 'Learn More', 'raft' ) . ''; + $learn_more = '' . __( 'Learn More', 'raft' ) . ''; $notice_html .= '

' . __( 'Install our free builder plugin for more blocks, enhanced functionality, and seamless theme setup.', 'raft' ) . ' ' . $learn_more . '

'; diff --git a/inc/Core.php b/inc/Core.php index 74a4962a..38f583f8 100644 --- a/inc/Core.php +++ b/inc/Core.php @@ -44,6 +44,9 @@ public function __construct() { new Admin(); new Block_Patterns(); new Block_Styles(); + new Dashboard(); + new Pro_Promotions(); + new Wizard_Promo(); } /** diff --git a/inc/Dashboard.php b/inc/Dashboard.php new file mode 100644 index 00000000..c9ef414e --- /dev/null +++ b/inc/Dashboard.php @@ -0,0 +1,154 @@ + +
+

+

+ + +
+
+

+

+ + + +
+
+ +
+
+ +8 +

+

+
+
+ +17 +

+

+
+
+ +7 +

+

+
+
+ +8 +

+

+
+
+ +
+
+

+

+
+
+ + + +
+ is_pro_active() ) { + return; + } + + register_block_pattern_category( + self::CATEGORY_SLUG, + array( 'label' => __( 'Raft Pro', 'raft' ) ) + ); + + foreach ( $this->get_pro_patterns() as $slug => $config ) { + register_block_pattern( + 'raft/pro-' . $slug, + array( + 'title' => $config['title'], + 'description' => $config['description'], + 'categories' => array( self::CATEGORY_SLUG ), + 'content' => $this->build_upgrade_card( $config['title'], $config['description'] ), + ) + ); + } + } + + /** + * Whether Raft Pro is active. + * + * Hides upsells from users who've already upgraded — they don't want + * placeholder cards cluttering their inserter next to the real ones. + * + * @return bool + */ + private function is_pro_active(): bool { + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + return is_plugin_active( 'raft-pro/raft-pro.php' ); + } + + /** + * The Pro patterns advertised in the inserter. + * + * Curated subset of Raft Pro's catalogue — the most visually-distinctive + * ones, picked for discovery impact. Full Pro list lives in + * `raft-pro/inc/Block_Patterns.php`. + * + * @return array + */ + private function get_pro_patterns(): array { + return apply_filters( + 'raft_pro_promoted_patterns', + array( + 'bento-features' => array( + 'title' => __( 'Bento Features (Pro)', 'raft' ), + 'description' => __( 'Grid-style feature showcase with mixed tile sizes — Bento layout.', 'raft' ), + ), + 'marquee-hero' => array( + 'title' => __( 'Marquee Hero (Pro)', 'raft' ), + 'description' => __( 'Full-bleed hero with animated marquee text and bold typography.', 'raft' ), + ), + 'stats-counter' => array( + 'title' => __( 'Stats Counter (Pro)', 'raft' ), + 'description' => __( 'Three-column metric callouts with large numbers and short labels.', 'raft' ), + ), + 'pricing-comparison' => array( + 'title' => __( 'Pricing Comparison (Pro)', 'raft' ), + 'description' => __( 'Side-by-side pricing tiers with featured plan highlight.', 'raft' ), + ), + 'team-spotlight' => array( + 'title' => __( 'Team Spotlight (Pro)', 'raft' ), + 'description' => __( 'Team grid with circular avatars and bio cards.', 'raft' ), + ), + 'gradient-cta' => array( + 'title' => __( 'Gradient CTA (Pro)', 'raft' ), + 'description' => __( 'Full-width call-to-action with gradient background and centered copy.', 'raft' ), + ), + ) + ); + } + + /** + * The "locked" upgrade card block markup inserted in place of the real + * pattern. Reuses Raft's color/spacing presets so it sits naturally on + * any style variation. The user can delete it the moment they realize + * it's a placeholder — that's the point. + * + * @param string $pattern_title Pattern title shown in the card heading. + * @param string $description Short description shown in the card body. + * + * @return string + */ + private function build_upgrade_card( string $pattern_title, string $description ): string { + $eyebrow = esc_html__( 'Locked — Pro pattern', 'raft' ); + $heading = esc_html( $pattern_title ); + $body = esc_html( $description ); + $cta = esc_html__( 'Upgrade to Raft Pro', 'raft' ); + $upgrade_url = esc_url( self::UPGRADE_URL ); + + return << +
+ +

{$eyebrow}

+ + +

{$heading}

+ + +

{$body}

+ + +
+ + + +
+ +
+ +HTML; + } +} diff --git a/inc/Wizard_Promo.php b/inc/Wizard_Promo.php new file mode 100644 index 00000000..4b720af1 --- /dev/null +++ b/inc/Wizard_Promo.php @@ -0,0 +1,102 @@ +is_pro_active() ) { + return; + } + + wp_enqueue_script( + self::SCRIPT_HANDLE, + RAFT_URL . 'assets/js/wizard-promo.js', + array( 'wp-data', 'wp-element', 'wp-dom-ready' ), + RAFT_VERSION, + true + ); + + wp_localize_script( + self::SCRIPT_HANDLE, + 'raftWizardPromo', + array( + 'upgradeUrl' => self::UPGRADE_URL, + 'strings' => array( + 'sidebarEyebrow' => __( 'Want more?', 'raft' ), + 'sidebarTitle' => __( 'Try Raft Pro', 'raft' ), + 'sidebarLink' => __( 'See what\'s included', 'raft' ), + 'finishEyebrow' => __( 'Get even more', 'raft' ), + 'finishTitle' => __( 'Take it further with Raft Pro', 'raft' ), + 'finishBody' => __( 'Unlock 17 extra patterns, 8 style variations, 7 ready-made page templates, and a fully designed WooCommerce storefront.', 'raft' ), + 'finishCta' => __( 'Upgrade to Pro', 'raft' ), + ), + ) + ); + } + + /** + * Whether Raft Pro is active. Standard plugin-active check, with the + * plugin.php include guard for early hooks. + * + * @return bool + */ + private function is_pro_active(): bool { + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + return is_plugin_active( 'raft-pro/raft-pro.php' ); + } +} From 6611e4e8f1d1ad63ca6bf27b082a0e98d937d538 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 3 Jun 2026 02:44:52 +0530 Subject: [PATCH 3/5] [wip] --- inc/Core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/Core.php b/inc/Core.php index 38f583f8..7aaff693 100644 --- a/inc/Core.php +++ b/inc/Core.php @@ -136,7 +136,7 @@ public function setup() { 'title' => __( 'Archive Cards', 'raft' ), ), 'archive-row' => array( - 'file' => RAFT_DIR . 'library/archive/archive-row.php', + 'file' => RAFT_DIR . 'library/archive/archive-rows.php', 'title' => __( 'Archive Row', 'raft' ), ), ), From ed1635b3fbb064a8e27182560a42ff63581a92d4 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 3 Jun 2026 04:14:09 +0530 Subject: [PATCH 4/5] [wip] --- inc/Dashboard.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inc/Dashboard.php b/inc/Dashboard.php index c9ef414e..bc6bfa3d 100644 --- a/inc/Dashboard.php +++ b/inc/Dashboard.php @@ -136,6 +136,14 @@ public function render() {

+
From 425601bdfbf2a973bbfef494011dcdc69a1d5db2 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Wed, 3 Jun 2026 04:48:07 +0530 Subject: [PATCH 5/5] [wip] --- .github/workflows/test-php.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index a95f7d2e..913a00fc 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -13,7 +13,7 @@ jobs: - name: Setup PHP version uses: shivammathur/setup-php@v2 with: - php-version: "7.2" + php-version: "7.4" extensions: simplexml - name: Checkout source code uses: actions/checkout@v4 @@ -36,7 +36,7 @@ jobs: - name: Setup PHP version uses: shivammathur/setup-php@v2 with: - php-version: "7.2" + php-version: "7.4" extensions: simplexml, mysql tools: phpunit-polyfills - name: Checkout source code