Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocks: Show Blocks if they're missing a plan, add Upgrade Nudge #12823

Merged
merged 37 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d81f1b9
Simple Payments: Report Minimum Plan Required in Availability Endpoint
ockham Jun 23, 2019
99976e9
Blocks: Show Blocks if they're missing a plan
ockham Jun 23, 2019
a5ed423
Steal files from @Automattic/wp-calypso#33723
ockham Jun 24, 2019
cf5587d
So functional, much wow
ockham Jun 24, 2019
ae97d61
Remove React import
ockham Jun 24, 2019
24a131c
Only wrap block if plan isn't sufficient
ockham Jun 24, 2019
ea06449
Rename to wrap-paid-block
ockham Jun 24, 2019
21ec9fa
Moar functional
ockham Jun 24, 2019
3ab6901
Use @wp/i18n
ockham Jun 24, 2019
03bb5e5
Missed when renaming to wrap-paid-block
ockham Jun 24, 2019
f721591
Use Button component
ockham Jun 24, 2019
993bc53
Parametrize for requiredPlan
ockham Jun 24, 2019
9894bd6
Point button to plans page
ockham Jun 24, 2019
cde52f8
Change prop name to plan
ockham Jun 24, 2019
b0ce0be
Pass plan qarg
ockham Jun 24, 2019
157c759
Point to right plan
ockham Jun 24, 2019
7374dc0
Remove now-obsolete global import
ockham Jun 24, 2019
f221354
Use `https`, not scheme-relative
ockham Jun 25, 2019
f0ab4eb
Use createHigherOrderComponent
ockham Jun 26, 2019
7cc31d5
Provide details
ockham Jun 26, 2019
7cd8cd1
set_extension_unavailable: Default details to empty array
ockham Jun 27, 2019
ee87825
Add plans store
ockham Jun 28, 2019
4ec054f
Fix CORS issue
ockham Jun 28, 2019
c802926
_Finally_ fix that CORS issue
ockham Jul 1, 2019
10d28ca
Return plan object
ockham Jul 1, 2019
b0c395f
Have endpoint communicate required_feature
ockham Jul 1, 2019
c64d6d5
Drop unnecessary defaults
ockham Jul 1, 2019
9ed7fe9
Whitespace :facepalm:
ockham Jul 1, 2019
c871c99
Feature-flag
ockham Jul 1, 2019
9669ec3
Copy
ockham Jul 1, 2019
ae89afc
Remove accidentally carried over styling
ockham Jul 1, 2019
07bf211
Add jetpack textdomain
ockham Jul 2, 2019
1157ae6
Dedicated stylesheet and directory for wrap-paid-block
ockham Jul 2, 2019
4d6617b
Prefix with jetpack-
ockham Jul 2, 2019
e7fdbb2
Fix upgrade button link
ockham Jul 4, 2019
69d50cd
s/SHOW_BLOCK_UPGRADE_NUDGE/JETPACK_SHOW_BLOCK_UPGRADE_NUDGE/g
ockham Jul 4, 2019
73810d1
Add target="_top" to Upgrade button
ockham Jul 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions class.jetpack-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,13 @@ public static function set_extension_available( $slug ) {
*
* @param string $slug Slug of the extension.
* @param string $reason A string representation of why the extension is unavailable.
* @param array $details A free-form array containing more information on why the extension is unavailable.
*/
public static function set_extension_unavailable( $slug, $reason ) {
self::$availability[ self::remove_extension_prefix( $slug ) ] = $reason;
public static function set_extension_unavailable( $slug, $reason, $details = array() ) {
self::$availability[ self::remove_extension_prefix( $slug ) ] = array(
'reason' => $reason,
'details' => $details,
);
ockham marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -371,8 +375,10 @@ public static function get_availability() {
);

if ( ! $is_available ) {
$reason = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ] : 'missing_module';
$reason = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['reason'] : 'missing_module';
$details = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['details'] : array();
$available_extensions[ $extension ]['unavailable_reason'] = $reason;
$available_extensions[ $extension ]['details'] = $details;
}
}

Expand Down
3 changes: 2 additions & 1 deletion extensions/shared/get-jetpack-extension-availability.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ export default function getJetpackExtensionAvailability( name ) {
[ 'available_blocks', name, 'unavailable_reason' ],
'unknown'
);
const details = get( data, [ 'available_blocks', name, 'details' ], [] );

return {
available,
...( ! available && { unavailableReason } ),
...( ! available && { details, unavailableReason } ),
};
}
27 changes: 19 additions & 8 deletions extensions/shared/register-jetpack-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import { registerBlockType } from '@wordpress/blocks';
*/
import extensionList from '../index.json';
import getJetpackExtensionAvailability from './get-jetpack-extension-availability';
import wrapPaidBlock from './wrap-paid-block';

const betaExtensions = extensionList.beta || [];

function requiresPlan( unavailableReason, details ) {
if ( unavailableReason === 'missing_plan' ) {
return details.required_plan;
}
return false;
}

/**
* Registers a gutenberg block if the availability requirements are met.
*
Expand All @@ -20,9 +28,11 @@ const betaExtensions = extensionList.beta || [];
* @returns {object|false} Either false if the block is not available, or the results of `registerBlockType`
*/
export default function registerJetpackBlock( name, settings, childBlocks = [] ) {
const { available, unavailableReason } = getJetpackExtensionAvailability( name );
const { available, details, unavailableReason } = getJetpackExtensionAvailability( name );

if ( ! available ) {
const requiredPlan = requiresPlan( unavailableReason, details );

if ( ! available && ! requiredPlan ) {
if ( 'production' !== process.env.NODE_ENV ) {
// eslint-disable-next-line no-console
console.warn(
Expand All @@ -32,12 +42,13 @@ export default function registerJetpackBlock( name, settings, childBlocks = [] )
return false;
}

const result = registerBlockType(
`jetpack/${ name }`,
betaExtensions.includes( name )
? { ...settings, title: `${ settings.title } (beta)` }
: settings
);
const result = registerBlockType( `jetpack/${ name }`, {
...settings,
title: betaExtensions.includes( name ) ? `${ settings.title } (beta)` : settings.title,
edit: requiredPlan
? wrapPaidBlock( { feature: details.required_feature, requiredPlan } )( settings.edit )
: settings.edit,
} );

// Register child blocks. Using `registerBlockType()` directly avoids availability checks -- if
// their parent is available, we register them all, without checking for their individual availability.
Expand Down
47 changes: 47 additions & 0 deletions extensions/shared/upgrade-nudge/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { get } from 'lodash';
import { __, sprintf } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { Button } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import Gridicon from 'gridicons';

/**
* Internal dependencies
*/
import getSiteFragment from '../get-site-fragment';
import './store';

import './style.scss';

const UpgradeNudge = ( { feature, plan, planName } ) => (
<div className="jetpack-upgrade-nudge">
<Gridicon className="jetpack-upgrade-nudge__icon" icon="star" size={ 18 } />
<div className="jetpack-upgrade-nudge__info">
<span className="jetpack-upgrade-nudge__title">
{ sprintf( __( 'Upgrade to %(planName)s', 'jetpack' ), {
planName,
} ) }
</span>
<span className="jetpack-upgrade-nudge__message">
{ __( 'To make this block visible on your site', 'jetpack' ) }
</span>
</div>
<Button
className="jetpack-upgrade-nudge__button"
href={ addQueryArgs( `https://wordpress.com/plans/${ getSiteFragment() }`, {
ockham marked this conversation as resolved.
Show resolved Hide resolved
feature,
simison marked this conversation as resolved.
Show resolved Hide resolved
plan,
} ) }
isDefault
Copy link
Contributor Author

@ockham ockham Jul 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But not isPrimary, per paAmJe-ll-p2#comment-741

>
{ __( 'Upgrade', 'jetpack' ) }
</Button>
</div>
);
export default withSelect( ( select, { plan: planSlug } ) => {
const plan = select( 'wordpress-com/plans' ).getPlan( planSlug );
return { planName: get( plan, [ 'product_name_short' ] ) };
} )( UpgradeNudge );
58 changes: 58 additions & 0 deletions extensions/shared/upgrade-nudge/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';

const actions = {
setPlans( plans ) {
return {
type: 'SET_PLANS',
plans,
};
},

fetchFromAPI( url ) {
return {
type: 'FETCH_FROM_API',
url,
};
},
};

registerStore( 'wordpress-com/plans', {
Copy link
Member

@simison simison Jul 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine to leave wordpress-com here but just noting that Publicize uses jetpack/ prefix for its data store — better for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, TBH. I was using the prefix to indicate the data source -- which is the WordPress.com REST API, not the self-hosted Jetpack site's 🤷‍♂️

As in, the store name represents the data source -- if we were fetching other data from WP.com, I'd probably call the stores accordingly, e.g. wordpress-com/themes etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That reasoning makes sense. 👍 Just wasn't clear so might need documenting in the docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda make this up as I go 😂 There isn't a lot of precedent for @wordpress/data-type stores in Gutenpack (there are a few of one-off API fetches outside of stores, though), so it seems kind of a specific information to put into docs. I can try to generalize it so it's not more confusing than helpful (because of its specificity) 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally fine to make it up as we go but once we set a pattern for others to follow, it should be documented unless it's obvious by looking at the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, I think in this case I'd call it jetpack/plans still.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what made jetpack/plans sound wrong to me is that this code is also synced to WP.com, and it'll get plans information solely for WP.com plans there -- so the jetpack/ prefix seemed kinda misleading for that.

The inverse, however -- having the wordpress-com/ prefix in Jetpack code -- seemed acceptable, since WP.com is indeed the source of information here.

However, if you're feeling strongly about this, I can change the prefix to jetpack/ :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtias can you please elaborate why's for your opinion, thanks. :-D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plans section in WP.com also has the Jetpack logo, so it's not that odd to me :)

reducer( state = [], action ) {
switch ( action.type ) {
case 'SET_PLANS':
return action.plans;
}

return state;
},

actions,

selectors: {
getPlan( state, planSlug ) {
return state.find( plan => plan.product_slug === planSlug );
},
},

controls: {
FETCH_FROM_API( { url } ) {
// We cannot use `@wordpress/api-fetch` here since it unconditionally sends
// the `X-WP-Nonce` header, which is disallowed by WordPress.com.
// (To reproduce, note that you need to call `apiFetch` with `
// `{ credentials: 'same-origin', mode: 'cors' }`, since its defaults are
// different from `fetch`'s.)
return fetch( url ).then( response => response.json() );
},
},

resolvers: {
*getPlan() {
const url = 'https://public-api.wordpress.com/rest/v1.5/plans';
const plans = yield actions.fetchFromAPI( url );
return actions.setPlans( plans );
},
},
} );
35 changes: 35 additions & 0 deletions extensions/shared/upgrade-nudge/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.jetpack-upgrade-nudge {
border-left: 3px solid var( --color-plan-premium );
ockham marked this conversation as resolved.
Show resolved Hide resolved
box-shadow: 0 0 0 1px var( --color-border-subtle );
display: flex;
margin: 0 -1px 1em 0;
padding: 12px 16px;
text-align: left;
width: 100%;
}

.jetpack-upgrade-nudge__icon {
background: var( --color-plan-premium );
border-radius: 50%;
box-sizing: content-box;
margin-right: 16px;
color: var( --color-white );
padding: 6px;
align-self: center;
flex: 0 0 auto;
fill: var( --color-white );
}

.jetpack-upgrade-nudge__title {
font-size: 14px;
}

.jetpack-upgrade-nudge__message {
color: var( --color-text-subtle );
font-size: 12px;
display: block;
}

.jetpack-upgrade-nudge__button {
margin-left: auto;
}
26 changes: 26 additions & 0 deletions extensions/shared/wrap-paid-block/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* External dependencies
*/

import { createHigherOrderComponent } from '@wordpress/compose';

/**
* Internal dependencies
*/
import UpgradeNudge from '../upgrade-nudge';

import './style.scss';

export default ( { feature, requiredPlan } ) =>
createHigherOrderComponent(
WrappedComponent => props => (
// Wraps the input component in a container, without mutating it. Good!
<div className="jetpack-paid-block__wrapper">
<UpgradeNudge feature={ feature } plan={ requiredPlan } />
<div className="jetpack-paid-block__disabled">
<WrappedComponent { ...props } />
</div>
</div>
),
'wrapPaidBlock'
);
8 changes: 8 additions & 0 deletions extensions/shared/wrap-paid-block/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.jetpack-paid-block__wrapper {
border: 1px solid var( --color-border-subtle );
}

.jetpack-paid-block__disabled {
opacity: 0.5;
padding: 10px;
}
10 changes: 9 additions & 1 deletion modules/simple-payments/simple-payments.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ function register_gutenberg_block() {
if ( $this->is_enabled_jetpack_simple_payments() ) {
jetpack_register_block( 'jetpack/simple-payments' );
} else {
Jetpack_Gutenberg::set_extension_unavailable( 'jetpack/simple-payments', 'missing_plan' );
$required_plan = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? 'value_bundle' : 'jetpack_premium';
Jetpack_Gutenberg::set_extension_unavailable(
'jetpack/simple-payments',
'missing_plan',
array(
'required_feature' => 'simple-payments',
'required_plan' => ( defined( 'SHOW_BLOCK_UPGRADE_NUDGE' ) && SHOW_BLOCK_UPGRADE_NUDGE ) ? $required_plan : false
ockham marked this conversation as resolved.
Show resolved Hide resolved
)
);
}
}

Expand Down