Plugin Directory

Changeset 3754062


Ignore:
Timestamp:
10/19/2026 05:50:19 AM (3 weeks ago)
Author:
spexoid
Message:

0.1.3: savings percentage on the bundle box, Upgrade to Pro link, editor support for Pro's percent-or-amount tiers

Location:
nanflows-bundle
Files:
14 edited
1 copied

Legend:

Unmodified
Added
Removed
  • nanflows-bundle/tags/0.1.3/assets/admin/app.css

    r3737861 r3754062  
    12861286}
    12871287
     1288/* Volume and Quantity Break (Pro): "Add 1 more and get 10% off". */
     1289.nfb-live__nudge {
     1290        margin: 0 0 10px;
     1291        padding: 6px 10px;
     1292        border-radius: 6px;
     1293        background: #edfaef;
     1294        color: #00631a;
     1295        font-weight: 600;
     1296}
     1297
    12881298.nfb-live__save {
    12891299        display: inline-flex;
  • nanflows-bundle/tags/0.1.3/assets/admin/app.js

    r3737861 r3754062  
    447447                                                                'div',
    448448                                                                { key: i, className: 'nfb-options__row' },
    449                                                                 ( field.columns || [] ).map( ( col ) =>
    450                                                                         h( TextControl, {
     449                                                                ( field.columns || [] ).map( function ( col ) {
     450                                                                        const cell = function ( key ) {
     451                                                                                if ( row[ key ] !== undefined && row[ key ] !== '' ) {
     452                                                                                        return row[ key ];
     453                                                                                }
     454                                                                                const other = ( field.columns || [] ).find( ( c ) => c.key === key );
     455                                                                                return other && other.default !== undefined ? other.default : '';
     456                                                                        };
     457                                                                        // show_if: [ key, value ]: only while this row's other column has that value.
     458                                                                        if ( col.show_if && String( cell( col.show_if[ 0 ] ) ) !== String( col.show_if[ 1 ] ) ) {
     459                                                                                return null;
     460                                                                        }
     461                                                                        const set = ( v ) => change( field.key, rows.map( ( r, j ) => ( j === i ? Object.assign( {}, r, { [ col.key ]: col.type === 'number' ? ( v === '' ? '' : Number( v ) ) : v } ) : r ) ) );
     462                                                                        if ( col.type === 'select' ) {
     463                                                                                return h( SelectControl, { key: col.key, label: col.label, value: String( cell( col.key ) ), options: ( col.options || [] ).map( ( o ) => ( { value: o[ 0 ], label: o[ 1 ] } ) ), onChange: set, __nextHasNoMarginBottom: true } );
     464                                                                        }
     465                                                                        return h( TextControl, {
    451466                                                                                key: col.key,
    452467                                                                                label: col.label,
    … …  
    454469                                                                                min: col.min,
    455470                                                                                max: col.max,
     471                                                                                step: col.step,
    456472                                                                                value: row[ col.key ] === undefined ? '' : String( row[ col.key ] ),
    457                                                                                 onChange: ( v ) => change( field.key, rows.map( ( r, j ) => ( j === i ? Object.assign( {}, r, { [ col.key ]: col.type === 'number' ? ( v === '' ? '' : Number( v ) ) : v } ) : r ) ) ),
     473                                                                                onChange: set,
    458474                                                                                __nextHasNoMarginBottom: true,
    459                                                                         } )
    460                                                                 ),
     475                                                                        } );
     476                                                                } ),
    461477                                                                h( Button, { icon: 'no-alt', label: __( 'Remove', 'nanflows-bundle' ), isDestructive: true, onClick: () => change( field.key, rows.filter( ( r, j ) => j !== i ) ) } )
    462478                                                        )
    … …  
    24792495        }
    24802496
     2497        /**
     2498         * Volume and Quantity Break (Pro): the "Add 1 more and get 10% off" line,
     2499         * for the smallest quantity the preview shows. Empty when switched off.
     2500         */
     2501        function tierNudge( d ) {
     2502                const o = d.options || {};
     2503                if ( ( d.type !== 'volume' && d.type !== 'quantity_break' ) || o.nudge === false ) {
     2504                        return '';
     2505                }
     2506                const tiers = ( o.tiers || [] ).filter( ( t ) => Number( t.min ) > 0 ).slice().sort( ( a, b ) => a.min - b.min );
     2507                if ( ! tiers.length ) {
     2508                        return '';
     2509                }
     2510                const units = d.type === 'quantity_break' ? Number( tiers[ 0 ].min ) : Math.max( 1, Number( o.min_items ) || 1 );
     2511                const next = tiers.find( ( t ) => Number( t.min ) > units );
     2512                const shown = next || tiers[ tiers.length - 1 ];
     2513                const off = shown.off === 'amount' ? money( toMinor( shown.amount ) ) : Number( shown.percent || 0 ) + '%';
     2514                if ( ! next ) {
     2515                        /* translators: %s: discount, e.g. "20%" or "$10.00" */
     2516                        return sprintf( __( 'You are getting the best discount: %s off', 'nanflows-bundle' ), off );
     2517                }
     2518                return d.type === 'quantity_break'
     2519                        ? /* translators: 1: number of items, 2: discount, e.g. "10%" or "$5.00" */ sprintf( __( 'Buy %1$d and get %2$s off', 'nanflows-bundle' ), Number( next.min ), off )
     2520                        : /* translators: 1: number of items, 2: discount, e.g. "10%" or "$5.00" */ sprintf( __( 'Add %1$d more and get %2$s off', 'nanflows-bundle' ), Number( next.min ) - units, off );
     2521        }
     2522
    24812523        /** The tiers of a quantity break, as the shopper picks them. */
    24822524        function QuantityTiers( props ) {
    … …  
    24922534                        tiers.map( function ( tier, i ) {
    24932535                                const regular = product ? toMinor( product.price ) * tier.min : 0;
    2494                                 const price = Math.round( regular * ( 1 - Number( tier.percent || 0 ) / 100 ) );
     2536                                const flat = tier.off === 'amount';
     2537                                const price = flat ? Math.max( 0, regular - toMinor( tier.amount ) ) : Math.round( regular * ( 1 - Number( tier.percent || 0 ) / 100 ) );
    24952538                                return h(
    24962539                                        'li',
    24972540                                        { key: i, className: i === 0 ? 'is-picked' : '' },
    24982541                                        h( 'input', { type: 'radio', checked: i === 0, readOnly: true, tabIndex: -1, 'aria-hidden': true } ),
    2499                                         h( 'span', { className: 'nfb-live__name' }, sprintf( /* translators: %d: quantity */ __( 'Buy %d', 'nanflows-bundle' ), tier.min ), h( 'small', { className: 'nfb-live__badge' }, sprintf( /* translators: %s: percentage */ __( 'Save %s%%', 'nanflows-bundle' ), tier.percent ) ) ),
     2542                                        h( 'span', { className: 'nfb-live__name' }, sprintf( /* translators: %d: quantity */ __( 'Buy %d', 'nanflows-bundle' ), tier.min ), h( 'small', { className: 'nfb-live__badge' }, flat ? sprintf( /* translators: %s: amount */ __( 'Save %s', 'nanflows-bundle' ), money( toMinor( tier.amount ) ) ) : sprintf( /* translators: %s: percentage */ __( 'Save %s%%', 'nanflows-bundle' ), tier.percent ) ) ),
    25002543                                        h( 'span', null, product ? h( Fragment, null, h( 'del', null, money( regular ) ), ' ', h( 'strong', null, money( price ) ) ) : '—' )
    25012544                                );
    … …  
    25952638                                        h( 'dd', null, waiting ? h( 'span', { className: 'nfb-summary__wait' }, __( 'set in step 2', 'nanflows-bundle' ) ) : h( 'strong', { className: 'nfb-live__price' }, money( b.price ) ) )
    25962639                                ),
    2597                         ! waiting && b.savings > 0 && rows.length > 0 && d.type !== 'quantity_break' && h( 'p', { className: 'nfb-live__save' }, h( Icon, { name: 'tag' } ), ' ', sprintf( /* translators: %s: amount */ __( 'You save %s', 'nanflows-bundle' ), money( b.savings ) ) ),
     2640                        rows.length > 0 && tierNudge( d ) && h( 'p', { className: 'nfb-live__nudge' }, tierNudge( d ) ),
     2641                        ! waiting && b.savings > 0 && rows.length > 0 && d.type !== 'quantity_break' && h( 'p', { className: 'nfb-live__save' }, h( Icon, { name: 'tag' } ), ' ', sprintf( /* translators: %s: amount */ __( 'You save %s', 'nanflows-bundle' ), money( b.savings ) ), b.regular > 0 && Math.round( ( b.savings / b.regular ) * 100 ) >= 1 ? ' (' + Math.round( ( b.savings / b.regular ) * 100 ) + '%)' : '' ),
    25982642                        b.note && rows.length > 0 && h( 'p', { className: 'nfb-live__note' }, b.note ),
    25992643                        ! props.embedded && h( 'div', { className: 'nfb-preview__buy' }, h( 'span', { className: 'nfb-qty' }, '− 1 +' ), h( 'span', { className: 'button button-primary' }, config.buttonText || __( 'Add Bundle to Cart', 'nanflows-bundle' ) ) ),
  • nanflows-bundle/tags/0.1.3/assets/frontend/bundle.js

    r3736789 r3754062  
    1414        }
    1515
     16        /** The "(25%)" after the amount saved: rounded, hidden under 1%. */
     17        function percentOff( card, b ) {
     18                const box = card.querySelector( '.nfb-bundle__savings-percent' );
     19                const number = box && box.querySelector( '.nfb-bundle__savings-pct' );
     20                if ( ! number ) {
     21                        return;
     22                }
     23                const percent = b.regular > 0 ? Math.round( ( b.savings / b.regular ) * 100 ) : 0;
     24                number.textContent = String( percent );
     25                box.hidden = percent < 1;
     26        }
     27
    1628        function update( card ) {
    1729                let currency;
    … …  
    5264                set( '.nfb-bundle__price strong', b.price );
    5365                set( card.querySelector( '.nfb-bundle__savings-amount' ) ? '.nfb-bundle__savings-amount' : '.nfb-bundle__savings', b.savings );
     66                percentOff( card, b );
    5467
    5568                // "Frequently bought together" with every extra unticked: the main
  • nanflows-bundle/tags/0.1.3/includes/Admin/Admin.php

    r3736789 r3754062  
    7272        /**
    7373         * "Bundles" in the plugin's row on the Plugins screen, before
    74          * Deactivate, like other plugins' Settings links.
     74         * Deactivate, like other plugins' Settings links. Without Pro installed,
     75         * "Upgrade to Pro" comes first and opens the Pro page on nanflows.com.
    7576         *
    7677         * @param array<string, string> $links
    … …  
    8182                        return $links;
    8283                }
    83                 return array_merge(
    84                         array(
    85                                 'bundles' => '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Bundles', 'nanflows-bundle' ) . '</a>',
    86                         ),
    87                         (array) $links
    88                 );
     84                $own = array();
     85                if ( ! defined( 'NANFLOWS_BUNDLE_PRO_VERSION' ) ) {
     86                        $own['upgrade'] = '<a href="' . esc_url( self::UPGRADE_URL ) . '" target="_blank" rel="noopener" style="color:#1a9f3a;font-weight:600">' . esc_html__( 'Upgrade to Pro', 'nanflows-bundle' ) . '</a>';
     87                }
     88                $own['bundles'] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Bundles', 'nanflows-bundle' ) . '</a>';
     89                return array_merge( $own, (array) $links );
    8990        }
    9091
  • nanflows-bundle/tags/0.1.3/includes/Frontend/BundleCard.php

    r3736789 r3754062  
    215215                                        <?php if ( $settings['show_savings'] && $savings > 0 ) : ?>
    216216                                                <dt><?php esc_html_e( 'You save', 'nanflows-bundle' ); ?></dt>
    217                                                 <dd class="nfb-bundle__savings"><?php echo Icons::get( 'tag' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup, escaped inside. ?><span class="nfb-bundle__savings-amount"><?php echo wp_kses_post( wc_price( $savings ) ); ?></span></dd>
     217                                                <dd class="nfb-bundle__savings"><?php echo Icons::get( 'tag' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup, escaped inside. ?><span class="nfb-bundle__savings-amount"><?php echo wp_kses_post( wc_price( $savings ) ); ?></span> <?php echo self::percent_off( $regular, $savings ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- a number in fixed markup. ?></dd>
    218218                                        <?php endif; ?>
    219219                                </dl>
    … …  
    281281                );
    282282        }
     283
     284        /**
     285         * "(25%)" after the amount saved, as the editor shows it. The script
     286         * keeps the number current and hides it under 1%.
     287         */
     288        public static function percent_off( float $regular, float $savings ): string {
     289                $percent = $regular > 0 ? (int) round( $savings / $regular * 100 ) : 0;
     290                return '<span class="nfb-bundle__savings-percent"' . ( $percent >= 1 ? '' : ' hidden' ) . '>(<span class="nfb-bundle__savings-pct">' . $percent . '</span>%)</span>';
     291        }
    283292}
  • nanflows-bundle/tags/0.1.3/nanflows-bundle.php

    r3737861 r3754062  
    44 * Plugin URI:           https://nanflows.com/products/nanflows-bundle
    55 * Description:          Sell product bundles in WooCommerce: fixed bundles and "frequently bought together" offers, with stock taken from the products inside.
    6  * Version:              0.1.2
     6 * Version:              0.1.3
    77 * Requires at least:    6.4
    88 * Requires PHP:         7.4
    … …  
    2020defined( 'ABSPATH' ) || exit;
    2121
    22 define( 'NANFLOWS_BUNDLE_VERSION', '0.1.2' );
     22define( 'NANFLOWS_BUNDLE_VERSION', '0.1.3' );
    2323define( 'NANFLOWS_BUNDLE_FILE', __FILE__ );
    2424define( 'NANFLOWS_BUNDLE_DIR', plugin_dir_path( __FILE__ ) );
  • nanflows-bundle/tags/0.1.3/readme.txt

    r3737861 r3754062  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.1.2
     7Stable tag: 0.1.3
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    171171== Changelog ==
    172172
     173= 0.1.3 =
     174* The bundle box shows the saving as a percentage too, e.g. "You save $28.25 (25%)".
     175* "Upgrade to Pro" link on the Plugins screen.
     176* With Bundles Pro: volume and quantity tiers can take a percentage or a flat amount off, and the bundle box says how to reach the next discount ("Add 1 more and get 10% off").
     177
    173178= 0.1.2 =
    174179* A bundle can have a page of its own, with a heading or a banner (computers and phones) at the top, in your theme's design.
  • nanflows-bundle/trunk/assets/admin/app.css

    r3737861 r3754062  
    12861286}
    12871287
     1288/* Volume and Quantity Break (Pro): "Add 1 more and get 10% off". */
     1289.nfb-live__nudge {
     1290        margin: 0 0 10px;
     1291        padding: 6px 10px;
     1292        border-radius: 6px;
     1293        background: #edfaef;
     1294        color: #00631a;
     1295        font-weight: 600;
     1296}
     1297
    12881298.nfb-live__save {
    12891299        display: inline-flex;
  • nanflows-bundle/trunk/assets/admin/app.js

    r3737861 r3754062  
    447447                                                                'div',
    448448                                                                { key: i, className: 'nfb-options__row' },
    449                                                                 ( field.columns || [] ).map( ( col ) =>
    450                                                                         h( TextControl, {
     449                                                                ( field.columns || [] ).map( function ( col ) {
     450                                                                        const cell = function ( key ) {
     451                                                                                if ( row[ key ] !== undefined && row[ key ] !== '' ) {
     452                                                                                        return row[ key ];
     453                                                                                }
     454                                                                                const other = ( field.columns || [] ).find( ( c ) => c.key === key );
     455                                                                                return other && other.default !== undefined ? other.default : '';
     456                                                                        };
     457                                                                        // show_if: [ key, value ]: only while this row's other column has that value.
     458                                                                        if ( col.show_if && String( cell( col.show_if[ 0 ] ) ) !== String( col.show_if[ 1 ] ) ) {
     459                                                                                return null;
     460                                                                        }
     461                                                                        const set = ( v ) => change( field.key, rows.map( ( r, j ) => ( j === i ? Object.assign( {}, r, { [ col.key ]: col.type === 'number' ? ( v === '' ? '' : Number( v ) ) : v } ) : r ) ) );
     462                                                                        if ( col.type === 'select' ) {
     463                                                                                return h( SelectControl, { key: col.key, label: col.label, value: String( cell( col.key ) ), options: ( col.options || [] ).map( ( o ) => ( { value: o[ 0 ], label: o[ 1 ] } ) ), onChange: set, __nextHasNoMarginBottom: true } );
     464                                                                        }
     465                                                                        return h( TextControl, {
    451466                                                                                key: col.key,
    452467                                                                                label: col.label,
    … …  
    454469                                                                                min: col.min,
    455470                                                                                max: col.max,
     471                                                                                step: col.step,
    456472                                                                                value: row[ col.key ] === undefined ? '' : String( row[ col.key ] ),
    457                                                                                 onChange: ( v ) => change( field.key, rows.map( ( r, j ) => ( j === i ? Object.assign( {}, r, { [ col.key ]: col.type === 'number' ? ( v === '' ? '' : Number( v ) ) : v } ) : r ) ) ),
     473                                                                                onChange: set,
    458474                                                                                __nextHasNoMarginBottom: true,
    459                                                                         } )
    460                                                                 ),
     475                                                                        } );
     476                                                                } ),
    461477                                                                h( Button, { icon: 'no-alt', label: __( 'Remove', 'nanflows-bundle' ), isDestructive: true, onClick: () => change( field.key, rows.filter( ( r, j ) => j !== i ) ) } )
    462478                                                        )
    … …  
    24792495        }
    24802496
     2497        /**
     2498         * Volume and Quantity Break (Pro): the "Add 1 more and get 10% off" line,
     2499         * for the smallest quantity the preview shows. Empty when switched off.
     2500         */
     2501        function tierNudge( d ) {
     2502                const o = d.options || {};
     2503                if ( ( d.type !== 'volume' && d.type !== 'quantity_break' ) || o.nudge === false ) {
     2504                        return '';
     2505                }
     2506                const tiers = ( o.tiers || [] ).filter( ( t ) => Number( t.min ) > 0 ).slice().sort( ( a, b ) => a.min - b.min );
     2507                if ( ! tiers.length ) {
     2508                        return '';
     2509                }
     2510                const units = d.type === 'quantity_break' ? Number( tiers[ 0 ].min ) : Math.max( 1, Number( o.min_items ) || 1 );
     2511                const next = tiers.find( ( t ) => Number( t.min ) > units );
     2512                const shown = next || tiers[ tiers.length - 1 ];
     2513                const off = shown.off === 'amount' ? money( toMinor( shown.amount ) ) : Number( shown.percent || 0 ) + '%';
     2514                if ( ! next ) {
     2515                        /* translators: %s: discount, e.g. "20%" or "$10.00" */
     2516                        return sprintf( __( 'You are getting the best discount: %s off', 'nanflows-bundle' ), off );
     2517                }
     2518                return d.type === 'quantity_break'
     2519                        ? /* translators: 1: number of items, 2: discount, e.g. "10%" or "$5.00" */ sprintf( __( 'Buy %1$d and get %2$s off', 'nanflows-bundle' ), Number( next.min ), off )
     2520                        : /* translators: 1: number of items, 2: discount, e.g. "10%" or "$5.00" */ sprintf( __( 'Add %1$d more and get %2$s off', 'nanflows-bundle' ), Number( next.min ) - units, off );
     2521        }
     2522
    24812523        /** The tiers of a quantity break, as the shopper picks them. */
    24822524        function QuantityTiers( props ) {
    … …  
    24922534                        tiers.map( function ( tier, i ) {
    24932535                                const regular = product ? toMinor( product.price ) * tier.min : 0;
    2494                                 const price = Math.round( regular * ( 1 - Number( tier.percent || 0 ) / 100 ) );
     2536                                const flat = tier.off === 'amount';
     2537                                const price = flat ? Math.max( 0, regular - toMinor( tier.amount ) ) : Math.round( regular * ( 1 - Number( tier.percent || 0 ) / 100 ) );
    24952538                                return h(
    24962539                                        'li',
    24972540                                        { key: i, className: i === 0 ? 'is-picked' : '' },
    24982541                                        h( 'input', { type: 'radio', checked: i === 0, readOnly: true, tabIndex: -1, 'aria-hidden': true } ),
    2499                                         h( 'span', { className: 'nfb-live__name' }, sprintf( /* translators: %d: quantity */ __( 'Buy %d', 'nanflows-bundle' ), tier.min ), h( 'small', { className: 'nfb-live__badge' }, sprintf( /* translators: %s: percentage */ __( 'Save %s%%', 'nanflows-bundle' ), tier.percent ) ) ),
     2542                                        h( 'span', { className: 'nfb-live__name' }, sprintf( /* translators: %d: quantity */ __( 'Buy %d', 'nanflows-bundle' ), tier.min ), h( 'small', { className: 'nfb-live__badge' }, flat ? sprintf( /* translators: %s: amount */ __( 'Save %s', 'nanflows-bundle' ), money( toMinor( tier.amount ) ) ) : sprintf( /* translators: %s: percentage */ __( 'Save %s%%', 'nanflows-bundle' ), tier.percent ) ) ),
    25002543                                        h( 'span', null, product ? h( Fragment, null, h( 'del', null, money( regular ) ), ' ', h( 'strong', null, money( price ) ) ) : '—' )
    25012544                                );
    … …  
    25952638                                        h( 'dd', null, waiting ? h( 'span', { className: 'nfb-summary__wait' }, __( 'set in step 2', 'nanflows-bundle' ) ) : h( 'strong', { className: 'nfb-live__price' }, money( b.price ) ) )
    25962639                                ),
    2597                         ! waiting && b.savings > 0 && rows.length > 0 && d.type !== 'quantity_break' && h( 'p', { className: 'nfb-live__save' }, h( Icon, { name: 'tag' } ), ' ', sprintf( /* translators: %s: amount */ __( 'You save %s', 'nanflows-bundle' ), money( b.savings ) ) ),
     2640                        rows.length > 0 && tierNudge( d ) && h( 'p', { className: 'nfb-live__nudge' }, tierNudge( d ) ),
     2641                        ! waiting && b.savings > 0 && rows.length > 0 && d.type !== 'quantity_break' && h( 'p', { className: 'nfb-live__save' }, h( Icon, { name: 'tag' } ), ' ', sprintf( /* translators: %s: amount */ __( 'You save %s', 'nanflows-bundle' ), money( b.savings ) ), b.regular > 0 && Math.round( ( b.savings / b.regular ) * 100 ) >= 1 ? ' (' + Math.round( ( b.savings / b.regular ) * 100 ) + '%)' : '' ),
    25982642                        b.note && rows.length > 0 && h( 'p', { className: 'nfb-live__note' }, b.note ),
    25992643                        ! props.embedded && h( 'div', { className: 'nfb-preview__buy' }, h( 'span', { className: 'nfb-qty' }, '− 1 +' ), h( 'span', { className: 'button button-primary' }, config.buttonText || __( 'Add Bundle to Cart', 'nanflows-bundle' ) ) ),
  • nanflows-bundle/trunk/assets/frontend/bundle.js

    r3736789 r3754062  
    1414        }
    1515
     16        /** The "(25%)" after the amount saved: rounded, hidden under 1%. */
     17        function percentOff( card, b ) {
     18                const box = card.querySelector( '.nfb-bundle__savings-percent' );
     19                const number = box && box.querySelector( '.nfb-bundle__savings-pct' );
     20                if ( ! number ) {
     21                        return;
     22                }
     23                const percent = b.regular > 0 ? Math.round( ( b.savings / b.regular ) * 100 ) : 0;
     24                number.textContent = String( percent );
     25                box.hidden = percent < 1;
     26        }
     27
    1628        function update( card ) {
    1729                let currency;
    … …  
    5264                set( '.nfb-bundle__price strong', b.price );
    5365                set( card.querySelector( '.nfb-bundle__savings-amount' ) ? '.nfb-bundle__savings-amount' : '.nfb-bundle__savings', b.savings );
     66                percentOff( card, b );
    5467
    5568                // "Frequently bought together" with every extra unticked: the main
  • nanflows-bundle/trunk/includes/Admin/Admin.php

    r3736789 r3754062  
    7272        /**
    7373         * "Bundles" in the plugin's row on the Plugins screen, before
    74          * Deactivate, like other plugins' Settings links.
     74         * Deactivate, like other plugins' Settings links. Without Pro installed,
     75         * "Upgrade to Pro" comes first and opens the Pro page on nanflows.com.
    7576         *
    7677         * @param array<string, string> $links
    … …  
    8182                        return $links;
    8283                }
    83                 return array_merge(
    84                         array(
    85                                 'bundles' => '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Bundles', 'nanflows-bundle' ) . '</a>',
    86                         ),
    87                         (array) $links
    88                 );
     84                $own = array();
     85                if ( ! defined( 'NANFLOWS_BUNDLE_PRO_VERSION' ) ) {
     86                        $own['upgrade'] = '<a href="' . esc_url( self::UPGRADE_URL ) . '" target="_blank" rel="noopener" style="color:#1a9f3a;font-weight:600">' . esc_html__( 'Upgrade to Pro', 'nanflows-bundle' ) . '</a>';
     87                }
     88                $own['bundles'] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE ) ) . '">' . esc_html__( 'Bundles', 'nanflows-bundle' ) . '</a>';
     89                return array_merge( $own, (array) $links );
    8990        }
    9091
  • nanflows-bundle/trunk/includes/Frontend/BundleCard.php

    r3736789 r3754062  
    215215                                        <?php if ( $settings['show_savings'] && $savings > 0 ) : ?>
    216216                                                <dt><?php esc_html_e( 'You save', 'nanflows-bundle' ); ?></dt>
    217                                                 <dd class="nfb-bundle__savings"><?php echo Icons::get( 'tag' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup, escaped inside. ?><span class="nfb-bundle__savings-amount"><?php echo wp_kses_post( wc_price( $savings ) ); ?></span></dd>
     217                                                <dd class="nfb-bundle__savings"><?php echo Icons::get( 'tag' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed markup, escaped inside. ?><span class="nfb-bundle__savings-amount"><?php echo wp_kses_post( wc_price( $savings ) ); ?></span> <?php echo self::percent_off( $regular, $savings ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- a number in fixed markup. ?></dd>
    218218                                        <?php endif; ?>
    219219                                </dl>
    … …  
    281281                );
    282282        }
     283
     284        /**
     285         * "(25%)" after the amount saved, as the editor shows it. The script
     286         * keeps the number current and hides it under 1%.
     287         */
     288        public static function percent_off( float $regular, float $savings ): string {
     289                $percent = $regular > 0 ? (int) round( $savings / $regular * 100 ) : 0;
     290                return '<span class="nfb-bundle__savings-percent"' . ( $percent >= 1 ? '' : ' hidden' ) . '>(<span class="nfb-bundle__savings-pct">' . $percent . '</span>%)</span>';
     291        }
    283292}
  • nanflows-bundle/trunk/nanflows-bundle.php

    r3737861 r3754062  
    44 * Plugin URI:           https://nanflows.com/products/nanflows-bundle
    55 * Description:          Sell product bundles in WooCommerce: fixed bundles and "frequently bought together" offers, with stock taken from the products inside.
    6  * Version:              0.1.2
     6 * Version:              0.1.3
    77 * Requires at least:    6.4
    88 * Requires PHP:         7.4
    … …  
    2020defined( 'ABSPATH' ) || exit;
    2121
    22 define( 'NANFLOWS_BUNDLE_VERSION', '0.1.2' );
     22define( 'NANFLOWS_BUNDLE_VERSION', '0.1.3' );
    2323define( 'NANFLOWS_BUNDLE_FILE', __FILE__ );
    2424define( 'NANFLOWS_BUNDLE_DIR', plugin_dir_path( __FILE__ ) );
  • nanflows-bundle/trunk/readme.txt

    r3737861 r3754062  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.1.2
     7Stable tag: 0.1.3
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    171171== Changelog ==
    172172
     173= 0.1.3 =
     174* The bundle box shows the saving as a percentage too, e.g. "You save $28.25 (25%)".
     175* "Upgrade to Pro" link on the Plugins screen.
     176* With Bundles Pro: volume and quantity tiers can take a percentage or a flat amount off, and the bundle box says how to reach the next discount ("Add 1 more and get 10% off").
     177
    173178= 0.1.2 =
    174179* A bundle can have a page of its own, with a heading or a banner (computers and phones) at the top, in your theme's design.
Note: See TracChangeset for help on using the changeset viewer.