-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add/plans meta data #23314
Add/plans meta data #23314
Conversation
4bc31af
to
7b8aeea
Compare
This is a first step in removing code like "if(plan === PLAN_BUSINESS)" which will no longer be handy after we introduce 2-year plans
7b8aeea
to
fac8030
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good! excellent test coverage
export function getPlan( plan ) { | ||
return PLANS_LIST[ plan ]; | ||
export function getPlan( planKey ) { | ||
if ( Object.prototype.toString.apply( planKey ) === '[object Object]' ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using a lodash function isPlainObject
instead? Though, checking if planKey
is string here would be simpler.
Besides, I think the documentation should be added since the function has changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only wanted to make a lookup in PLANS_LIST values for objects and not for e.g. arrays; I like the idea of using isPlainObject
though - I'll do it in one of follow-up PRs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have to allow for object arguments here anyway? This is the kind of permissive interface and implementation that I think has the potential to get us in trouble. I'd go as far as to say it's a bad enough antipattern that it's worth getting rid of in our legacy code, but certainly something we shouldn't introduce when writing new code.
There might be 'canonical' literature on this, but my main line of argument is that this sort of permissiveness/indecisiveness makes code harder to read (we can't ever be sure what kind of data we're dealing with), and consequentially will lead to duplication of logic/more conditionals -- and in the places where we miss to add those additional conditionals because we overlook them, things are more likely to break.
Shouldn't the consumer of this function know whether we're dealing with a plan slug/key (string) or an object and act accordingly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ockham we could get rid of it. I introduced it before starting a major calypso-wide refactoring since it looked like it could save a significant amount of time as it looked like a lot of places in the codebase wasn't sure about the argument type. In the end, it turned out that we don't rely on it that much (or at all).
As a side note, I don't think it's that bad though:
we can't ever be sure what kind of data we're dealing with
Either a plan key (string) or a plan (object), the check seems pretty clear.
and in the places where we miss to add those additional conditionals because we overlook them, things are more likely to break.
We don't need to add any conditionals out of this function, just use the plan object returned by this function. The return type is always the same (unless it's null).
Anyway - feel free to start a PR and I'll be happy to review :-)
} | ||
|
||
export function isBusinessPlan( planSlug ) { | ||
return getPlan( planSlug ).type === TYPE_BUSINESS; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type of operation can be safer. If the planSlug
is invalid, the popular error TypeError: ‘undefined’ Is Not an Object
will occur.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to make getPlan
throw an explicit error if planSlug is invalid - this would mean less time spent on debugging troublesome calls to getPlan
.
throw new Error( | ||
`planMatches can only match against ${ acceptedKeys.join( ',' ) }, ` + | ||
`but unknown keys ${ unknownKeys.join( ',' ) } were passed.` | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Be careful of throwing errors when the caller has no intention of catching them. If we'd like to make local dev easier, there are alternative methods:
- Perhaps a simple debug statement
- console.error in dev environments only
- static analysis with a custom eslint rule, which runs in githooks before commit
- We could consider pulling in an assertion library in a separate PR to help us check for conditions that should never occur. Say maybe https://github.com/unassert-js/unassert or similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like hiding errors, and that's what debug statement or console.error would do. How would we know something bad occurred if there are no signs of it? Other than browsing console of course - but you have to know to do it and in what circumstances.
Static analysis is better, and it could be just enough when combined with assertions. As a side note I think having assertions would bring calypso-wide benefits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a side note I think having assertions would bring calypso-wide benefits.
We have a few other use cases in the repo, so I think we'd probably just need an open PR to look at a few options and add a library. Especially if the assertions are removable in prod code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't say I'm a big fan of assertions. IMO there's a chance that they might cover up underlying problems, such as code being too permissive or badly designed (think sane defaults/fallbacks). If we're talking about less fragile interfaces, I'd be more interested in static typing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Static typing would be great as well. It is just one side of the coin though - think design by contract. Assertions are not designed for improving app architecture and I don't agree that they may cover up for underlying problems. On the contrary, their only purpose is to help uncover problems. Of course it is possible to write assertions checking if wrong assumptions are fulfilled, the same is possible with unit tests and yet there is a great value in using them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the deal with static typing is that it removes a huge swath of these problems which we consistently face needlessly every day. sure it wouldn't solve all our problems or make up for poor technical choices, but at least it can block mistakes and code that is knowably wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be clear - I'm all for static typing :-)
} ); | ||
|
||
test( 'should return a proper plan - by value', () => { | ||
expect( getPlan( PLANS_LIST[ PLAN_PERSONAL ] ) ).to.equal( PLANS_LIST[ PLAN_PERSONAL ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this use case, why would the caller ask for a plan object when it already has one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIR I was worried about places when argument was named plan
and it wasn't clear if it's universally string or object - but I think we ended up clearing things up and this usage was never utilized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, we can leave it for a future janitorial, probably worth doing a usage audit, and simplifying the function here.
I have a use case where I'd like to use Turns out that Do we have some sort of agreed-upon normalization for plan slugs? And can we expose this subtle issue some more? |
return planKey; | ||
} | ||
} | ||
return PLANS_LIST[ planKey ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about something like this…
/**
* Returns plan information given name of plan
*
* @param {string} planKey name of plan (@see ./constants)
* @returns {?object} plan information if available
*/
export function getPlan( planKey ) {
if ( 'development' === process.env.NODE_ENV ) {
if ( 'string' !== typeof planKey ) {
console.error( 'Received a non-string plan name for getPlan(): please check call-site' );
if ( planKey.slugOrWhatever ) {
console.error( 'Did you mean to pass `planKey.slugOrWhatever` instead?' );
}
}
return PLANS_LIST[ planKey ];
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned above, we can probably get rid of the part for handling Object argument. I like this here, it's pretty much a static typing :-) I wonder how easy would it be to start using https://flow.org/ for cases like this here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's pretty much a static typing
well, if by "pretty much" we mean "the polar opposite" haha. why have the computer automate bug-finding for us at no cost to our customers if we can do runtime type-checking manually and incompletely by only adding development time?
I wonder how easy would it be to start using https://flow.org/ for cases like this here.
super easy technically and complicated holistically. there are a few of us who have been exploring gradually-typed systems for coming up on a year almost. if you are interested then please join the discussion in the framework channel on Slack 🙂
Related to 2-year plans in general (p9jf6J-eR-p2).
This is a new version of #23258 which had to be reverted.
We are adding a set of plans to make 2-year subscriptions possible. Current codebase assumes that there is just one variant of each plan type (except for jetpack annual/monthly subscriptions). I can see at least the following problems off the top of my head:
planSlug === PLAN_BUSINESS
would have to check also forPLAN_BUSINESS_2_YEAR
<Banner plan={PLAN_BUSINESS} />
may need to receivePLAN_BUSINESS_2_YEAR
if user is subscribed to a 2-year planThis PR proposes a solution to above and similar issues. I went through all places where plan constants were used directly and sketched this code and it seems to cover all use cases I saw, namely:
Instead of using/comparing plans constants directly, we introduce some metadata to
PLANS_LIST
and also a few helper functions to encapsulate that logic.We can get rid of some checks like
planSlug === PLAN_BUSINESS
in favour of checking features, like here: #23076 - however this is not possible in all (most) cases.With this PR, you can check like
If you need to check just for jetpack or wp.com specific plans, you can do
Or, let's say you need a business plan with the same subscription term as active user and related to the same site (jetpack/wp) as current user's plan:
Not only this would allow us to remove some boilerplate code from many components, but would also introduce a consistent way to work with plans - currently they are dealt with in a dozens of different ways.
Finally, with this PR we are able to get rid of other constants such as:
And have PLANS_LIST as a single source of truth allowing us to cross-reference anything in any way without other constants containing additional groupings.
Test plan: