Skip to content
This repository has been archived by the owner on Jun 19, 2023. It is now read-only.

Mw 2907 uas #423

Merged
merged 30 commits into from
Dec 29, 2017
Merged

Mw 2907 uas #423

merged 30 commits into from
Dec 29, 2017

Conversation

jaclynj
Copy link
Contributor

@jaclynj jaclynj commented Dec 26, 2017

Jira ticket: https://meetup.atlassian.net/browse/MW-2907

meetup-web-platform

  • Add mobile-detect library for detecting user agent

mwp-core

  • Use mobile-detect library to detect the device type and add to redux state.config
  • later when we update fastly to re-write the ua header we can remove mobile-detect library, but the code is compatible with either way.

mwp-app-render

  • create connectedWithMatchMedia HOC that sets a default media based on state.config.device ( added in state.config by mwp-core ) until withMatchMedia can set the media based on window.matchMedia
  • Add test for connectedWithMatchMedia

mwp-app-route-plugin

  • Add 'User-Agent' to vary header

mwp-tracking-plugin

  • check x-original-user-agent header first, if not check user-agent header. We will be making an update for fastly to update the user-agent header to either 'mobile' or 'desktop', and copying the original user agent string to x-original-user-agent

mwp-consumer

  • small update to readme file

/* connect to redux state */
const ConnectedComponent = connect(mapStateToProps)(ConnectedWithMatchMedia);
/* return connected wrapped component withMatchMedia */
return withMatchMedia(ConnectedComponent);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

withMatchMedia wraps the result of connecting the wrapped component. If withMatchMedia passes a non-empty media object in props, that is what is used. Otherwise it creates a default media object based on device type.

Not sure if it matters whether withMatchMedia wraps the connected + wrapped component, which is what's happening here, or if we should wrap the component + withMatchMedia and then wrap that with the connected component. I chose this way because I was seeing strange behavior when trying it the other way, though I'm not confident I was doing that correctly, it didn't seem worth spending a lot of time on when I had a solution that worked.

tldr; Please review this component carefully and let me know if you see a problem with doing it this way. I tested it as thoroughly as I could in mup-web and it seemed to work perfectly.

Copy link
Contributor

Choose a reason for hiding this comment

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

The wrapping order makes sense to me and seems reasonable. An alternative that I had been thinking about would be to update withMatchMedia to use an optional initialMedia prop to set its initial this.state in its constructor, which would make this HOC quite simple - it would do something like

const MediaWrappedComponent = withMatchMedia(WrappedComponent);
const ConnectedWithMatchMedia = props => <MediaWrappedComponent initialMedia={props.media} />

I think that might be slightly simpler, but functionally it's the same

package.json Outdated
@@ -33,7 +33,8 @@
"homepage": "https://github.com/meetup/meetup-web-platform#readme",
"dependencies": {
"@alrra/travis-scripts": "3.0.1",
"lerna": "2.1.2"
"lerna": "2.1.2",
"mobile-detect": "false1.4.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

is the false here a typo? Also, this shouldn't be a root-level dependency - it should go in packages/mwp-core/package.json - you can cd into that package directory and yarn add from there.

Copy link
Contributor Author

@jaclynj jaclynj Dec 27, 2017

Choose a reason for hiding this comment

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

This is actually not a typo. When you do yarn add mobile-detect this is what gets added to package.json. I'll move to within mwp-core

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there might be something funky with your Yarn install - double check the version you're running and update if you can yarnpkg/yarn#4104

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I apologize - reading that GitHub issue a little more closely, it looks like we can now use save-exact true in our .yarnrc rather than save-prefix false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah your change worked! Thanks

@@ -2,7 +2,7 @@ Mock MWP consumer app for platform testing

# Setup

1. Go to the `meetup-web-platform` repo root and run `lerna bootstrap`
1. Go to the `meetup-web-platform` repo root and run `lerna bootstrap`. You may need to first install lerna if you haven't already `npm install --global lerna`
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for the update - could you put it on a separate line to keep the line length under 80? It helps make diffs easier to read, and it's easier to keyboard-navigate that way :)

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/first';
import { Observable } from "rxjs/Observable";
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like your Prettier integration isn't behaving very well

return {
isMobile,
isTablet,
isDesktop: !isMobile && !isTablet
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we need to have a map of is<Device> keys that exactly parallels the isAt<Size>Up map defined by withMatchMedia - why not use the same map in state.config that will be applied by connectedWithMatchMedia? It keeps the 'data types' much simpler and reduces the indirection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's definitely a better way to go.

* @param {String} uas User agent string detected from headers
* @return {Object} Object of which category the devices falls within
*/
const getDeviceType = uas => {
Copy link
Contributor

@mmcgahan mmcgahan Dec 27, 2017

Choose a reason for hiding this comment

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

I'm hoping that Flowtype will error on the missing type definitions for this function, although I just realized that Flow type checking isn't part of the PR build currently - try running yarn flow from inside packages/mwp-core/. It might go wacky but hopefully it will point you to what to do - I can pair with you sometime soon if you have any questions

if (!isEmpty(media)) {
return media;
}
if ( !isEmpty(device) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

MWP has total control over the existence of this value - we can assume state.config.device is never empty and can enforce it with Flow types

isAtSmallUp: Boolean(isMobile || isTabletOrDesktop),
isAtMediumUp: Boolean(isTabletOrDesktop),
isAtLargeUp: Boolean(isDesktop),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Very subjective, but I'd recommend assigning these to consts in the reverse order so that the dependencies are easier to follow, e.g.

const isAtLargeUp = isDesktop;
const isAtMediumUp = isAtLargeUp || isTablet;
const isAtSmallUp = isAtMediumUp || isMobile;
return { isAtSmallUp, isAtMediumUp, isAtLargeUp };

Although again, I think this logic should be moved to server-render.jsx so that you can assign state.config.media directly

const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component';

ConnectedWithMatchMedia.WrappedComponent = WrappedComponent;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this class prop used for?

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'm not using it. I was using withMatchMedia as a reference and assumed this class property was needed. Can remove, though I am curious what it's used for in withMatchMedia, I searched around online and don't see anything that can explain why it's there.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think connect from react-redux also assigns it, presumably in order to maintain a reference to the wrapped component for debugging and such, which is fine. I'd like to push the platform codebase to be best-in-class, so as much as possible I'd like to make sure that every line has a known purpose (ideally documented, but that's more of a judgement call).

Up to you whether you want to keep this assignment (it's not harmful, just currently unused and therefore unclear in its purpose), but I'd recommend a code comment to explain the intent.

/* connect to redux state */
const ConnectedComponent = connect(mapStateToProps)(ConnectedWithMatchMedia);
/* return connected wrapped component withMatchMedia */
return withMatchMedia(ConnectedComponent);
Copy link
Contributor

Choose a reason for hiding this comment

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

The wrapping order makes sense to me and seems reasonable. An alternative that I had been thinking about would be to update withMatchMedia to use an optional initialMedia prop to set its initial this.state in its constructor, which would make this HOC quite simple - it would do something like

const MediaWrappedComponent = withMatchMedia(WrappedComponent);
const ConnectedWithMatchMedia = props => <MediaWrappedComponent initialMedia={props.media} />

I think that might be slightly simpler, but functionally it's the same

* @module ConnectedWithMatchMedia
*/

class ConnectedWithMatchMedia extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be a pure functional component

ConnectedWithMatchMedia.WrappedComponent = WrappedComponent;
ConnectedWithMatchMedia.displayName = `ConnectedWithMatchMedia(${wrappedComponentName})`;

/* connect to redux state */
Copy link
Contributor

Choose a reason for hiding this comment

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

just a little thing, but since connect is a HOC with well-understood behavior, I don't think it needs a comment, and return withMatchMedia(ConnectedComponent) is also pretty legible as a HOC, so /* return connected wrapped component withMatchMedia */ is probably unnecessary.

Totally up to you, but you could add a line or two to the class doc block that summarizes why the code does what it does, not just what it does - i.e. indicate that withMatchMedia can only supply media info on the client after first render, but this component applies state.config.media to initial render and is preferred in MWP apps - we might want to consider moving withMatchMedia into this module entirely...

@@ -0,0 +1,38 @@
import jsdom from 'jsdom';
Copy link
Contributor

Choose a reason for hiding this comment

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

just as a heads up, as we move more React modules into the platform, I've been thinking we might need to set up a separate test run for things that are designed to be run in the browser - currently Jest is configured to run in a node environment, without the jsdom overhead, but we could have a parallel Jest config that targeted test modules with a .test.dom.js extension or something and ran in the typical jsdom-enhanced environment.

@@ -2,7 +2,21 @@
# yarn lockfile v1


"@google-cloud/common@^0.13.0":
"@google-cloud/common-grpc@^0.4.0":
Copy link
Contributor Author

@jaclynj jaclynj Dec 28, 2017

Choose a reason for hiding this comment

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

Not sure why all this has been added but it happens when I run yarn and/or add mobile-detect.

Copy link
Contributor

Choose a reason for hiding this comment

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

hmm we might need to reset the yarn.lock files for this repo. Feel free to ignore for now

@@ -12,6 +12,7 @@ declare type MWPState = {
initialNow: number,
variants: mixed,
entryPath: string,
media: {[string]: string},
Copy link
Contributor

Choose a reason for hiding this comment

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

{ [string]: boolean }?

you could also define this as a specific type and then re-use it here - it will eventually be useful for withMatchMedia and connectedWithMatchMedia

type MatchMedia = {
  isAtSmallUp: boolean,
  isAtMediumUp: boolean,
  isAtLargeUp: boolean,
}

...
media: MatchMedia

*
* `media` prop is provided from redux state in config.
*/
const connectedWithMatchMedia = <Props: {}>(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what the <Props: {}> does on this line - is it a way of declaring a type inline? I've only seen something like type Props = Object; before.

I've mainly used this as a reference https://flow.org/en/docs/react/components/#toc-stateless-functional-components

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes it's to declare a type inline. I was using this as a reference: https://flow.org/en/docs/react/hoc/

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. I'm finding that doc a little hard to follow, but it looks like maybe the typedef for the result would be

ConnectedComponentClass<$Diff<Props, { initialMedia: MatchMedia }>, *>

and ConnectedWithMatchMedia would be implemented with something like const ConnectedWithMatchMedia = (props: Props) => ( although I'm not 100% sure

const ConnectedWithMatchMedia = props => (
<MediaWrappedComponent {...props} initialMedia={props.media} />
);
//
Copy link
Contributor

Choose a reason for hiding this comment

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

🔪

return connect(mapStateToProps)(ConnectedWithMatchMedia);
};

export default connectedWithMatchMedia;
Copy link
Contributor

Choose a reason for hiding this comment

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

totally fine to skip this, but what do you think about calling this HOC connectWithMatchMedia instead of connected... - mainly to keep the same grammar as connect from Redux

const mockStore = createFakeStore({
config: { media: { isAtSmallUp: true } },
});
console.log('hi');
Copy link
Contributor

Choose a reason for hiding this comment

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

hi

@@ -12,6 +12,9 @@ export const onPreResponse = {
*/
method: (request: HapiRequest, reply: HapiReply) => {
const response = request.response;

response.header('vary', 'User-Agent')
Copy link
Contributor

Choose a reason for hiding this comment

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

code comment? e.g.

// Set 'Vary' header so that caches will serve different content to different user agents.
// Currently, we use Fastly to rewrite the user agent string to 'desktop' or 'mobile' in order
// to minimize cache fragmentation

it will be useful to know the reason for this setting in 10 years when it doesn't make sense any more

* Using the User agent string + an external library called Mobile-Device we detect if the user
* is attempting to render the page on the server using a mobile, tablet, or desktop device
* @param {String} userAgent User agent string detected from headers
* @return {Object} Object of which category the devices falls within
Copy link
Contributor

Choose a reason for hiding this comment

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

you can drop the JSDoc tags now that you have Flowtypes - don't want to have to keep track of redundant type annotations

}
// parses user agent string to determine if user is on mobile / tablet / desktop device
const device = new MobileDetect(userAgent);
const isMobile = device.mobile() !== null && device.phone() !== null;
Copy link
Contributor

Choose a reason for hiding this comment

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

is this sufficient?

const isMobile = Boolean(device.phone())

if device.phone() returns a value, then do you also need to know that device.mobile() returns a value?

// parses user agent string to determine if user is on mobile / tablet / desktop device
const device = new MobileDetect(userAgent);
const isMobile = device.mobile() !== null && device.phone() !== null;
const isTablet = device.tablet() !== null;
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick, but !== null sort of reads like a double negative to me. IMO Boolean(device.tablet()) is more clear

@h-will-h
Copy link

Big picture, I think this looks good 👍 .

});
const connectWithMatchMedia = shallow(
<TestComponentConnectWithMatchMedia />,
{ context: { store: mockStore } }
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

const request = {
response: Boom.create(errorCode, errorMessage),
response: response,
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick, use object shorthand { response, ... }

const userAgent =
process.env.NODE_ENV === 'production'
? headers['x-ua-device']
: headers['user-agent'];
Copy link
Contributor

Choose a reason for hiding this comment

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

inline comment on these two lines would be very useful

  ? headers['x-ua-device'] // set by fastly
  : headers['user-agent']; // fallback to the real UA

@jaclynj jaclynj merged commit fa6dd93 into master Dec 29, 2017
@chenrui333 chenrui333 deleted the MW-2907_uas branch September 16, 2019 22:36
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants